"""E2E: maintenance on/off lifecycle + apps:status on a real K8s cluster.

Verifies actual pod behavior — not just API success, but that:
- maintenance:on actually kills all pods (0 running)
- maintenance:off actually restores pods (N running)
- per-service maintenance leaves other services untouched
- apps:status returns real aggregated data
- annotations persist correctly across operations

Run with: pytest tests/e2e/test_maintenance_lifecycle.py -v -s
"""

from __future__ import annotations

from typing import Any

import pytest

from kuberoku.factory import KuberokuFactory
from kuberoku.k8s import labels as k8s_labels
from kuberoku.k8s.resources import safe_name
from tests.e2e.helpers import (
    cleanup_namespace_fixture,
    is_pod_active,
    make_real_factory,
    skip_if_no_cluster,
    wait_for_no_pods,
    wait_for_pods_ready,
)


@pytest.fixture(scope="module")
def real_factory() -> KuberokuFactory:
    return make_real_factory("e2e-maint")


@pytest.fixture(scope="module", autouse=True)
def cleanup_namespace(real_factory: KuberokuFactory) -> Any:
    yield
    cleanup_namespace_fixture(real_factory)


# ── Prerequisites ────────────────────────────────────────────────────


def test_00_skip_if_no_cluster() -> None:
    skip_if_no_cluster()


def test_01_create_and_deploy(real_factory: KuberokuFactory) -> None:
    """Create app and deploy nginx with 2 replicas."""
    real_factory.apps.create("mainttest", on_step=lambda s: None)
    real_factory.deploy.deploy(
        "mainttest",
        image="nginx:1.27-alpine",
        ports=[{"containerPort": 80, "protocol": "TCP"}],
        wait=False,
    )
    # Wait for first pod, then scale to 2
    wait_for_pods_ready(real_factory, "mainttest", expected=1)
    real_factory.ps.scale("mainttest", {"web": 2})
    pods = wait_for_pods_ready(real_factory, "mainttest", expected=2)
    assert len(pods) >= 2
    print(f"\n  Deployed mainttest with {len(pods)} web pods")


def test_02_set_config(real_factory: KuberokuFactory) -> None:
    """Set config vars so apps:status has something to report."""
    real_factory.config.set("mainttest", {"DATABASE_URL": "postgres://localhost/db"})
    real_factory.config.set(
        "mainttest",
        {"API_KEY": "secret123"},
        secret=True,
    )
    # Config set triggers rolling restart; wait for new pods
    wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)
    print("\n  Set 1 configmap var + 1 secret var")


# ── apps:status — verify real aggregated data ────────────────────────


def test_03_apps_status_shows_real_data(real_factory: KuberokuFactory) -> None:
    """apps:status should aggregate real data from the cluster."""
    # Wait for rolling update from config.set to finish (exactly 2 ready pods)
    wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)

    status = real_factory.apps.status("mainttest")

    # App exists
    assert status.app.name == "mainttest"
    print(f"\n  App: {status.app.name}")

    # Dynos — should have at least 2 web dynos
    web_dynos = [d for d in status.dynos if d.process_type == "web"]
    assert len(web_dynos) >= 2, f"Expected >= 2 web dynos, got {len(web_dynos)}"
    print(f"  Dynos: {len(web_dynos)} web ({[d.state for d in web_dynos]})")

    # Config counts
    assert status.config_count >= 1, f"Expected >= 1 config var, got {status.config_count}"
    assert status.secret_count >= 1, f"Expected >= 1 secret, got {status.secret_count}"
    print(f"  Config: {status.config_count} vars, {status.secret_count} secrets")

    # Latest release
    assert status.latest_release is not None
    assert status.latest_release.version >= 1
    print(f"  Release: v{status.latest_release.version}")

    # Maintenance should be OFF
    assert status.app.maintenance is False
    print("  Maintenance: off")


# ── apps:maintenance:on — verify pods actually die ───────────────────


def test_04_maintenance_on(real_factory: KuberokuFactory) -> None:
    """apps:maintenance:on should scale ALL pods to 0 and save replicas."""
    steps: list[str] = []
    info = real_factory.apps.maintenance_on("mainttest", on_step=steps.append)

    assert info.enabled is True
    assert info.app_wide is True
    assert info.since is not None
    assert info.saved_replicas.get("web") == 2
    print(f"\n  Maintenance ON: saved_replicas={info.saved_replicas}")
    print(f"  Steps: {steps}")


def test_05_verify_pods_gone(real_factory: KuberokuFactory) -> None:
    """After maintenance:on, no Running pods should exist."""
    wait_for_no_pods(real_factory, "mainttest", timeout=60)

    # Double-check: list pods and verify none are active (non-terminating)
    selector = k8s_labels.selector(real_factory.domain, "mainttest", "web")
    pods = real_factory.k8s.list_pods(real_factory.namespace, labels=selector)
    active = [p for p in pods if is_pod_active(p)]
    assert len(active) == 0, f"Expected 0 active pods, found {len(active)}"
    print(f"\n  Pods after maintenance:on: {len(active)} active (correct!)")


def test_06_verify_deployment_has_zero_replicas(real_factory: KuberokuFactory) -> None:
    """The Deployment spec should have replicas=0."""
    dep_name = safe_name(real_factory.prefix, "mainttest", "web")
    dep = real_factory.k8s.get_deployment(real_factory.namespace, dep_name)
    replicas = dep.get("spec", {}).get("replicas", -1)
    assert replicas == 0, f"Expected replicas=0, got {replicas}"
    print(f"\n  Deployment {dep_name} replicas={replicas}")


def test_07_status_reflects_maintenance(real_factory: KuberokuFactory) -> None:
    """apps:status should show maintenance=True and 0 running dynos."""
    status = real_factory.apps.status("mainttest")
    assert status.app.maintenance is True, "app.maintenance should be True"

    running_dynos = [d for d in status.dynos if d.state == "running"]
    assert len(running_dynos) == 0, f"Expected 0 running dynos, got {len(running_dynos)}"
    print(f"\n  Status: maintenance={status.app.maintenance}, running_dynos={len(running_dynos)}")


def test_08_info_reflects_maintenance(real_factory: KuberokuFactory) -> None:
    """apps:info should show maintenance=True."""
    app = real_factory.apps.info("mainttest")
    assert app.maintenance is True
    print(f"\n  info() maintenance={app.maintenance}")


def test_09_maintenance_status(real_factory: KuberokuFactory) -> None:
    """maintenance_status() should return correct state."""
    info = real_factory.apps.maintenance_status("mainttest")
    assert info.enabled is True
    assert info.app_wide is True
    assert info.saved_replicas.get("web") == 2
    assert info.since is not None
    print(f"\n  maintenance_status: enabled={info.enabled}, saved={info.saved_replicas}")


# ── apps:maintenance:off — verify pods come back ─────────────────────


def test_10_maintenance_off(real_factory: KuberokuFactory) -> None:
    """apps:maintenance:off should restore pods to saved replicas."""
    steps: list[str] = []
    info = real_factory.apps.maintenance_off("mainttest", on_step=steps.append)

    assert info.enabled is False
    assert info.app_wide is False
    assert info.saved_replicas == {}
    print(f"\n  Maintenance OFF: {steps}")


def test_11_verify_pods_restored(real_factory: KuberokuFactory) -> None:
    """After maintenance:off, 2 web pods should be Running again."""
    pods = wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)
    assert len(pods) >= 2
    print(f"\n  Pods restored: {len(pods)} running")


def test_12_verify_deployment_replicas_restored(real_factory: KuberokuFactory) -> None:
    """The Deployment spec should have replicas=2 after restore."""
    dep_name = safe_name(real_factory.prefix, "mainttest", "web")
    dep = real_factory.k8s.get_deployment(real_factory.namespace, dep_name)
    replicas = dep.get("spec", {}).get("replicas", -1)
    assert replicas == 2, f"Expected replicas=2, got {replicas}"
    print(f"\n  Deployment {dep_name} replicas={replicas}")


def test_13_status_after_restore(real_factory: KuberokuFactory) -> None:
    """apps:status should show maintenance=False and >= 2 running dynos."""
    # Let pods stabilize after restore — rolling updates may create transient surge
    wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)

    status = real_factory.apps.status("mainttest")
    assert status.app.maintenance is False

    web_dynos = [d for d in status.dynos if d.process_type == "web"]
    assert len(web_dynos) >= 2, f"Expected >= 2 web dynos, got {len(web_dynos)}"
    print(
        f"\n  Status after restore: maintenance={status.app.maintenance}, dynos={len(web_dynos)}"
    )


def test_14_maintenance_idempotent(real_factory: KuberokuFactory) -> None:
    """Calling maintenance:on twice should not fail or corrupt state."""
    real_factory.apps.maintenance_on("mainttest")
    wait_for_no_pods(real_factory, "mainttest", timeout=60)
    # Second call — should not 409 (retry logic)
    info = real_factory.apps.maintenance_on("mainttest")
    assert info.enabled is True
    # Restore for next tests
    real_factory.apps.maintenance_off("mainttest")
    wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)
    print("\n  Idempotent: on->on->off works without 409")


# ── Per-service maintenance — verify surgical precision ──────────────


def test_15_deploy_worker(real_factory: KuberokuFactory) -> None:
    """Add a worker process type via redeploy with commands."""
    # Ensure web is healthy first (from idempotent test restore)
    wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)

    # set_commands updates formation, redeploy creates the Deployment
    real_factory.ps.set_commands("mainttest", {"worker": "sleep 3600"})
    real_factory.deploy.deploy(
        "mainttest",
        image="nginx:1.27-alpine",
        ports=[{"containerPort": 80, "protocol": "TCP"}],
        wait=False,
        no_procfile=True,
    )
    wait_for_pods_ready(
        real_factory,
        "mainttest",
        process_type="worker",
        expected=1,
        timeout=90,
    )
    # Also ensure web is still running (may take time after rolling update)
    wait_for_pods_ready(real_factory, "mainttest", expected=2, timeout=90)
    print("\n  Deployed worker process (1 replica) + web (2 replicas)")


def test_16_service_maintenance_on_worker(real_factory: KuberokuFactory) -> None:
    """services:maintenance:on worker — should only kill worker pods."""
    steps: list[str] = []
    real_factory.services.maintenance_on(
        "mainttest",
        "worker",
        on_step=steps.append,
    )
    print(f"\n  Worker maintenance ON: {steps}")


def test_17_verify_worker_gone_web_alive(real_factory: KuberokuFactory) -> None:
    """Worker pods should be gone, web pods should still be running."""
    # Worker: 0 pods
    wait_for_no_pods(real_factory, "mainttest", process_type="worker", timeout=60)

    # Web: still >= 2 pods — untouched by per-service maintenance
    web_pods = wait_for_pods_ready(real_factory, "mainttest", process_type="web", expected=2)
    assert len(web_pods) >= 2
    print(f"\n  Worker: 0 pods, Web: {len(web_pods)} pods (correct!)")


def test_18_maintenance_info_per_service(real_factory: KuberokuFactory) -> None:
    """maintenance_status should show worker in services list, not app-wide."""
    info = real_factory.apps.maintenance_status("mainttest")
    assert info.enabled is True
    assert info.app_wide is False, "Should NOT be app-wide (only worker)"
    assert "worker" in info.services
    assert info.saved_replicas.get("worker") == 1
    print(f"\n  Maintenance info: services={info.services}, saved={info.saved_replicas}")


def test_19_service_maintenance_off_worker(real_factory: KuberokuFactory) -> None:
    """services:maintenance:off worker — restore worker pods."""
    steps: list[str] = []
    real_factory.services.maintenance_off(
        "mainttest",
        "worker",
        on_step=steps.append,
    )
    print(f"\n  Worker maintenance OFF: {steps}")


def test_20_verify_worker_restored(real_factory: KuberokuFactory) -> None:
    """Worker pod should be back, web still running."""
    worker_pods = wait_for_pods_ready(
        real_factory,
        "mainttest",
        process_type="worker",
        expected=1,
        timeout=90,
    )
    web_pods = wait_for_pods_ready(
        real_factory,
        "mainttest",
        process_type="web",
        expected=2,
    )
    assert len(worker_pods) >= 1
    assert len(web_pods) >= 2
    print(f"\n  Worker: {len(worker_pods)} pod, Web: {len(web_pods)} pods (all restored)")


def test_21_maintenance_cleared_after_last_service(
    real_factory: KuberokuFactory,
) -> None:
    """After restoring the last service, maintenance flag should be fully cleared."""
    info = real_factory.apps.maintenance_status("mainttest")
    assert info.enabled is False
    assert info.services == ()
    assert info.saved_replicas == {}

    app = real_factory.apps.info("mainttest")
    assert app.maintenance is False
    print(f"\n  All clear: enabled={info.enabled}, maintenance={app.maintenance}")


# ── Cleanup ──────────────────────────────────────────────────────────


def test_22_cleanup(real_factory: KuberokuFactory) -> None:
    """Destroy the test app."""
    real_factory.apps.destroy("mainttest")
    apps = real_factory.apps.list()
    assert not any(a.name == "mainttest" for a in apps)
    print("\n  Cleanup complete")
