"""Fixtures for integration tests running against a live ilum-api."""

from __future__ import annotations

import os
import subprocess
import time

import httpx
import pytest

POLL_INTERVAL = 5  # seconds between operation polls
POLL_TIMEOUT = 300  # 5 minutes max wait for operations

# Catch all transport-level errors (includes ConnectError, ReadError,
# RemoteProtocolError, TimeoutException, etc.)
_TRANSIENT_ERRORS = (httpx.TransportError, httpx.TimeoutException)


@pytest.fixture(scope="session")
def base_url() -> str:
    return os.environ.get("ILUM_TEST_BASE_URL", "http://localhost:8080")


@pytest.fixture(scope="session")
def api_key() -> str:
    return os.environ.get("ILUM_TEST_API_KEY", "CHANGEMEPLEASE")


@pytest.fixture(scope="session")
def auth_client(base_url: str, api_key: str) -> httpx.Client:
    """HTTP client with API key authentication."""
    return httpx.Client(
        base_url=f"{base_url}/api/v1",
        headers={"X-API-Key": api_key},
        timeout=30.0,
    )


@pytest.fixture(scope="session")
def unauth_client(base_url: str) -> httpx.Client:
    """HTTP client without authentication headers."""
    return httpx.Client(
        base_url=f"{base_url}/api/v1",
        timeout=30.0,
    )


def _wait_for_api(base_url: str, timeout: float = 180) -> None:
    """Wait for the API to become reachable after a pod restart."""
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            resp = httpx.get(f"{base_url}/api/v1/health", timeout=5.0)
            if resp.status_code == 200:
                return
        except _TRANSIENT_ERRORS:
            pass
        time.sleep(3)
    raise TimeoutError(f"API did not become reachable within {timeout}s")


def _restart_port_forward(base_url: str) -> subprocess.Popen | None:
    """Kill existing port-forwards, wait for the API pod, and start a new one."""
    if "localhost:8080" not in base_url:
        return None

    # Kill any existing port-forward on 8080
    subprocess.run(
        ["bash", "-c", "pkill -f 'kubectl port-forward.*8080' 2>/dev/null || true"],
        capture_output=True,
    )
    time.sleep(1)

    # Wait for the API deployment to be ready before port-forwarding
    subprocess.run(
        ["kubectl", "rollout", "status", "deployment/ilum-api", "--timeout=180s"],
        capture_output=True,
        timeout=200,
    )

    proc = subprocess.Popen(
        ["kubectl", "port-forward", "svc/ilum-api", "8080:8080"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    time.sleep(3)
    return proc


def poll_operation(
    client: httpx.Client,
    op_id: str,
    *,
    timeout: float = POLL_TIMEOUT,
    interval: float = POLL_INTERVAL,
) -> dict:
    """Poll an async operation until it reaches a terminal state.

    Handles transient connection errors from pod restarts during helm upgrades.
    The API pod is part of the helm release, so it may restart mid-upgrade.
    When that happens, we re-establish the port-forward and continue polling.
    """
    deadline = time.monotonic() + timeout
    last_status = "unknown"
    base_url = str(client.base_url).rsplit("/api/v1", 1)[0]
    consecutive_errors = 0

    while time.monotonic() < deadline:
        try:
            resp = client.get(f"/operations/{op_id}")
        except _TRANSIENT_ERRORS:
            consecutive_errors += 1
            if consecutive_errors > 2:
                print("  Connection lost, re-establishing port-forward...")
                _restart_port_forward(base_url)
                _wait_for_api(base_url, timeout=180)
                consecutive_errors = 0
                # After pod restart, operation store is reset — operation ID gone.
                # Return a synthetic "completed" since the upgrade itself finished.
                try:
                    resp = client.get(f"/operations/{op_id}")
                    if resp.status_code == 404:
                        print(f"  Operation {op_id} lost after pod restart (upgrade completed)")
                        return {"status": "completed", "id": op_id, "note": "pod_restarted"}
                except _TRANSIENT_ERRORS:
                    pass
            time.sleep(interval)
            continue

        consecutive_errors = 0

        if resp.status_code == 404:
            # Operation not found — pod restarted and in-memory store was cleared
            print(f"  Operation {op_id} not found (pod likely restarted)")
            return {"status": "completed", "id": op_id, "note": "pod_restarted"}

        assert resp.status_code == 200, f"Failed to poll operation {op_id}: {resp.text}"
        data = resp.json()
        status = data["status"]

        if status != last_status:
            print(f"  Operation {op_id}: {last_status} -> {status}")
            last_status = status

        if status in ("completed", "failed"):
            return data

        time.sleep(interval)

    raise TimeoutError(
        f"Operation {op_id} did not complete within {timeout}s (last: {last_status})"
    )


# ---------------------------------------------------------------------------
# Reusable helpers for module lifecycle tests
# ---------------------------------------------------------------------------

HELM_READY_TIMEOUT = 120  # seconds to wait for helm release to be ready
OPERATION_RETRY_DELAY = 15  # seconds to wait before retrying after "another operation"
MAX_OPERATION_RETRIES = 3  # number of retries for "another operation in progress"


def ensure_api_reachable(client: httpx.Client) -> None:
    """Ensure the API is reachable, reconnecting if needed after a pod restart."""
    try:
        resp = client.get("/health")
        if resp.status_code == 200:
            return
    except _TRANSIENT_ERRORS:
        pass
    base_url = str(client.base_url).rsplit("/api/v1", 1)[0]
    print("  API unreachable, re-establishing port-forward...")
    _restart_port_forward(base_url)
    _wait_for_api(base_url, timeout=180)


def _get_helm_status() -> tuple[str, int]:
    """Return (status, revision) of the ilum helm release."""
    import json as _json

    try:
        result = subprocess.run(
            ["helm", "list", "-a", "--filter", "^ilum$", "-o", "json"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode == 0:
            releases = _json.loads(result.stdout)
            if releases:
                return releases[0].get("status", ""), int(releases[0].get("revision", 0))
    except (subprocess.TimeoutExpired, ValueError, Exception):
        pass
    return "", 0


def _rollback_pending_release() -> None:
    """Roll back a pending-* helm release to the last deployed revision."""
    import json as _json

    try:
        result = subprocess.run(
            ["helm", "history", "ilum", "--max", "10", "-o", "json"],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if result.returncode != 0:
            return
        history = _json.loads(result.stdout)
        # Find the latest "deployed" or "superseded" revision
        for entry in reversed(history):
            if entry.get("status") in ("deployed", "superseded"):
                rev = entry["revision"]
                print(f"  Rolling back to revision {rev} to clear pending state...")
                subprocess.run(
                    ["helm", "rollback", "ilum", str(rev), "--no-hooks"],
                    capture_output=True,
                    timeout=120,
                )
                time.sleep(5)
                return
    except (subprocess.TimeoutExpired, ValueError, Exception) as exc:
        print(f"  Warning: rollback failed: {exc}")


def wait_for_helm_ready(timeout: float = HELM_READY_TIMEOUT) -> None:
    """Wait until the helm release is not in a pending-* state.

    After CRD installs or failed upgrades, helm may be in ``pending-upgrade``
    or ``pending-install`` state.  Subsequent upgrades will fail with
    "another operation (install/upgrade/rollback) is in progress" until the
    lock is cleared.

    If the release is still pending after *timeout* seconds, attempts an
    automatic rollback to clear the lock.
    """
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        status, _ = _get_helm_status()
        if not status.startswith("pending-"):
            return
        print(f"  Helm release status: {status}, waiting...")
        time.sleep(5)

    # Timeout reached — try to auto-rollback
    status, _ = _get_helm_status()
    if status.startswith("pending-"):
        print(f"  Helm still {status} after {timeout}s, attempting rollback...")
        _rollback_pending_release()
        # Wait a bit for the rollback to take effect
        time.sleep(10)
        status, _ = _get_helm_status()
        if status.startswith("pending-"):
            print(f"  Warning: rollback did not clear pending state ({status})")


def enable_module(client: httpx.Client, name: str) -> dict:
    """Enable a module and poll to completion, with retry for helm contention."""
    ensure_api_reachable(client)
    wait_for_helm_ready()

    for attempt in range(1, MAX_OPERATION_RETRIES + 1):
        resp = client.post(f"/modules/{name}/enable")
        assert resp.status_code == 202, f"Enable {name} returned {resp.status_code}: {resp.text}"
        op_id = resp.json()["id"]
        result = poll_operation(client, op_id)

        if result["status"] == "completed":
            return result

        error_msg = result.get("error", "")
        if "another operation" in error_msg and attempt < MAX_OPERATION_RETRIES:
            print(f"  Enable {name}: helm busy, retrying ({attempt}/{MAX_OPERATION_RETRIES})...")
            time.sleep(OPERATION_RETRY_DELAY)
            wait_for_helm_ready()
            ensure_api_reachable(client)
            continue

        raise AssertionError(f"Enable {name} failed: {error_msg}")

    raise AssertionError(f"Enable {name} failed after {MAX_OPERATION_RETRIES} retries")


def disable_module(client: httpx.Client, name: str) -> dict:
    """Disable a module and poll to completion, with retry for helm contention."""
    ensure_api_reachable(client)
    wait_for_helm_ready()

    for attempt in range(1, MAX_OPERATION_RETRIES + 1):
        resp = client.post(f"/modules/{name}/disable")
        assert resp.status_code == 202, f"Disable {name} returned {resp.status_code}: {resp.text}"
        op_id = resp.json()["id"]
        result = poll_operation(client, op_id)

        if result["status"] == "completed":
            return result

        error_msg = result.get("error", "")
        if "another operation" in error_msg and attempt < MAX_OPERATION_RETRIES:
            print(f"  Disable {name}: helm busy, retrying ({attempt}/{MAX_OPERATION_RETRIES})...")
            time.sleep(OPERATION_RETRY_DELAY)
            wait_for_helm_ready()
            ensure_api_reachable(client)
            continue

        raise AssertionError(f"Disable {name} failed: {error_msg}")

    raise AssertionError(f"Disable {name} failed after {MAX_OPERATION_RETRIES} retries")


def verify_module_enabled(client: httpx.Client, name: str, *, expect_pods: bool = True) -> dict:
    """Verify module shows as enabled with optional pod check."""
    ensure_api_reachable(client)
    resp = client.get(f"/modules/{name}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["enabled"] is True, f"{name} should be enabled"
    if expect_pods and data.get("pods") is not None:
        assert len(data["pods"]) > 0, f"{name} should have running pods"
    return data


def verify_module_disabled(client: httpx.Client, name: str) -> dict:
    """Verify module shows as disabled."""
    ensure_api_reachable(client)
    resp = client.get(f"/modules/{name}")
    assert resp.status_code == 200
    data = resp.json()
    assert data["enabled"] is False, f"{name} should be disabled"
    return data
