"""Multi-app isolation tests — deploy operations don't cross app boundaries.

Also includes multi-process-type + multi-port tests to verify that a single app
can run web, worker, smtp (etc.) with the SAME image but different ports,
replicas, and commands — each fully isolated in its own Deployment/Service/
NetworkPolicy.  Heroku model: one image per app, Procfile determines commands.
"""

from __future__ import annotations

import pytest

from kuberoku.factory import KuberokuFactory
from kuberoku.k8s.resources import safe_name
from tests.backends.fake import FakeK8sClient, FakeNotFoundError


def _two_app_factory(ns: str) -> KuberokuFactory:
    factory = KuberokuFactory(k8s_client=FakeK8sClient(), namespace=ns)
    factory.apps.create("app1")
    factory.apps.create("app2")
    return factory


def test_deploy_two_apps_independent(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", wait=False)
    factory.deploy.deploy("app2", image="app2:v1", wait=False)
    dep1 = factory.k8s.get_deployment(factory.namespace, safe_name(factory.prefix, "app1", "web"))
    dep2 = factory.k8s.get_deployment(factory.namespace, safe_name(factory.prefix, "app2", "web"))
    assert dep1["spec"]["template"]["spec"]["containers"][0]["image"] == "app1:v1"
    assert dep2["spec"]["template"]["spec"]["containers"][0]["image"] == "app2:v1"


def test_deploy_app1_doesnt_affect_app2(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", wait=False)
    factory.deploy.deploy("app2", image="app2:v1", wait=False)
    # Redeploy app1
    factory.deploy.deploy("app1", image="app1:v2", wait=False)
    dep2 = factory.k8s.get_deployment(factory.namespace, safe_name(factory.prefix, "app2", "web"))
    assert dep2["spec"]["template"]["spec"]["containers"][0]["image"] == "app2:v1"


def test_releases_isolated_per_app(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", wait=False)
    factory.deploy.deploy("app1", image="app1:v2", wait=False)
    factory.deploy.deploy("app2", image="app2:v1", wait=False)
    releases1 = factory.releases.list("app1")
    releases2 = factory.releases.list("app2")
    assert len(releases1) == 2
    assert len(releases2) == 1


def test_rollback_app1_doesnt_touch_app2(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", wait=False)
    factory.deploy.deploy("app1", image="app1:v2", wait=False)
    factory.deploy.deploy("app2", image="app2:v1", wait=False)
    factory.releases.rollback("app1")
    dep2 = factory.k8s.get_deployment(factory.namespace, safe_name(factory.prefix, "app2", "web"))
    assert dep2["spec"]["template"]["spec"]["containers"][0]["image"] == "app2:v1"


def test_config_restart_only_targets_own_deployments(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", wait=False)
    factory.deploy.deploy("app2", image="app2:v1", wait=False)
    # Get app2's deployment resourceVersion before config change
    dep2_before = factory.k8s.get_deployment(
        factory.namespace, safe_name(factory.prefix, "app2", "web")
    )
    rv_before = dep2_before["metadata"]["resourceVersion"]
    # Config set on app1
    factory.config.set("app1", {"KEY": "val"})
    # app2's deployment should be unchanged
    dep2_after = factory.k8s.get_deployment(
        factory.namespace, safe_name(factory.prefix, "app2", "web")
    )
    rv_after = dep2_after["metadata"]["resourceVersion"]
    assert rv_before == rv_after


def test_scale_app1_doesnt_affect_app2(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", replicas=1, wait=False)
    factory.deploy.deploy("app2", image="app2:v1", replicas=1, wait=False)
    factory.ps.scale("app1", {"web": 5})
    dep2 = factory.k8s.get_deployment(factory.namespace, safe_name(factory.prefix, "app2", "web"))
    assert dep2["spec"]["replicas"] == 1


def test_ps_type_app1_doesnt_affect_app2(tmp_namespace: str) -> None:
    factory = _two_app_factory(tmp_namespace)
    factory.deploy.deploy("app1", image="app1:v1", wait=False)
    factory.deploy.deploy("app2", image="app2:v1", wait=False)
    factory.ps.set_type("app1", "web", cpu="500m")
    dep2 = factory.k8s.get_deployment(factory.namespace, safe_name(factory.prefix, "app2", "web"))
    assert dep2["spec"]["template"]["spec"]["containers"][0].get("resources", {}) == {}


# ── Multi-process-type + multi-port tests ─────────────────────────


_MAILSERVER_IMAGE = "mailserver:v1"


def _mailserver_factory(ns: str) -> KuberokuFactory:
    """Create factory with a mailserver app: web (2 ports), worker (0 ports), smtp (3 ports).

    All process types use the SAME image (Heroku model).  The Procfile / ps:set
    commands determine what each type runs.  Ports are configured per type.
    """
    factory = KuberokuFactory(k8s_client=FakeK8sClient(), namespace=ns)
    factory.apps.create("mailserver")

    # Deploy web: 2 ports (HTTP + HTTPS) — same image
    factory.deploy.deploy(
        "mailserver",
        image=_MAILSERVER_IMAGE,
        process_type="web",
        ports=[
            {"containerPort": 8080, "protocol": "TCP"},
            {"containerPort": 8443, "protocol": "TCP"},
        ],
        wait=False,
    )

    # Deploy worker: 0 ports (headless background processor) — same image
    factory.deploy.deploy(
        "mailserver",
        image=_MAILSERVER_IMAGE,
        process_type="worker",
        ports=[],
        wait=False,
    )

    # Deploy smtp: 3 ports (SMTP + submission + SMTPS) — same image
    factory.deploy.deploy(
        "mailserver",
        image=_MAILSERVER_IMAGE,
        process_type="smtp",
        ports=[
            {"containerPort": 25, "protocol": "TCP"},
            {"containerPort": 587, "protocol": "TCP"},
            {"containerPort": 465, "protocol": "TCP"},
        ],
        wait=False,
    )

    return factory


def test_three_process_types_three_deployments(tmp_namespace: str) -> None:
    """Each process type gets its own Deployment — all with the SAME image."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    web = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "web"))
    worker = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "worker"))
    smtp = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "smtp"))

    # Heroku model: ALL process types share the same image
    assert web["spec"]["template"]["spec"]["containers"][0]["image"] == _MAILSERVER_IMAGE
    assert worker["spec"]["template"]["spec"]["containers"][0]["image"] == _MAILSERVER_IMAGE
    assert smtp["spec"]["template"]["spec"]["containers"][0]["image"] == _MAILSERVER_IMAGE


def test_each_type_has_correct_ports(tmp_namespace: str) -> None:
    """web has 2 ports, worker has 0, smtp has 3 — total 5 across the app."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    web_dep = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "web"))
    web_ports = web_dep["spec"]["template"]["spec"]["containers"][0].get("ports", [])
    assert len(web_ports) == 2
    assert {p["containerPort"] for p in web_ports} == {8080, 8443}

    worker_dep = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "worker"))
    worker_ports = worker_dep["spec"]["template"]["spec"]["containers"][0].get("ports", [])
    assert len(worker_ports) == 0

    smtp_dep = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "smtp"))
    smtp_ports = smtp_dep["spec"]["template"]["spec"]["containers"][0].get("ports", [])
    assert len(smtp_ports) == 3
    assert {p["containerPort"] for p in smtp_ports} == {25, 587, 465}


def test_services_only_for_types_with_ports(tmp_namespace: str) -> None:
    """web and smtp get Services. worker (no ports) does not."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    # web Service: 2 ports
    web_svc = factory.k8s.get_service(ns, safe_name(prefix, "mailserver", "web"))
    assert len(web_svc["spec"]["ports"]) == 2
    assert {p["port"] for p in web_svc["spec"]["ports"]} == {8080, 8443}

    # smtp Service: 3 ports
    smtp_svc = factory.k8s.get_service(ns, safe_name(prefix, "mailserver", "smtp"))
    assert len(smtp_svc["spec"]["ports"]) == 3
    assert {p["port"] for p in smtp_svc["spec"]["ports"]} == {25, 587, 465}

    # worker: no Service
    with pytest.raises(FakeNotFoundError):
        factory.k8s.get_service(ns, safe_name(prefix, "mailserver", "worker"))


def test_network_policies_match_ports(tmp_namespace: str) -> None:
    """process-allow NetworkPolicies exist for web and smtp with correct ports."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    # web: 2 ports in policy
    web_np = factory.k8s.get_network_policy(
        ns, safe_name(prefix, "netpol-allow", "mailserver-web")
    )
    web_np_ports = web_np["spec"]["ingress"][0]["ports"]
    assert len(web_np_ports) == 2
    assert {p["port"] for p in web_np_ports} == {8080, 8443}

    # smtp: 3 ports in policy
    smtp_np = factory.k8s.get_network_policy(
        ns, safe_name(prefix, "netpol-allow", "mailserver-smtp")
    )
    smtp_np_ports = smtp_np["spec"]["ingress"][0]["ports"]
    assert len(smtp_np_ports) == 3
    assert {p["port"] for p in smtp_np_ports} == {25, 587, 465}

    # worker: no process-allow policy
    with pytest.raises(FakeNotFoundError):
        factory.k8s.get_network_policy(ns, safe_name(prefix, "netpol-allow", "mailserver-worker"))


def test_scale_types_independently(tmp_namespace: str) -> None:
    """Scale web=3, worker=5, smtp=2 — each Deployment gets correct replicas."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    factory.ps.scale("mailserver", {"web": 3, "worker": 5, "smtp": 2})

    web = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "web"))
    worker = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "worker"))
    smtp = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "smtp"))

    assert web["spec"]["replicas"] == 3
    assert worker["spec"]["replicas"] == 5
    assert smtp["spec"]["replicas"] == 2


def test_set_commands_per_type(tmp_namespace: str) -> None:
    """Set different commands for each type — each Deployment gets its own."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    factory.ps.set_commands(
        "mailserver",
        {
            "web": "gunicorn webmail:app",
            "worker": "celery -A tasks worker",
            "smtp": "postfix start",
        },
    )

    web = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "web"))
    worker = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "worker"))
    smtp = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "smtp"))

    assert web["spec"]["template"]["spec"]["containers"][0]["command"] == [
        "/bin/sh",
        "-c",
        "gunicorn webmail:app",
    ]
    assert worker["spec"]["template"]["spec"]["containers"][0]["command"] == [
        "/bin/sh",
        "-c",
        "celery -A tasks worker",
    ]
    assert smtp["spec"]["template"]["spec"]["containers"][0]["command"] == [
        "/bin/sh",
        "-c",
        "postfix start",
    ]


def test_set_resources_per_type(tmp_namespace: str) -> None:
    """Set different CPU/memory for each type."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    factory.ps.set_type("mailserver", "web", cpu="250m", memory="256Mi")
    factory.ps.set_type("mailserver", "worker", cpu="1000m", memory="1Gi")
    factory.ps.set_type("mailserver", "smtp", cpu="100m", memory="128Mi")

    web_res = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "web"))["spec"][
        "template"
    ]["spec"]["containers"][0]["resources"]
    worker_res = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "worker"))["spec"][
        "template"
    ]["spec"]["containers"][0]["resources"]
    smtp_res = factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", "smtp"))["spec"][
        "template"
    ]["spec"]["containers"][0]["resources"]

    assert web_res["requests"]["cpu"] == "250m"
    assert web_res["requests"]["memory"] == "256Mi"
    assert worker_res["requests"]["cpu"] == "1000m"
    assert worker_res["requests"]["memory"] == "1Gi"
    assert smtp_res["requests"]["cpu"] == "100m"
    assert smtp_res["requests"]["memory"] == "128Mi"


def test_release_captures_all_process_images(tmp_namespace: str) -> None:
    """A release after multi-type deploy has all 3 types — same image."""
    factory = _mailserver_factory(tmp_namespace)
    releases = factory.releases.list("mailserver")
    latest = max(releases, key=lambda r: r.version)
    assert "web" in latest.images
    assert "worker" in latest.images
    assert "smtp" in latest.images
    # Heroku model: all types share the same image
    assert latest.images["web"] == _MAILSERVER_IMAGE
    assert latest.images["worker"] == _MAILSERVER_IMAGE
    assert latest.images["smtp"] == _MAILSERVER_IMAGE


def test_destroy_cleans_all_types(tmp_namespace: str) -> None:
    """Destroy removes all 3 Deployments, 2 Services, and all NetworkPolicies."""
    factory = _mailserver_factory(tmp_namespace)
    ns = factory.namespace
    prefix = factory.prefix

    factory.apps.destroy("mailserver")

    # All Deployments gone
    for ptype in ("web", "worker", "smtp"):
        with pytest.raises(FakeNotFoundError):
            factory.k8s.get_deployment(ns, safe_name(prefix, "mailserver", ptype))

    # All Services gone
    for ptype in ("web", "smtp"):
        with pytest.raises(FakeNotFoundError):
            factory.k8s.get_service(ns, safe_name(prefix, "mailserver", ptype))

    # App not in list
    assert not any(a.name == "mailserver" for a in factory.apps.list())
