"""Fixtures for E2E tests against a live k3d cluster.

Provides Kubernetes clients, CRD installation, node name discovery,
and autouse cleanup that restores node state after every test.
"""

import subprocess
import sys
import time
from pathlib import Path

import pytest
from kubernetes import client, config
from kubernetes.client.exceptions import ApiException

# ---------------------------------------------------------------------------
# Cross-repo imports: add apollo to sys.path so we can import the
# controller and policy evaluator without installing apollo as a package.
# ---------------------------------------------------------------------------
_CITADEL_ROOT = Path(__file__).resolve().parents[2]  # citadel/
_APOLLO_ROOT = _CITADEL_ROOT.parent / "apollo"

# Add apollo root so "from apollo.spoke.node_lifecycle ..." works
if str(_APOLLO_ROOT) not in sys.path:
    sys.path.insert(0, str(_APOLLO_ROOT))

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
CRD_GROUP = "citadel.dev"
CRD_VERSION = "v1alpha1"
CRD_PLURAL = "nodeterminationpolicies"
CRD_YAML = _CITADEL_ROOT / "crds" / "nodeterminationpolicy.yaml"
E2E_NAMESPACE = "e2e-test"

CITADEL_LABELS = [
    "citadel.dev/terminate-reason",
    "citadel.dev/terminate-after",
    "citadel.dev/policy-name",
    "citadel.dev/managed-by",
]


# ---------------------------------------------------------------------------
# Session-scoped: cluster reachability gate
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def k8s_clients():
    """Load kubeconfig and return a dict of Kubernetes API clients.

    Skips the entire test session if the cluster is unreachable.
    """
    try:
        config.load_kube_config()
    except config.ConfigException:
        pytest.skip("No kubeconfig available")

    core = client.CoreV1Api()

    # Verify cluster is actually reachable
    try:
        core.list_node(_request_timeout=5)
    except Exception as exc:
        pytest.skip(f"Cluster unreachable: {exc}")

    return {
        "core": core,
        "custom": client.CustomObjectsApi(),
        "extensions": client.ApiextensionsV1Api(),
        "policy": client.PolicyV1Api(),
        "apps": client.AppsV1Api(),
    }


@pytest.fixture(scope="session")
def cluster_reachable(k8s_clients):
    """Sentinel fixture: skip the entire suite if cluster is down."""
    return True


# ---------------------------------------------------------------------------
# Session-scoped: CRD installation
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def install_crds(k8s_clients):
    """Install the NodeTerminationPolicy CRD and wait for it to be established."""
    assert CRD_YAML.exists(), f"CRD file not found: {CRD_YAML}"

    subprocess.run(
        ["kubectl", "apply", "-f", str(CRD_YAML)],
        check=True,
        capture_output=True,
        text=True,
    )

    # Wait for CRD to become established (up to 30s)
    ext = k8s_clients["extensions"]
    crd_name = f"{CRD_PLURAL}.{CRD_GROUP}"
    for _ in range(30):
        try:
            crd = ext.read_custom_resource_definition(crd_name)
            for cond in crd.status.conditions or []:
                if cond.type == "Established" and cond.status == "True":
                    return crd_name
        except ApiException:
            pass
        time.sleep(1)

    pytest.fail("CRD did not become established within 30 seconds")


# ---------------------------------------------------------------------------
# Session-scoped: node name discovery
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def node_name(k8s_clients):
    """Return the name of the first worker node (agent).

    In a k3d cluster, worker nodes contain 'agent' in their name.
    """
    core = k8s_clients["core"]
    nodes = core.list_node().items
    for node in nodes:
        if "agent" in node.metadata.name:
            return node.metadata.name
    pytest.skip("No worker (agent) node found in cluster")


@pytest.fixture(scope="session")
def server_node(k8s_clients):
    """Return the name of the server (control-plane) node."""
    core = k8s_clients["core"]
    nodes = core.list_node().items
    for node in nodes:
        if "server" in node.metadata.name:
            return node.metadata.name
    pytest.skip("No server node found in cluster")


# ---------------------------------------------------------------------------
# Per-test: cleanup node labels and cordon state
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def cleanup_node_labels(k8s_clients):
    """After each test, clear citadel.dev/* labels and uncordon all nodes."""
    yield

    core = k8s_clients["core"]
    nodes = core.list_node().items
    for node in nodes:
        name = node.metadata.name
        labels = node.metadata.labels or {}

        # Remove citadel labels (patch with None to delete)
        label_patch = {}
        for label in CITADEL_LABELS:
            if label in labels:
                label_patch[label] = None

        needs_patch = bool(label_patch)
        body = {}

        if label_patch:
            body["metadata"] = {"labels": label_patch}

        # Uncordon if cordoned
        if node.spec.unschedulable:
            body["spec"] = {"unschedulable": False}
            needs_patch = True

        if needs_patch:
            try:
                core.patch_node(name, body)
            except ApiException:
                pass  # Best-effort cleanup



# ---------------------------------------------------------------------------
# Citadel domain objects
# ---------------------------------------------------------------------------
@pytest.fixture()
def node_manager():
    """Create a Citadel NodeManager connected to the live cluster."""
    from citadel_operator.k3s.node import NodeManager
    return NodeManager()


@pytest.fixture()
def health_checker():
    """Create a Citadel HealthChecker with default 48h max age."""
    from citadel_operator.k3s.health import HealthChecker
    return HealthChecker()


# ---------------------------------------------------------------------------
# E2E test namespace for workloads
# ---------------------------------------------------------------------------
@pytest.fixture()
def e2e_namespace(k8s_clients):
    """Create and destroy an isolated namespace for test workloads."""
    core = k8s_clients["core"]
    ns = client.V1Namespace(metadata=client.V1ObjectMeta(name=E2E_NAMESPACE))

    try:
        core.create_namespace(ns)
    except ApiException as e:
        if e.status == 409:
            # Already exists — clean it out
            _delete_all_pods_in_ns(core, E2E_NAMESPACE)
        else:
            raise

    yield E2E_NAMESPACE

    # Teardown: delete namespace (cascades all resources)
    try:
        core.delete_namespace(E2E_NAMESPACE)
    except ApiException:
        pass

    # Wait for namespace to be fully gone (up to 60s)
    for _ in range(60):
        try:
            core.read_namespace(E2E_NAMESPACE)
            time.sleep(1)
        except ApiException as e:
            if e.status == 404:
                break


def _delete_all_pods_in_ns(core: client.CoreV1Api, namespace: str) -> None:
    """Delete all pods in a namespace."""
    try:
        core.delete_collection_namespaced_pod(namespace)
    except ApiException:
        pass


# ---------------------------------------------------------------------------
# Helper fixture: create a policy via the K8s API
# ---------------------------------------------------------------------------
@pytest.fixture()
def create_policy(k8s_clients, install_crds):
    """Return a callable that creates a NodeTerminationPolicy and tracks it for cleanup."""
    custom = k8s_clients["custom"]
    created: list[str] = []

    def _create(name: str, spec: dict) -> dict:
        body = {
            "apiVersion": f"{CRD_GROUP}/{CRD_VERSION}",
            "kind": "NodeTerminationPolicy",
            "metadata": {"name": name},
            "spec": spec,
        }
        result = custom.create_cluster_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            body=body,
        )
        created.append(name)
        return result

    yield _create

    # Cleanup all policies created via this fixture
    for policy_name in created:
        try:
            custom.delete_cluster_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                plural=CRD_PLURAL,
                name=policy_name,
            )
        except ApiException as e:
            if e.status != 404:
                raise
