"""Contract tests for Ingress CRUD — fake matches real K8s behavior.

These tests run identically on FakeK8sClient and the real K8s cluster,
proving the fake matches real K8s behavior for Ingress operations.
Uses k8s_client + tmp_namespace fixtures from root conftest.py.
"""

from __future__ import annotations

import contextlib
from typing import Any

import pytest

from kuberoku.k8s.client import K8sClient
from tests.backends.fake import FakeConflictError, FakeNotFoundError
from tests.e2e.helpers import poll_until


def _ingress_body(name: str, hostname: str = "example.com") -> dict[str, Any]:
    return {
        "apiVersion": "networking.k8s.io/v1",
        "kind": "Ingress",
        "metadata": {
            "name": name,
            "labels": {"app": "test"},
        },
        "spec": {
            "rules": [
                {
                    "host": hostname,
                    "http": {
                        "paths": [
                            {
                                "path": "/",
                                "pathType": "Prefix",
                                "backend": {
                                    "service": {
                                        "name": "web",
                                        "port": {"number": 8080},
                                    }
                                },
                            }
                        ]
                    },
                }
            ]
        },
    }


class TestIngressCreate:
    def test_create_returns_resource_version(self, k8s_client, tmp_namespace) -> None:
        result = k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress"))
        assert result["metadata"]["resourceVersion"]

    def test_create_duplicate_raises_conflict(self, k8s_client, tmp_namespace) -> None:
        k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress"))
        with pytest.raises(FakeConflictError):
            k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress"))


class TestIngressGet:
    def test_get_returns_created(self, k8s_client, tmp_namespace) -> None:
        k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress", "foo.com"))
        result = k8s_client.get_ingress(tmp_namespace, "my-ingress")
        rules = result["spec"]["rules"]
        assert rules[0]["host"] == "foo.com"

    def test_get_not_found_raises(self, k8s_client, tmp_namespace) -> None:
        with pytest.raises(FakeNotFoundError):
            k8s_client.get_ingress(tmp_namespace, "nonexistent")


class TestIngressUpdate:
    def test_update_changes_rules(self, k8s_client, tmp_namespace) -> None:
        k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress", "old.com"))
        # Read-modify-write with retry: real K8s ingress controller may mutate
        for _attempt in range(5):
            ingress = k8s_client.get_ingress(tmp_namespace, "my-ingress")
            ingress["spec"]["rules"][0]["host"] = "new.com"
            try:
                k8s_client.update_ingress(tmp_namespace, "my-ingress", ingress)
                break
            except FakeConflictError:
                continue
        else:
            pytest.fail("Ingress update failed after 5 retries due to conflict")
        updated = k8s_client.get_ingress(tmp_namespace, "my-ingress")
        assert updated["spec"]["rules"][0]["host"] == "new.com"

    def test_update_stale_rv_raises_conflict(self, k8s_client, tmp_namespace) -> None:
        k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress"))
        ingress = k8s_client.get_ingress(tmp_namespace, "my-ingress")
        current_rv = ingress["metadata"]["resourceVersion"]
        # Use a numeric but outdated resourceVersion.  Real K8s resourceVersions
        # are opaque integers; passing a non-numeric string like "stale" causes a
        # 500 Internal Server Error instead of the expected 409 Conflict.
        # Subtract 1 to guarantee the RV differs from the current one.
        stale_rv = str(int(current_rv) - 1) if current_rv.isdigit() else "0"
        ingress["metadata"]["resourceVersion"] = stale_rv
        with pytest.raises(FakeConflictError):
            k8s_client.update_ingress(tmp_namespace, "my-ingress", ingress)


class TestIngressDelete:
    def test_delete_removes(self, k8s_client, tmp_namespace) -> None:
        k8s_client.create_ingress(tmp_namespace, _ingress_body("my-ingress"))
        k8s_client.delete_ingress(tmp_namespace, "my-ingress")

        def _get_or_none() -> dict[str, Any] | None:
            try:
                return k8s_client.get_ingress(tmp_namespace, "my-ingress")
            except FakeNotFoundError:
                return None

        poll_until(
            fn=_get_or_none,
            predicate=lambda result: result is None,
            timeout=30,
            interval=0.5,
            message="Ingress my-ingress still exists after deletion",
        )

    def test_delete_not_found_raises(self, k8s_client, tmp_namespace) -> None:
        with pytest.raises(FakeNotFoundError):
            k8s_client.delete_ingress(tmp_namespace, "nonexistent")


class TestIngressList:
    def test_list_empty(self, k8s_client, tmp_namespace) -> None:
        assert k8s_client.list_ingresses(tmp_namespace) == []

    def test_list_returns_all(self, k8s_client, tmp_namespace) -> None:
        k8s_client.create_ingress(tmp_namespace, _ingress_body("ing-1"))
        k8s_client.create_ingress(tmp_namespace, _ingress_body("ing-2"))
        results = k8s_client.list_ingresses(tmp_namespace)
        names = {r["metadata"]["name"] for r in results}
        assert names == {"ing-1", "ing-2"}

    def test_list_filters_by_labels(self, k8s_client, tmp_namespace) -> None:
        body_a = _ingress_body("ing-a")
        body_a["metadata"]["labels"] = {"env": "prod"}
        body_b = _ingress_body("ing-b")
        body_b["metadata"]["labels"] = {"env": "staging"}
        k8s_client.create_ingress(tmp_namespace, body_a)
        k8s_client.create_ingress(tmp_namespace, body_b)
        results = k8s_client.list_ingresses(tmp_namespace, labels={"env": "prod"})
        assert len(results) == 1
        assert results[0]["metadata"]["name"] == "ing-a"

    def test_list_namespace_scoped(self, k8s_client, tmp_namespace, request) -> None:
        # Use two different namespaces to prove scoping
        ns_a = tmp_namespace
        ns_b = f"{tmp_namespace}-alt"
        if request.config.getoption("--backend") == "real":
            real = K8sClient.from_context()
            real.create_namespace(ns_b)

            def _cleanup() -> None:
                with contextlib.suppress(Exception):
                    real._core.delete_namespace(ns_b)

            request.addfinalizer(_cleanup)
        k8s_client.create_ingress(ns_a, _ingress_body("ing-1"))
        k8s_client.create_ingress(ns_b, _ingress_body("ing-2"))
        assert len(k8s_client.list_ingresses(ns_a)) == 1
        assert len(k8s_client.list_ingresses(ns_b)) == 1


class TestIngressClasses:
    def test_list_empty(self, k8s_client, tmp_namespace) -> None:
        # Note: On a real cluster, IngressClasses may already exist.
        # For fake backend, this verifies the empty state.
        classes = k8s_client.list_ingress_classes()
        # Can't assert == [] on real clusters, just verify it's a list
        assert isinstance(classes, list)

    def test_inject_and_list(self, k8s_client, tmp_namespace) -> None:
        ic_name = f"test-nginx-{tmp_namespace[:8]}"
        k8s_client.create_ingress_class(
            {
                "apiVersion": "networking.k8s.io/v1",
                "kind": "IngressClass",
                "metadata": {"name": ic_name},
                "spec": {"controller": "k8s.io/ingress-nginx"},
            }
        )
        classes = poll_until(
            fn=lambda: k8s_client.list_ingress_classes(),
            predicate=lambda cs: ic_name in {c["metadata"]["name"] for c in cs},
            timeout=10,
            interval=0.5,
            message=f"IngressClass {ic_name} not found in list",
        )
        assert ic_name in {c["metadata"]["name"] for c in classes}
