"""E2E: Real drain of a worker node with test workloads.

These tests deploy actual pods to the cluster and exercise the Eviction API.
They target a worker (agent) node only — never the control-plane server.
"""

import time

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

from citadel_operator.k3s.node import NodeManager

pytestmark = pytest.mark.usefixtures("cluster_reachable")

# Timeout for drain operations in these tests
DRAIN_TIMEOUT = 60
# Timeout for waiting for pods to become ready (image pull can be slow in k3d)
POD_READY_TIMEOUT = 180


def _wait_for_pods_ready(
    core: client.CoreV1Api,
    namespace: str,
    label_selector: str,
    expected: int,
    timeout: int = POD_READY_TIMEOUT,
) -> None:
    """Wait until `expected` pods with the given label are Running and Ready."""
    deadline = time.monotonic() + timeout
    ready_count = 0
    while time.monotonic() < deadline:
        pods = core.list_namespaced_pod(namespace, label_selector=label_selector)
        ready_count = 0
        for pod in pods.items:
            if pod.status.phase == "Running":
                for cond in pod.status.conditions or []:
                    if cond.type == "Ready" and cond.status == "True":
                        ready_count += 1
                        break
        if ready_count >= expected:
            return
        time.sleep(2)
    raise TimeoutError(
        f"Only {ready_count}/{expected} pods ready after {timeout}s "
        f"(selector={label_selector}, ns={namespace})"
    )


def _deploy_test_workload(
    apps: client.AppsV1Api,
    core: client.CoreV1Api,
    namespace: str,
    node_name: str,
    replicas: int = 2,
    name: str = "e2e-nginx",
) -> None:
    """Deploy a lightweight test workload pinned to a specific node.

    Uses rancher/mirrored-pause which is pre-cached on all k3s nodes,
    avoiding image pull issues in network-isolated k3d clusters.
    """
    deployment = client.V1Deployment(
        metadata=client.V1ObjectMeta(name=name, namespace=namespace),
        spec=client.V1DeploymentSpec(
            replicas=replicas,
            selector=client.V1LabelSelector(
                match_labels={"app": name},
            ),
            template=client.V1PodTemplateSpec(
                metadata=client.V1ObjectMeta(labels={"app": name}),
                spec=client.V1PodSpec(
                    # Use nodeSelector instead of nodeName so the scheduler
                    # respects cordon state. nodeName bypasses the scheduler.
                    node_selector={"kubernetes.io/hostname": node_name},
                    termination_grace_period_seconds=3,
                    containers=[
                        client.V1Container(
                            name="pause",
                            image="rancher/mirrored-pause:3.6",
                            image_pull_policy="IfNotPresent",
                            resources=client.V1ResourceRequirements(
                                requests={"cpu": "10m", "memory": "8Mi"},
                                limits={"cpu": "50m", "memory": "16Mi"},
                            ),
                        )
                    ],
                ),
            ),
        ),
    )

    try:
        apps.create_namespaced_deployment(namespace, deployment)
    except ApiException as e:
        if e.status == 409:
            apps.replace_namespaced_deployment(name, namespace, deployment)
        else:
            raise

    _wait_for_pods_ready(core, namespace, f"app={name}", replicas)


class TestDrain:
    """Real drain operations on a worker node."""

    def test_drain_evicts_test_pods(
        self, k8s_clients, node_manager, node_name, e2e_namespace
    ):
        """Deploy test pods to worker, drain it, verify pods are evicted."""
        apps = k8s_clients["apps"]
        core = k8s_clients["core"]

        _deploy_test_workload(apps, core, e2e_namespace, node_name)

        # Verify pods are on the target node
        pods = core.list_namespaced_pod(
            e2e_namespace, label_selector="app=e2e-nginx"
        )
        on_node = [p for p in pods.items if p.spec.node_name == node_name]
        assert len(on_node) > 0, "No test pods scheduled on target node"

        # Drain the node
        result = node_manager.drain(node_name, timeout_seconds=DRAIN_TIMEOUT)

        assert result.success is True
        assert result.pods_evicted > 0
        assert result.node_name == node_name

        # Verify no test pods remain on the drained node
        pods = core.list_namespaced_pod(
            e2e_namespace, label_selector="app=e2e-nginx"
        )
        still_on_node = [
            p for p in pods.items
            if p.spec.node_name == node_name
            and p.metadata.deletion_timestamp is None
        ]
        assert len(still_on_node) == 0, (
            f"{len(still_on_node)} test pods still on drained node"
        )

    def test_drain_skips_daemonset_pods(
        self, k8s_clients, node_manager, node_name, e2e_namespace
    ):
        """DaemonSet pods should survive a drain while regular pods are evicted."""
        apps = k8s_clients["apps"]
        core = k8s_clients["core"]

        # Create a DaemonSet so we have a DS pod on the target node
        ds = client.V1DaemonSet(
            metadata=client.V1ObjectMeta(
                name="e2e-ds", namespace=e2e_namespace
            ),
            spec=client.V1DaemonSetSpec(
                selector=client.V1LabelSelector(
                    match_labels={"app": "e2e-ds"},
                ),
                template=client.V1PodTemplateSpec(
                    metadata=client.V1ObjectMeta(labels={"app": "e2e-ds"}),
                    spec=client.V1PodSpec(
                        termination_grace_period_seconds=3,
                        # Tolerate all taints so it schedules everywhere
                        tolerations=[
                            client.V1Toleration(operator="Exists"),
                        ],
                        containers=[
                            client.V1Container(
                                name="pause",
                                image="rancher/mirrored-pause:3.6",
                                image_pull_policy="IfNotPresent",
                                resources=client.V1ResourceRequirements(
                                    requests={"cpu": "10m", "memory": "8Mi"},
                                    limits={"cpu": "50m", "memory": "16Mi"},
                                ),
                            )
                        ],
                    ),
                ),
            ),
        )
        try:
            apps.create_namespaced_daemon_set(e2e_namespace, ds)
        except ApiException as e:
            if e.status != 409:
                raise

        # Also deploy a regular workload alongside
        _deploy_test_workload(
            apps, core, e2e_namespace, node_name,
            replicas=1, name="e2e-regular",
        )

        # Wait for the DaemonSet pod to be running on our target node
        deadline = time.monotonic() + 30
        ds_pod_on_node = None
        while time.monotonic() < deadline:
            pods = core.list_namespaced_pod(
                e2e_namespace,
                label_selector="app=e2e-ds",
                field_selector=f"spec.nodeName={node_name}",
            )
            for pod in pods.items:
                if pod.status.phase == "Running":
                    ds_pod_on_node = pod.metadata.name
                    break
            if ds_pod_on_node:
                break
            time.sleep(2)
        assert ds_pod_on_node, "DaemonSet pod never reached Running on target node"

        # Drain the node
        result = node_manager.drain(node_name, timeout_seconds=DRAIN_TIMEOUT)
        assert result.success is True

        # The DaemonSet pod should still be on the node
        pods_after = core.list_namespaced_pod(
            e2e_namespace,
            label_selector="app=e2e-ds",
            field_selector=f"spec.nodeName={node_name}",
        )
        ds_pods_remaining = [
            p for p in pods_after.items
            if p.metadata.deletion_timestamp is None
        ]
        assert len(ds_pods_remaining) >= 1, (
            "DaemonSet pod was evicted — drain should skip DaemonSet pods"
        )

        # The regular deployment pod should have been evicted
        regular_pods = core.list_namespaced_pod(
            e2e_namespace,
            label_selector="app=e2e-regular",
            field_selector=f"spec.nodeName={node_name}",
        )
        regular_remaining = [
            p for p in regular_pods.items
            if p.metadata.deletion_timestamp is None
        ]
        assert len(regular_remaining) == 0, (
            f"Regular pod was NOT evicted — {len(regular_remaining)} still on node"
        )

    def test_pdb_blocks_eviction(
        self, k8s_clients, node_manager, node_name, e2e_namespace
    ):
        """A PDB with minAvailable=replicas should block drain, causing timeout."""
        apps = k8s_clients["apps"]
        core = k8s_clients["core"]
        policy_api = k8s_clients["policy"]

        replicas = 2
        _deploy_test_workload(
            apps, core, e2e_namespace, node_name,
            replicas=replicas, name="e2e-pdb-test",
        )

        # Create a PDB that blocks all evictions: minAvailable = replicas
        pdb = client.V1PodDisruptionBudget(
            metadata=client.V1ObjectMeta(
                name="e2e-pdb-block", namespace=e2e_namespace
            ),
            spec=client.V1PodDisruptionBudgetSpec(
                min_available=replicas,
                selector=client.V1LabelSelector(
                    match_labels={"app": "e2e-pdb-test"},
                ),
            ),
        )
        try:
            policy_api.create_namespaced_pod_disruption_budget(e2e_namespace, pdb)
        except ApiException as e:
            if e.status == 409:
                policy_api.replace_namespaced_pod_disruption_budget(
                    "e2e-pdb-block", e2e_namespace, pdb
                )
            else:
                raise

        # Wait briefly for PDB to be recognized
        time.sleep(3)

        # Drain with a short timeout — should fail because PDB blocks eviction
        result = node_manager.drain(node_name, timeout_seconds=15)

        # The drain should have timed out or failed due to PDB
        assert not result.success or len(result.pods_failed) > 0, (
            "Expected drain to fail or have failed pods due to PDB"
        )

    def test_after_drain_node_is_cordoned(self, k8s_clients, node_manager, node_name):
        """After a drain, the node should be cordoned (unschedulable)."""
        # Drain the node (even with no test pods, drain still cordons)
        node_manager.drain(node_name, timeout_seconds=DRAIN_TIMEOUT)

        info = node_manager.get_node(node_name)
        assert not info.schedulable, "Node should be cordoned after drain"

        # Uncordon and verify it's schedulable again
        node_manager.uncordon(node_name)
        info = node_manager.get_node(node_name)
        assert info.schedulable, "Node should be schedulable after uncordon"
