"""Tests for domains:add."""

from __future__ import annotations

from typing import Any

import pytest

from kuberoku.exceptions import (
    AppNotFoundError,
    CertManagerNotFoundError,
    DomainAlreadyExistsError,
    IngressControllerNotFoundError,
)
from kuberoku.factory import KuberokuFactory
from kuberoku.k8s import resources
from kuberoku.sdk.ingress import detect_ingress_controller
from tests.backends.fake import FakeK8sClient, FakeNotFoundError


class TestDomainsAdd:
    def test_add_creates_ingress(self, factory: KuberokuFactory, app_with_web: str) -> None:
        domain = factory.domains.add(app_with_web, "api.example.com")
        assert domain.hostname == "api.example.com"
        assert domain.app == app_with_web
        assert domain.process_type == "web"
        assert domain.port == 8080
        assert domain.tls is True
        assert domain.issuer == "letsencrypt-prod"
        assert domain.auto_generated is False

    def test_add_ingress_exists_in_k8s(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        rules = ingress["spec"]["rules"]
        assert len(rules) == 1
        assert rules[0]["host"] == "api.example.com"

    def test_add_tls_entry(self, factory: KuberokuFactory, k8s: Any, app_with_web: str) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        tls = ingress["spec"].get("tls", [])
        assert len(tls) == 1
        assert "api.example.com" in tls[0]["hosts"]

    def test_add_no_tls(self, factory: KuberokuFactory, k8s: Any, app_with_web: str) -> None:
        domain = factory.domains.add(app_with_web, "api.example.com", no_tls=True)
        assert domain.tls is False
        assert domain.issuer is None

        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        assert "tls" not in ingress["spec"]

    def test_add_multiple_domains(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        factory.domains.add(app_with_web, "admin.example.com")

        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        rules = ingress["spec"]["rules"]
        assert len(rules) == 2
        hosts = {r["host"] for r in rules}
        assert hosts == {"api.example.com", "admin.example.com"}

    def test_add_custom_port(self, factory: KuberokuFactory, app_with_web: str) -> None:
        domain = factory.domains.add(app_with_web, "api.example.com", port=3000)
        assert domain.port == 3000

    @pytest.mark.fake_only
    def test_add_custom_process_type(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        # Create an admin process type
        prefix = factory.prefix
        dom = factory.domain
        ns = factory.namespace
        dep_body = resources.build_deployment(
            prefix,
            dom,
            app_with_web,
            "admin",
            "myapp:v1",
            1,
            None,
            ports=[{"containerPort": 9090, "protocol": "TCP"}],
        )
        k8s.create_deployment(ns, dep_body)
        svc_body = resources.build_service(
            prefix,
            dom,
            app_with_web,
            "admin",
            [{"port": 9090, "targetPort": 9090, "protocol": "TCP"}],
        )
        k8s.create_service(ns, svc_body)

        domain = factory.domains.add(app_with_web, "admin.example.com", process_type="admin")
        assert domain.process_type == "admin"
        assert domain.port == 9090

    def test_add_creates_external_policy(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        np_name = resources.safe_name(factory.prefix, "netpol-external", f"{app_with_web}-web")
        np = k8s.get_network_policy(factory.namespace, np_name)
        annots = np["metadata"]["annotations"]
        assert "domains" in annots[f"{factory.domain}/exposed-by"]

    def test_add_duplicate_hostname_raises(
        self, factory: KuberokuFactory, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        with pytest.raises(DomainAlreadyExistsError):
            factory.domains.add(app_with_web, "api.example.com")

    @pytest.mark.fake_only
    def test_add_no_ingress_controller_raises(
        self, app_with_web: str, k8s: Any, factory: KuberokuFactory
    ) -> None:
        # Factory with no ingress classes
        bare_k8s = FakeK8sClient()
        # Copy the app data from the test k8s
        bare_k8s._store = k8s._store
        bare_k8s._namespaces = k8s._namespaces
        bare_factory = KuberokuFactory(k8s_client=bare_k8s, namespace=factory.namespace)
        with pytest.raises(IngressControllerNotFoundError):
            bare_factory.domains.add(app_with_web, "api.example.com")

    @pytest.mark.fake_only
    def test_add_no_cert_manager_tls_raises(
        self, app_with_web: str, k8s: Any, factory: KuberokuFactory
    ) -> None:
        # Remove cert-manager by deleting the deployment from the store
        k8s.delete_deployment("cert-manager", "cert-manager")
        bare_factory = KuberokuFactory(k8s_client=k8s, namespace=factory.namespace)
        with pytest.raises(CertManagerNotFoundError):
            bare_factory.domains.add(app_with_web, "api.example.com")

    @pytest.mark.fake_only
    def test_add_no_cert_manager_no_tls_works(
        self, app_with_web: str, k8s: Any, factory: KuberokuFactory
    ) -> None:
        bare_k8s = FakeK8sClient()
        bare_k8s._store = k8s._store
        bare_k8s._namespaces = k8s._namespaces
        bare_k8s._ingress_classes = k8s._ingress_classes
        bare_factory = KuberokuFactory(k8s_client=bare_k8s, namespace=factory.namespace)
        domain = bare_factory.domains.add(app_with_web, "api.example.com", no_tls=True)
        assert domain.tls is False

    def test_add_nonexistent_app_raises(self, factory: KuberokuFactory) -> None:
        with pytest.raises(AppNotFoundError):
            factory.domains.add("nonexistent", "api.example.com")

    def test_add_ingress_class_set(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        ic = detect_ingress_controller(factory.k8s, factory.namespace)
        assert ic is not None
        assert ingress["spec"]["ingressClassName"] == ic.class_name

    def test_add_cert_manager_annotation(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        annots = ingress.get("metadata", {}).get("annotations", {})
        assert annots.get("cert-manager.io/cluster-issuer") == "letsencrypt-prod"

    def test_add_auto_generated_domain(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        domain = factory.domains.add(app_with_web, "myapp.apps.example.com", auto_generated=True)
        assert domain.auto_generated is True

        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)
        annots = ingress.get("metadata", {}).get("annotations", {})
        auto = annots.get(f"{factory.domain}/auto-domains", "")
        assert "myapp.apps.example.com" in auto

    def test_add_on_step_callback(self, factory: KuberokuFactory, app_with_web: str) -> None:
        steps: list[str] = []
        factory.domains.add(app_with_web, "api.example.com", on_step=steps.append)
        assert len(steps) > 0
        assert any("Ingress" in s for s in steps)

    def test_add_default_port_without_service(self, factory: KuberokuFactory) -> None:
        """Port defaults to 8080 when no Service exists."""
        app_name = "bare-app"
        factory.apps.create(app_name)
        # No deployment/service — IngressClass already in conftest/cluster
        domain = factory.domains.add(app_name, "bare.example.com", no_tls=True)
        assert domain.port == 8080

    def test_add_custom_issuer(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        domain = factory.domains.add(app_with_web, "api.example.com", issuer="letsencrypt-staging")
        assert domain.issuer == "letsencrypt-staging"


class TestDomainsAddBackendVerification:
    """Verify backend service name, port, and TLS secret in Ingress rules."""

    def test_backend_service_name_matches(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)

        expected_svc_name = resources.safe_name(factory.prefix, app_with_web, "web")
        backend = ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]
        assert backend["name"] == expected_svc_name

    def test_backend_service_port_matches(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)

        backend = ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]
        assert backend["port"]["number"] == 8080

    def test_tls_secret_name_format(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)

        tls = ingress["spec"]["tls"]
        assert len(tls) == 1
        expected_secret = "tls-api-example-com"
        assert tls[0]["secretName"] == expected_secret

    def test_backend_custom_port(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        factory.domains.add(app_with_web, "api.example.com", port=3000)
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)

        backend = ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]
        assert backend["port"]["number"] == 3000

    def test_tls_secret_name_dots_replaced(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        """TLS secret name replaces dots with dashes in hostname."""
        factory.domains.add(app_with_web, "www.deep.sub.example.com")
        ingress_name = resources.safe_name(factory.prefix, "ingress", app_with_web)
        ingress = k8s.get_ingress(factory.namespace, ingress_name)

        tls = ingress["spec"]["tls"]
        assert len(tls) == 1
        expected_secret = "tls-www-deep-sub-example-com"
        assert tls[0]["secretName"] == expected_secret


class TestSharedExternalPolicyOwnership:
    """Test co-ownership of external policies by expose and domains."""

    def test_expose_then_domains_add_shares_policy(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        """expose:on creates policy with 'expose', domains:add adds 'domains'."""
        # First: deploy the web process so expose_on can find the Service
        factory.deploy.deploy(app_with_web, image="myapp:v1", wait=False)

        # expose:on creates external policy with exposed-by: expose
        factory.services.expose_on(app_with_web, "web")
        np_name = resources.safe_name(factory.prefix, "netpol-external", f"{app_with_web}-web")
        np = k8s.get_network_policy(factory.namespace, np_name)
        annots = np["metadata"]["annotations"]
        assert annots[f"{factory.domain}/exposed-by"] == "expose"

        # domains:add should add "domains" to the existing policy
        factory.domains.add(app_with_web, "api.example.com")
        np = k8s.get_network_policy(factory.namespace, np_name)
        annots = np["metadata"]["annotations"]
        owners = set(annots[f"{factory.domain}/exposed-by"].split(","))
        assert owners == {"domains", "expose"}

    def test_domains_remove_leaves_expose_policy(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        """After expose+domains add, domains:remove leaves policy with 'expose'."""
        factory.deploy.deploy(app_with_web, image="myapp:v1", wait=False)

        # Set up both owners
        factory.services.expose_on(app_with_web, "web")
        factory.domains.add(app_with_web, "api.example.com")

        # Remove domain
        factory.domains.remove(app_with_web, "api.example.com")

        np_name = resources.safe_name(factory.prefix, "netpol-external", f"{app_with_web}-web")
        np = k8s.get_network_policy(factory.namespace, np_name)
        annots = np["metadata"]["annotations"]
        assert annots[f"{factory.domain}/exposed-by"] == "expose"

    def test_expose_off_after_domain_removed_deletes_policy(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        """After removing domains owner, expose:off deletes policy entirely."""
        factory.deploy.deploy(app_with_web, image="myapp:v1", wait=False)

        # Set up both owners
        factory.services.expose_on(app_with_web, "web")
        factory.domains.add(app_with_web, "api.example.com")

        # Remove domain owner first
        factory.domains.remove(app_with_web, "api.example.com")

        # Now expose:off should delete the policy entirely
        factory.services.expose_off(app_with_web, "web")

        np_name = resources.safe_name(factory.prefix, "netpol-external", f"{app_with_web}-web")
        with pytest.raises(FakeNotFoundError):
            k8s.get_network_policy(factory.namespace, np_name)

    def test_full_lifecycle_expose_domains_remove_unexpose(
        self, factory: KuberokuFactory, k8s: Any, app_with_web: str
    ) -> None:
        """Full lifecycle: expose -> domains:add -> domains:remove -> expose:off."""
        factory.deploy.deploy(app_with_web, image="myapp:v1", wait=False)
        np_name = resources.safe_name(factory.prefix, "netpol-external", f"{app_with_web}-web")

        # 1. Expose
        factory.services.expose_on(app_with_web, "web")
        np = k8s.get_network_policy(factory.namespace, np_name)
        assert np["metadata"]["annotations"][f"{factory.domain}/exposed-by"] == "expose"

        # 2. Add domain
        factory.domains.add(app_with_web, "api.example.com")
        np = k8s.get_network_policy(factory.namespace, np_name)
        owners = set(np["metadata"]["annotations"][f"{factory.domain}/exposed-by"].split(","))
        assert owners == {"domains", "expose"}

        # 3. Remove domain
        factory.domains.remove(app_with_web, "api.example.com")
        np = k8s.get_network_policy(factory.namespace, np_name)
        assert np["metadata"]["annotations"][f"{factory.domain}/exposed-by"] == "expose"

        # 4. Unexpose: policy should be deleted
        factory.services.expose_off(app_with_web, "web")
        with pytest.raises(FakeNotFoundError):
            k8s.get_network_policy(factory.namespace, np_name)
