# tests/unit/services/test_merchant_domain_service.py
"""Unit tests for MerchantDomainService."""

import uuid
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

import pytest
from pydantic import ValidationError

from app.modules.tenancy.exceptions import (
    DomainAlreadyVerifiedException,
    DomainNotVerifiedException,
    DomainVerificationFailedException,
    MaxDomainsReachedException,
    MerchantDomainAlreadyExistsException,
    MerchantDomainNotFoundException,
    MerchantNotFoundException,
)
from app.modules.tenancy.models.merchant_domain import MerchantDomain
from app.modules.tenancy.models.store_domain import StoreDomain
from app.modules.tenancy.schemas.merchant_domain import (
    MerchantDomainCreate,
    MerchantDomainUpdate,
)
from app.modules.tenancy.services.merchant_domain_service import (
    merchant_domain_service,
)

# =============================================================================
# ADD DOMAIN TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantDomainServiceAdd:
    """Test suite for adding merchant domains."""

    def test_add_domain_success(self, db, test_merchant):
        """Test successfully adding a domain to a merchant."""
        unique_id = str(uuid.uuid4())[:8]
        domain_data = MerchantDomainCreate(
            domain=f"newmerchant{unique_id}.example.com",
            is_primary=True,
        )

        result = merchant_domain_service.add_domain(
            db, test_merchant.id, domain_data
        )
        db.commit()

        assert result is not None
        assert result.merchant_id == test_merchant.id
        assert result.domain == f"newmerchant{unique_id}.example.com"
        assert result.is_primary is True
        assert result.is_verified is False
        assert result.is_active is False
        assert result.verification_token is not None

    def test_add_domain_merchant_not_found(self, db):
        """Test adding domain to non-existent merchant raises exception."""
        domain_data = MerchantDomainCreate(
            domain="test.example.com",
            is_primary=True,
        )

        with pytest.raises(MerchantNotFoundException):
            merchant_domain_service.add_domain(db, 99999, domain_data)

    def test_add_domain_already_exists_as_merchant_domain(
        self, db, test_merchant
    ):
        """Test adding a domain that already exists as MerchantDomain raises exception."""
        unique_id = str(uuid.uuid4())[:8]
        existing = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"existing{unique_id}.example.com",
            verification_token=f"ext_{unique_id}",
        )
        db.add(existing)
        db.commit()

        domain_data = MerchantDomainCreate(
            domain=f"existing{unique_id}.example.com",
            is_primary=True,
        )

        with pytest.raises(MerchantDomainAlreadyExistsException):
            merchant_domain_service.add_domain(
                db, test_merchant.id, domain_data
            )

    def test_add_domain_already_exists_as_store_domain(
        self, db, test_merchant, test_store
    ):
        """Test adding a domain that already exists as StoreDomain raises exception."""
        unique_id = str(uuid.uuid4())[:8]
        sd = StoreDomain(
            store_id=test_store.id,
            domain=f"storeexist{unique_id}.example.com",
            verification_token=f"se_{unique_id}",
        )
        db.add(sd)
        db.commit()

        domain_data = MerchantDomainCreate(
            domain=f"storeexist{unique_id}.example.com",
            is_primary=True,
        )

        with pytest.raises(MerchantDomainAlreadyExistsException):
            merchant_domain_service.add_domain(
                db, test_merchant.id, domain_data
            )

    def test_add_domain_max_limit_reached(self, db, test_merchant):
        """Test adding domain when max limit reached raises exception."""
        for i in range(merchant_domain_service.max_domains_per_merchant):
            domain = MerchantDomain(
                merchant_id=test_merchant.id,
                domain=f"limit{i}_{uuid.uuid4().hex[:6]}.example.com",
                verification_token=f"lim_{i}_{uuid.uuid4().hex[:6]}",
            )
            db.add(domain)  # noqa: PERF006
        db.commit()

        domain_data = MerchantDomainCreate(
            domain="onemore.example.com",
            is_primary=True,
        )

        with pytest.raises(MaxDomainsReachedException):
            merchant_domain_service.add_domain(
                db, test_merchant.id, domain_data
            )

    def test_add_domain_reserved_subdomain(self):
        """Test adding a domain with reserved subdomain is rejected by schema."""
        with pytest.raises(ValidationError) as exc_info:
            MerchantDomainCreate(
                domain="admin.example.com",
                is_primary=True,
            )

        assert "reserved subdomain" in str(exc_info.value).lower()

    def test_add_domain_sets_primary_unsets_others(self, db, test_merchant):
        """Test adding a primary domain unsets other primary domains."""
        unique_id = str(uuid.uuid4())[:8]
        first = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"first{unique_id}.example.com",
            is_primary=True,
            verification_token=f"f_{unique_id}",
        )
        db.add(first)
        db.commit()

        domain_data = MerchantDomainCreate(
            domain=f"second{unique_id}.example.com",
            is_primary=True,
        )
        result = merchant_domain_service.add_domain(
            db, test_merchant.id, domain_data
        )
        db.commit()
        db.refresh(first)

        assert result.is_primary is True
        assert first.is_primary is False


# =============================================================================
# GET DOMAINS TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantDomainServiceGet:
    """Test suite for getting merchant domains."""

    def test_get_merchant_domains_success(self, db, test_merchant):
        """Test getting all domains for a merchant."""
        unique_id = str(uuid.uuid4())[:8]
        d1 = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"get1{unique_id}.example.com",
            verification_token=f"g1_{unique_id}",
        )
        d2 = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"get2{unique_id}.example.com",
            verification_token=f"g2_{unique_id}",
            is_primary=False,
        )
        db.add_all([d1, d2])
        db.commit()

        domains = merchant_domain_service.get_merchant_domains(
            db, test_merchant.id
        )

        domain_names = [d.domain for d in domains]
        assert f"get1{unique_id}.example.com" in domain_names
        assert f"get2{unique_id}.example.com" in domain_names

    def test_get_merchant_domains_merchant_not_found(self, db):
        """Test getting domains for non-existent merchant raises exception."""
        with pytest.raises(MerchantNotFoundException):
            merchant_domain_service.get_merchant_domains(db, 99999)

    def test_get_domain_by_id_success(self, db, test_merchant):
        """Test getting a domain by ID."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"byid{unique_id}.example.com",
            verification_token=f"bi_{unique_id}",
        )
        db.add(domain)
        db.commit()
        db.refresh(domain)

        result = merchant_domain_service.get_domain_by_id(db, domain.id)
        assert result.id == domain.id

    def test_get_domain_by_id_not_found(self, db):
        """Test getting non-existent domain raises exception."""
        with pytest.raises(MerchantDomainNotFoundException):
            merchant_domain_service.get_domain_by_id(db, 99999)


# =============================================================================
# UPDATE DOMAIN TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantDomainServiceUpdate:
    """Test suite for updating merchant domains."""

    def test_update_domain_set_primary(self, db, test_merchant):
        """Test setting a domain as primary."""
        unique_id = str(uuid.uuid4())[:8]
        d1 = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"upd1{unique_id}.example.com",
            is_primary=True,
            is_verified=True,
            verified_at=datetime.now(UTC),
            verification_token=f"u1_{unique_id}",
        )
        d2 = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"upd2{unique_id}.example.com",
            is_primary=False,
            is_verified=True,
            verified_at=datetime.now(UTC),
            verification_token=f"u2_{unique_id}",
        )
        db.add_all([d1, d2])
        db.commit()

        update_data = MerchantDomainUpdate(is_primary=True)
        result = merchant_domain_service.update_domain(
            db, d2.id, update_data
        )
        db.commit()
        db.refresh(d1)

        assert result.is_primary is True
        assert d1.is_primary is False

    def test_update_domain_activate_unverified_fails(self, db, test_merchant):
        """Test activating an unverified domain raises exception."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"unvact{unique_id}.example.com",
            is_verified=False,
            is_active=False,
            verification_token=f"ua_{unique_id}",
        )
        db.add(domain)
        db.commit()

        update_data = MerchantDomainUpdate(is_active=True)

        with pytest.raises(DomainNotVerifiedException):
            merchant_domain_service.update_domain(db, domain.id, update_data)

    def test_update_domain_activate_verified(self, db, test_merchant):
        """Test activating a verified domain succeeds."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"veract{unique_id}.example.com",
            is_verified=True,
            verified_at=datetime.now(UTC),
            is_active=False,
            verification_token=f"va_{unique_id}",
        )
        db.add(domain)
        db.commit()

        update_data = MerchantDomainUpdate(is_active=True)
        result = merchant_domain_service.update_domain(
            db, domain.id, update_data
        )
        db.commit()

        assert result.is_active is True

    def test_update_domain_deactivate(self, db, test_merchant):
        """Test deactivating a domain."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"deact{unique_id}.example.com",
            is_verified=True,
            verified_at=datetime.now(UTC),
            is_active=True,
            verification_token=f"da_{unique_id}",
        )
        db.add(domain)
        db.commit()

        update_data = MerchantDomainUpdate(is_active=False)
        result = merchant_domain_service.update_domain(
            db, domain.id, update_data
        )
        db.commit()

        assert result.is_active is False

    def test_update_domain_not_found(self, db):
        """Test updating non-existent domain raises exception."""
        update_data = MerchantDomainUpdate(is_primary=True)

        with pytest.raises(MerchantDomainNotFoundException):
            merchant_domain_service.update_domain(db, 99999, update_data)


# =============================================================================
# DELETE DOMAIN TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantDomainServiceDelete:
    """Test suite for deleting merchant domains."""

    def test_delete_domain_success(self, db, test_merchant):
        """Test successfully deleting a domain."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"todel{unique_id}.example.com",
            verification_token=f"td_{unique_id}",
        )
        db.add(domain)
        db.commit()
        domain_id = domain.id

        result = merchant_domain_service.delete_domain(db, domain_id)
        db.commit()

        assert "deleted successfully" in result

        with pytest.raises(MerchantDomainNotFoundException):
            merchant_domain_service.get_domain_by_id(db, domain_id)

    def test_delete_domain_not_found(self, db):
        """Test deleting non-existent domain raises exception."""
        with pytest.raises(MerchantDomainNotFoundException):
            merchant_domain_service.delete_domain(db, 99999)


# =============================================================================
# VERIFY DOMAIN TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantDomainServiceVerify:
    """Test suite for merchant domain verification."""

    @patch("dns.resolver.resolve")
    def test_verify_domain_success(self, mock_resolve, db, test_merchant):
        """Test successful domain verification."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"verify{unique_id}.example.com",
            is_verified=False,
            verification_token=f"vt_{unique_id}",
        )
        db.add(domain)
        db.commit()

        mock_txt = MagicMock()
        mock_txt.to_text.return_value = f'"vt_{unique_id}"'
        mock_resolve.return_value = [mock_txt]

        result_domain, message = merchant_domain_service.verify_domain(
            db, domain.id
        )
        db.commit()

        assert result_domain.is_verified is True
        assert result_domain.verified_at is not None
        assert "verified successfully" in message.lower()

    def test_verify_domain_already_verified(self, db, test_merchant):
        """Test verifying already verified domain raises exception."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"alrver{unique_id}.example.com",
            is_verified=True,
            verified_at=datetime.now(UTC),
            verification_token=f"av_{unique_id}",
        )
        db.add(domain)
        db.commit()

        with pytest.raises(DomainAlreadyVerifiedException):
            merchant_domain_service.verify_domain(db, domain.id)

    def test_verify_domain_not_found(self, db):
        """Test verifying non-existent domain raises exception."""
        with pytest.raises(MerchantDomainNotFoundException):
            merchant_domain_service.verify_domain(db, 99999)

    @patch("dns.resolver.resolve")
    def test_verify_domain_token_not_found(
        self, mock_resolve, db, test_merchant
    ):
        """Test verification fails when token not found in DNS."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"tnf{unique_id}.example.com",
            is_verified=False,
            verification_token=f"tnf_{unique_id}",
        )
        db.add(domain)
        db.commit()

        mock_txt = MagicMock()
        mock_txt.to_text.return_value = '"wrong_token"'
        mock_resolve.return_value = [mock_txt]

        with pytest.raises(DomainVerificationFailedException) as exc_info:
            merchant_domain_service.verify_domain(db, domain.id)

        assert "token not found" in str(exc_info.value).lower()

    @patch("dns.resolver.resolve")
    def test_verify_domain_dns_nxdomain(
        self, mock_resolve, db, test_merchant
    ):
        """Test verification fails when DNS record doesn't exist."""
        import dns.resolver

        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"nxdom{unique_id}.example.com",
            is_verified=False,
            verification_token=f"nx_{unique_id}",
        )
        db.add(domain)
        db.commit()

        mock_resolve.side_effect = dns.resolver.NXDOMAIN()

        with pytest.raises(DomainVerificationFailedException):
            merchant_domain_service.verify_domain(db, domain.id)


# =============================================================================
# VERIFICATION INSTRUCTIONS TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestMerchantDomainServiceInstructions:
    """Test suite for verification instructions."""

    def test_get_verification_instructions(self, db, test_merchant):
        """Test getting verification instructions."""
        unique_id = str(uuid.uuid4())[:8]
        domain = MerchantDomain(
            merchant_id=test_merchant.id,
            domain=f"instr{unique_id}.example.com",
            verification_token=f"inst_{unique_id}",
        )
        db.add(domain)
        db.commit()

        instructions = merchant_domain_service.get_verification_instructions(
            db, domain.id
        )

        assert instructions["domain"] == f"instr{unique_id}.example.com"
        assert instructions["verification_token"] == f"inst_{unique_id}"
        assert "instructions" in instructions
        assert "txt_record" in instructions
        assert instructions["txt_record"]["type"] == "TXT"
        assert instructions["txt_record"]["name"] == "_orion-verify"
        assert "common_registrars" in instructions

    def test_get_verification_instructions_not_found(self, db):
        """Test getting instructions for non-existent domain raises exception."""
        with pytest.raises(MerchantDomainNotFoundException):
            merchant_domain_service.get_verification_instructions(db, 99999)
