"""Fixtures for real end-to-end integration tests.

REFERENCE PATTERN — salvaged from pruned branch for relay M4/M5 E2E testing.
Adapt imports and paths to current codebase before using.

These fixtures create actual network namespaces, start real Ollama
instances and real LLMHosts server processes, and test through real
HTTP over a virtual LAN.  Nothing is mocked.

Topology for the default "home LAN" fixture::

    ┌───────────────────┐
    │   gpu-node (n0)   │  Ollama @ 10.99.0.2:11434
    │   qwen2.5:0.5b    │  (real model, real CPU inference)
    └────────┬──────────┘
             │ veth
    ┌────────┴──────────┐
    │    bridge (br0)   │  10.99.0.1/24
    └────────┬──────────┘
             │ veth
    ┌────────┴──────────┐
    │  llmhostss-srv(n1) │  LLMHosts proxy @ 10.99.0.3:4000
    │  discovers Ollama  │  (real FastAPI + uvicorn)
    └────────┬──────────┘
             │ veth
    ┌────────┴──────────┐
    │  client (n2)      │  Test requests from 10.99.0.4
    │  (curl / httpx)   │
    └───────────────────┘
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
import textwrap

import pytest

from .virtual_network import VirtualNetwork, wait_for_port


def _is_root() -> bool:
    return os.geteuid() == 0


def _has_ip_netns() -> bool:
    """Check if we can create network namespaces."""
    try:
        os.makedirs("/var/run/netns", exist_ok=True)
        r = subprocess.run(
            ["ip", "netns", "add", "_e2e_probe"],
            capture_output=True,
            timeout=5,
        )
        if r.returncode == 0:
            subprocess.run(["ip", "netns", "del", "_e2e_probe"], capture_output=True, timeout=5)
            return True
        return False
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False


# Skip the entire module if we can't create namespaces
_can_run = _is_root() and _has_ip_netns()
pytestmark = pytest.mark.skipif(not _can_run, reason="Requires root + network namespace support")


# ---------------------------------------------------------------------------
# Virtual LAN fixture (module-scoped for speed)
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def virtual_lan():
    """Create a 3-node virtual LAN: gpu-node, llmhosts-server, client."""
    net = VirtualNetwork(name="e2e", subnet="10.99.0")
    nodes = net.setup(num_nodes=3)
    yield net, nodes
    net.teardown()


# ---------------------------------------------------------------------------
# Real Ollama on gpu-node (n0)
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def ollama_node(virtual_lan):
    """Start a real Ollama server inside the gpu-node namespace.

    Ollama shares the host filesystem (same binary, same models), but
    its network is isolated — it listens only on 10.99.0.2:11434.
    """
    _net, nodes = virtual_lan
    gpu_node = nodes[0]  # 10.99.0.2

    # Start Ollama inside the namespace, binding to the namespace IP
    log_path = f"/tmp/ollama-e2e-{gpu_node.name}.log"
    log_file = open(log_path, "w")  # noqa: SIM115

    # Find the correct models directory (where manifests live)
    models_dir = os.environ.get("OLLAMA_MODELS", "")
    if not models_dir:
        for candidate in ["/root/.ollama/models", "/usr/share/ollama/.ollama/models"]:
            if os.path.isdir(os.path.join(candidate, "manifests")):
                models_dir = candidate
                break
        if not models_dir:
            models_dir = "/root/.ollama/models"

    proc = gpu_node.spawn(
        ["ollama", "serve"],
        env={
            "OLLAMA_HOST": f"{gpu_node.ip}:11434",
            "OLLAMA_MODELS": models_dir,
            "HOME": "/root",
        },
        stdout=log_file,
        stderr=log_file,
    )

    # Wait for Ollama to be ready
    ready = wait_for_port(gpu_node.ip, 11434, timeout=30.0, ns_name=gpu_node.name)
    if not ready:
        proc.kill()
        log_file.close()
        with open(log_path) as f:
            logs = f.read()
        pytest.fail(f"Ollama failed to start in namespace {gpu_node.name}. Logs:\n{logs}")

    yield gpu_node, proc

    proc.terminate()
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        proc.kill()
    log_file.close()


# ---------------------------------------------------------------------------
# Real LLMHosts server on llmhosts-node (n1)
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def llmhosts_server(virtual_lan, ollama_node):
    """Start a real LLMHosts server inside the llmhosts-node namespace.

    It discovers the real Ollama at 10.99.0.2:11434 and serves the
    OpenAI/Anthropic compatible proxy at 10.99.0.3:4000.
    """
    _net, nodes = virtual_lan
    gpu_node = nodes[0]
    srv_node = nodes[1]  # 10.99.0.3

    # Write a minimal startup script that configures the Ollama host
    # BEFORE the app is created (so the lifespan dispatcher discovers the
    # real Ollama in the gpu-node namespace).
    # NOTE: Package is "llmhostss" (plural). Adapt imports to current module structure.
    startup_script = textwrap.dedent(f"""\
        import sys
        import os

        # Strip proxy vars so httpx talks directly to the namespace IP
        for var in list(os.environ):
            if "proxy" in var.lower() and var not in ("no_proxy", "NO_PROXY"):
                del os.environ[var]
        os.environ["no_proxy"] = "10.99.0.0/24,localhost,127.0.0.1"
        os.environ["NO_PROXY"] = "10.99.0.0/24,localhost,127.0.0.1"

        # Set Ollama host via env var (used by llmhostss.config)
        os.environ["LLMHOST_OLLAMA_HOST"] = "http://{gpu_node.ip}:11434"

        from llmhostss.proxy.app import create_app
        import uvicorn

        app = create_app()
        uvicorn.run(app, host="{srv_node.ip}", port=4000, log_level="info")
    """)

    script_path = f"/tmp/e2e_llmhosts_server_{srv_node.name}.py"
    with open(script_path, "w") as f:
        f.write(startup_script)

    log_path = f"/tmp/llmhosts-e2e-{srv_node.name}.log"
    log_file = open(log_path, "w")  # noqa: SIM115

    proc = srv_node.spawn(
        [sys.executable, script_path],
        env={
            "PYTHONPATH": os.getcwd(),  # adapt to repo root
            "HOME": "/root",
            "OLLAMA_HOST": f"http://{gpu_node.ip}:11434",
        },
        stdout=log_file,
        stderr=log_file,
    )

    # Wait for the LLMHosts server to be ready
    ready = wait_for_port(srv_node.ip, 4000, timeout=45.0, ns_name=srv_node.name)
    if not ready:
        proc.kill()
        log_file.close()
        with open(log_path) as f:
            logs = f.read()
        pytest.fail(f"LLMHosts server failed to start in {srv_node.name}. Logs:\n{logs}")

    yield srv_node, proc

    proc.terminate()
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        proc.kill()
    log_file.close()


# ---------------------------------------------------------------------------
# Client node helper
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def client_node(virtual_lan):
    """Return the client node (n2) for making requests."""
    _, nodes = virtual_lan
    return nodes[2]  # 10.99.0.4


def curl_json(
    node,
    url: str,
    *,
    method: str = "GET",
    data: dict | None = None,
    headers: dict[str, str] | None = None,
    timeout: float = 120.0,
) -> dict:
    """Execute curl from inside a node's namespace and parse JSON response."""
    cmd = ["curl", "-s", "-S", "--max-time", str(int(timeout))]

    if method == "POST":
        cmd += ["-X", "POST"]
    if data is not None:
        cmd += ["-H", "Content-Type: application/json", "-d", json.dumps(data)]
    if headers:
        for k, v in headers.items():
            cmd += ["-H", f"{k}: {v}"]
    cmd.append(url)

    result = node.exec(cmd, timeout=timeout + 5)
    if result.returncode != 0:
        raise RuntimeError(f"curl failed (rc={result.returncode}): {result.stderr}")

    return json.loads(result.stdout)
