"""End-to-end expose/connect/unexpose lifecycle on a real K8s cluster.

Tests the full gateway + LB expose lifecycle for both services and addons.

Marked @slow — skipped by default, run with ``--runslow``.

Requires:
- K8s cluster with nginx:1.27-alpine and redis:7 images pullable
- LoadBalancer support (or MetalLB / k3s servicelb)

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

from __future__ import annotations

from typing import Any

import pytest

from kuberoku.factory import KuberokuFactory
from kuberoku.k8s.resources import safe_name
from tests.e2e.helpers import (
    cleanup_namespace_fixture,
    find_running_pod,
    http_probe_from_pod,
    make_real_factory,
    skip_if_no_cluster,
    wait_for_lb_address,
    wait_for_pods_ready,
    wait_for_service_deleted,
    wait_for_sts_ready,
)

pytestmark = pytest.mark.slow


# ── Fixtures ─────────────────────────────────────────────────────


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


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


# ── Tests ────────────────────────────────────────────────────────


def test_00_skip_if_no_cluster(real_factory: KuberokuFactory) -> None:
    """Skip the entire module if no cluster is available."""
    skip_if_no_cluster()


def test_01_create_app(real_factory: KuberokuFactory) -> None:
    """Create an app for expose testing."""
    app = real_factory.apps.create("echo", on_step=lambda s: None)
    assert app.name == "echo"
    print(f"\n  App created: {app.name}")
    print(f"  Namespace: {real_factory.namespace}")


def test_02_deploy(real_factory: KuberokuFactory) -> None:
    """Deploy nginx with port 80."""
    release = real_factory.deploy.deploy(
        "echo",
        image="nginx:1.27-alpine",
        ports=[{"containerPort": 80, "protocol": "TCP"}],
        wait=False,
    )
    assert release.version >= 1
    print(f"\n  Deployed v{release.version}")


def test_03_wait_pods(real_factory: KuberokuFactory) -> None:
    """Wait for web pods to be Running+Ready."""
    wait_for_pods_ready(real_factory, "echo", "web")
    print("\n  Web pods ready")


def test_04_expose_service_gateway(real_factory: KuberokuFactory) -> None:
    """Expose web via gateway, verify allocation + HTTP probe."""
    endpoint = real_factory.services.expose_on("echo", "web", method="gateway")
    assert endpoint.method == "gateway"
    assert endpoint.gateway_port is not None
    assert 10000 <= endpoint.gateway_port <= 10999
    print(f"\n  Exposed via gateway port {endpoint.gateway_port}")

    # STRONG: HTTP probe from pod through gateway LB (best-effort)
    gw_svc_name = f"{real_factory.prefix}-gw-{endpoint.gateway_port}"
    gw_ip = wait_for_lb_address(real_factory, gw_svc_name, timeout=30)
    if gw_ip:
        pod_name = find_running_pod(real_factory, "echo")
        if pod_name:
            output = http_probe_from_pod(
                real_factory,
                pod_name,
                f"http://{gw_ip}:{endpoint.gateway_port}/",
            )
            if "PROBE_FAILED" not in output:
                print(f"  HTTP probe via gateway OK: {output[:100]}")
            else:
                print(f"  HTTP probe inconclusive: {output[:100]}")
    else:
        print("  LB IP pending — skipped HTTP probe")


def test_05_gateway_svc_selector(real_factory: KuberokuFactory) -> None:
    """Verify gateway LB Service has correct selector (not empty)."""
    svc_name = safe_name(real_factory.prefix, "echo", "web")
    alloc = real_factory.gateway.get_allocation("echo", svc_name)
    assert alloc is not None

    gw_svc_name = f"{real_factory.prefix}-gw-{alloc.external_port}"
    gw_svc = real_factory.k8s.get_service(real_factory.namespace, gw_svc_name)
    selector = gw_svc.get("spec", {}).get("selector", {})
    assert len(selector) > 0, f"Gateway LB Service selector is empty: {gw_svc}"
    print(f"\n  Gateway LB selector: {selector}")


def test_06_connect_via_gateway(real_factory: KuberokuFactory) -> None:
    """connect() returns gateway ConnectionTarget."""
    target = real_factory.services.resolve_connect("echo", "web")
    svc_name = safe_name(real_factory.prefix, "echo", "web")
    # resolve_connect always returns the direct service for processes
    assert target is not None
    assert target.service_name == svc_name
    print(f"\n  Connect target: {target.service_name}")


def test_07_unexpose_service_gateway(real_factory: KuberokuFactory) -> None:
    """Unexpose, verify allocation removed AND LB Service gone."""
    svc_name = safe_name(real_factory.prefix, "echo", "web")
    alloc_before = real_factory.gateway.get_allocation("echo", svc_name)
    gw_port = alloc_before.external_port if alloc_before else None

    result = real_factory.services.expose_off("echo", "web")
    assert result.method == "clusterip"

    alloc = real_factory.gateway.get_allocation("echo", svc_name)
    assert alloc is None
    print("\n  Gateway allocation removed")

    # STRONG: verify gateway LB service is deleted
    # K8s service deletion is async — poll until fully gone
    if gw_port is not None:
        gw_svc_name = f"{real_factory.prefix}-gw-{gw_port}"
        try:
            wait_for_service_deleted(real_factory, gw_svc_name)
            print(f"  Gateway LB service {gw_svc_name} correctly removed")
        except TimeoutError:
            pytest.fail(f"Gateway LB service {gw_svc_name} should have been deleted")


def test_08_expose_service_lb(real_factory: KuberokuFactory) -> None:
    """Expose via loadbalancer, verify Service type."""
    endpoint = real_factory.services.expose_on("echo", "web", method="loadbalancer")
    assert endpoint.method == "loadbalancer"
    assert endpoint.service_type == "LoadBalancer"

    svc_name = safe_name(real_factory.prefix, "echo", "web")
    svc = real_factory.k8s.get_service(real_factory.namespace, svc_name)
    assert svc["spec"]["type"] == "LoadBalancer"
    print(f"\n  Exposed via LoadBalancer")


def test_09_unexpose_service_lb(real_factory: KuberokuFactory) -> None:
    """Revert to ClusterIP."""
    result = real_factory.services.expose_off("echo", "web")
    assert result.method == "clusterip"

    svc_name = safe_name(real_factory.prefix, "echo", "web")
    svc = real_factory.k8s.get_service(real_factory.namespace, svc_name)
    assert svc["spec"]["type"] == "ClusterIP"
    print(f"\n  Reverted to ClusterIP")


def test_10_create_addon(real_factory: KuberokuFactory) -> None:
    """Create a redis addon."""
    addon = real_factory.addons.create("echo", "redis", on_step=lambda s: None)
    assert addon.addon_type == "redis"
    print(f"\n  Created redis addon")

    sts_name = safe_name(real_factory.prefix, "addon", "echo-redis")
    wait_for_sts_ready(real_factory, sts_name)
    print("  Redis pod ready")


def test_11_expose_addon_gateway(real_factory: KuberokuFactory) -> None:
    """Expose redis via gateway."""
    endpoint = real_factory.addons.expose_on("echo", "redis", method="gateway")
    assert endpoint.method == "gateway"
    assert endpoint.gateway_port is not None
    print(f"\n  Redis exposed via gateway port {endpoint.gateway_port}")


def test_12_addon_connect_gateway(real_factory: KuberokuFactory) -> None:
    """connect returns gateway target."""
    target = real_factory.addons.connect("echo", "redis")
    svc_name = safe_name(real_factory.prefix, "addon", "echo-redis")
    alloc = real_factory.gateway.get_allocation("echo", svc_name)
    assert alloc is not None
    expected_gw_svc = f"{real_factory.prefix}-gw-{alloc.external_port}"
    assert target.service_name == expected_gw_svc
    print(f"\n  Connect target: {target.service_name}")


def test_13_unexpose_addon_gateway(real_factory: KuberokuFactory) -> None:
    """Deallocate redis gateway."""
    result = real_factory.addons.expose_off("echo", "redis")
    assert result.method == "clusterip"

    svc_name = safe_name(real_factory.prefix, "addon", "echo-redis")
    alloc = real_factory.gateway.get_allocation("echo", svc_name)
    assert alloc is None
    print("\n  Redis gateway allocation removed")


def test_14_expose_and_verify_tcp(real_factory: KuberokuFactory) -> None:
    """Best-effort: exec from pod to verify gateway TCP connectivity."""
    endpoint = real_factory.services.expose_on("echo", "web", method="gateway")
    assert endpoint.gateway_port is not None

    gw_svc_name = f"{real_factory.prefix}-gw-{endpoint.gateway_port}"

    gw_ip = wait_for_lb_address(real_factory, gw_svc_name, timeout=60)
    if gw_ip is None:
        pytest.skip("LB IP stays pending — cannot test TCP connectivity")

    pod_name = find_running_pod(real_factory, "echo")
    assert pod_name is not None

    try:
        real_factory.k8s.exec_in_pod(
            real_factory.namespace,
            pod_name,
            [
                "sh",
                "-c",
                f"wget -q -O /dev/null -T 5 http://{gw_ip}:{endpoint.gateway_port}/ || true",
            ],
        )
        print(f"\n  TCP connectivity verified via {gw_ip}:{endpoint.gateway_port}")
    except Exception as e:
        print(f"\n  TCP test inconclusive: {e}")


def test_15_unexpose_and_verify_blocked(real_factory: KuberokuFactory) -> None:
    """Best-effort: verify connectivity breaks after unexpose."""
    svc_name = safe_name(real_factory.prefix, "echo", "web")
    alloc = real_factory.gateway.get_allocation("echo", svc_name)
    if alloc is None:
        pytest.skip("Not currently exposed")

    gw_port = alloc.external_port
    gw_svc_name = f"{real_factory.prefix}-gw-{gw_port}"

    real_factory.services.expose_off("echo", "web")

    # Verify allocation removed from ConfigMap (authoritative source)
    alloc_after = real_factory.gateway.get_allocation("echo", svc_name)
    assert alloc_after is None, "Gateway allocation should be removed from ConfigMap"
    print(f"\n  Gateway allocation removed for port {gw_port}")

    # Gateway LB service should be deleted (allow brief propagation delay)
    try:
        wait_for_service_deleted(real_factory, gw_svc_name)
    except TimeoutError:
        pytest.fail(f"Gateway LB service {gw_svc_name} should have been deleted")
    print(f"  Gateway LB service {gw_svc_name} correctly removed")


def test_16_cleanup(real_factory: KuberokuFactory) -> None:
    """Destroy app and addon."""
    real_factory.addons.destroy("echo", "redis")
    real_factory.apps.destroy("echo")
    apps = real_factory.apps.list()
    assert not any(a.name == "echo" for a in apps)
    print("\n  Cleanup complete")
