"""Contract tests: NetworkPolicy CRUD.

These tests run identically on FakeK8sClient and the real K8s cluster,
proving the fake matches real K8s behavior for NetworkPolicy operations.
"""

from __future__ import annotations

from typing import Any

import pytest

from tests.backends.fake import FakeConflictError, FakeNotFoundError
from tests.e2e.helpers import poll_until


def _deny_policy(name: str, app: str) -> dict[str, Any]:
    """Build a minimal deny-all NetworkPolicy for testing."""
    return {
        "apiVersion": "networking.k8s.io/v1",
        "kind": "NetworkPolicy",
        "metadata": {
            "name": name,
            "labels": {
                "app.kuberoku.com/app": app,
                "app.kuberoku.com/resource-type": "network-policy-deny",
            },
        },
        "spec": {
            "podSelector": {
                "matchLabels": {"app.kuberoku.com/app": app},
            },
            "policyTypes": ["Ingress"],
            "ingress": [],
        },
    }


def _allow_policy(name: str, app: str, port: int) -> dict[str, Any]:
    """Build a minimal allow NetworkPolicy for testing."""
    return {
        "apiVersion": "networking.k8s.io/v1",
        "kind": "NetworkPolicy",
        "metadata": {
            "name": name,
            "labels": {
                "app.kuberoku.com/app": app,
                "app.kuberoku.com/resource-type": "network-policy-allow",
            },
        },
        "spec": {
            "podSelector": {
                "matchLabels": {"app.kuberoku.com/app": app},
            },
            "policyTypes": ["Ingress"],
            "ingress": [
                {
                    "from": [
                        {
                            "podSelector": {
                                "matchLabels": {"app.kuberoku.com/app": app},
                            },
                        },
                    ],
                    "ports": [{"port": port, "protocol": "TCP"}],
                },
            ],
        },
    }


class TestNetworkPolicyCRUD:
    """NetworkPolicy create / get / update / delete / list contract."""

    def test_create_and_get(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        body = _deny_policy("test-deny", "myapi")
        created = k8s_client.create_network_policy(tmp_namespace, body)
        assert created["metadata"]["name"] == "test-deny"
        assert "resourceVersion" in created["metadata"]

        fetched = k8s_client.get_network_policy(tmp_namespace, "test-deny")
        assert fetched["metadata"]["name"] == "test-deny"
        # Real K8s omits empty lists; fake returns []. Both mean "deny all".
        assert fetched["spec"].get("ingress", []) == []

    def test_create_already_exists_raises_conflict(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        body = _deny_policy("dup-np", "myapi")
        k8s_client.create_network_policy(tmp_namespace, body)
        with pytest.raises(FakeConflictError):
            k8s_client.create_network_policy(tmp_namespace, body)

    def test_get_not_found_raises(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        with pytest.raises(FakeNotFoundError):
            k8s_client.get_network_policy(tmp_namespace, "no-such-np")

    def test_update_changes_spec(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        body = _deny_policy("update-np", "myapi")
        created = k8s_client.create_network_policy(tmp_namespace, body)

        # Update: add an allow rule
        updated_body = dict(created)
        updated_body["spec"] = {
            **created["spec"],
            "ingress": [{"ports": [{"port": 8080, "protocol": "TCP"}]}],
        }
        updated = k8s_client.update_network_policy(tmp_namespace, "update-np", updated_body)
        assert updated["spec"]["ingress"] != []
        assert updated["metadata"]["resourceVersion"] != created["metadata"]["resourceVersion"]

    def test_update_not_found_raises(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        body = _deny_policy("ghost-np", "myapi")
        with pytest.raises(FakeNotFoundError):
            k8s_client.update_network_policy(tmp_namespace, "ghost-np", body)

    def test_delete_then_get_raises(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        body = _deny_policy("del-np", "myapi")
        k8s_client.create_network_policy(tmp_namespace, body)
        k8s_client.delete_network_policy(tmp_namespace, "del-np")

        def _get_or_none() -> dict[str, Any] | None:
            try:
                return k8s_client.get_network_policy(tmp_namespace, "del-np")
            except FakeNotFoundError:
                return None

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

    def test_delete_not_found_raises(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        with pytest.raises(FakeNotFoundError):
            k8s_client.delete_network_policy(tmp_namespace, "nope")

    def test_list_with_label_selector(self, k8s_client, tmp_namespace):  # type: ignore[no-untyped-def]
        k8s_client.create_network_policy(tmp_namespace, _deny_policy("np-app1", "app1"))
        k8s_client.create_network_policy(tmp_namespace, _deny_policy("np-app2", "app2"))
        k8s_client.create_network_policy(tmp_namespace, _allow_policy("np-app1-web", "app1", 8080))

        # Filter by app
        app1_policies = k8s_client.list_network_policies(
            tmp_namespace, labels={"app.kuberoku.com/app": "app1"}
        )
        assert len(app1_policies) == 2
        names = {p["metadata"]["name"] for p in app1_policies}
        assert names == {"np-app1", "np-app1-web"}

        # Filter by resource type
        deny_policies = k8s_client.list_network_policies(
            tmp_namespace,
            labels={"app.kuberoku.com/resource-type": "network-policy-deny"},
        )
        assert len(deny_policies) == 2
