"""Integration test fixtures — starts real Rungate server + mock provider.

These fixtures manage the full test stack:
  Python SDK → HTTP → Rungate server (subprocess) → HTTP → Mock Provider (Python thread)

Usage:
  RUNGATE_SERVER_PATH=/path/to/rungate pytest -m integration -v
"""

from __future__ import annotations

import os
import signal
import subprocess
import tempfile
import time
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import httpx
import pytest

from rungate import Admin, Rungate

from .mock_provider import MockOpenAIProvider

RUNGATE_PORT = 18080
MOCK_PROVIDER_PORT = 18081
TEST_PASSWORD = "integration-test-password"
ENCRYPTION_KEY = "test-integration-key-32chars!!"


def find_rungate_server() -> Path | None:
    """Find the rungate server directory."""
    if env_path := os.environ.get("RUNGATE_SERVER_PATH"):
        p = Path(env_path)
        if (p / "dist" / "index.js").exists():
            return p
        return None

    sdk_dir = Path(__file__).parent.parent
    candidates = [
        sdk_dir.parent / "rungate",
        Path.home() / "Desktop" / "rungate",
        Path("/tmp/rungate"),
    ]
    for candidate in candidates:
        if (candidate / "dist" / "index.js").exists():
            return candidate
    return None


@dataclass
class RunningServer:
    url: str
    process: subprocess.Popen  # type: ignore[type-arg]
    db_path: str


def _wait_for_health(url: str, timeout: float = 10.0) -> bool:
    """Poll health endpoint until server is ready."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            resp = httpx.get(f"{url}/health", timeout=2)
            if resp.status_code == 200:
                data = resp.json()
                if data.get("status") == "healthy":
                    return True
        except Exception:
            pass
        time.sleep(0.2)
    return False


@pytest.fixture(scope="session")
def mock_provider():
    """Start mock OpenAI provider for the test session."""
    provider = MockOpenAIProvider(port=MOCK_PROVIDER_PORT)
    provider.start()
    yield provider
    provider.stop()


@pytest.fixture(scope="session")
def rungate_server(mock_provider: MockOpenAIProvider) -> RunningServer:  # type: ignore[misc]  # noqa: ARG001
    """Start Rungate server as subprocess. Depends on mock_provider to ensure it starts first."""
    server_path = find_rungate_server()
    if server_path is None:
        pytest.skip("Rungate server not found. Set RUNGATE_SERVER_PATH or build ../rungate")

    # Create temp DB file
    db_fd, db_path = tempfile.mkstemp(suffix=".db")
    os.close(db_fd)
    os.unlink(db_path)  # Let Rungate create it fresh

    # Write temp config
    config_fd, config_path = tempfile.mkstemp(suffix=".yaml")
    with os.fdopen(config_fd, "w") as f:
        f.write(
            f"port: {RUNGATE_PORT}\n"
            f"host: 127.0.0.1\n"
            f"encryption_key: {ENCRYPTION_KEY}\n"
            f"log_level: error\n"
            f"storage:\n"
            f"  type: sqlite\n"
            f"  path: {db_path}\n"
        )

    env = {**os.environ, "RUNGATE_DASHBOARD_PASSWORD": TEST_PASSWORD}

    proc = subprocess.Popen(
        ["node", str(server_path / "dist" / "index.js"), "start", "--config", config_path],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    url = f"http://127.0.0.1:{RUNGATE_PORT}"
    if not _wait_for_health(url):
        proc.kill()
        stdout = proc.stdout.read().decode() if proc.stdout else ""
        stderr = proc.stderr.read().decode() if proc.stderr else ""
        # Clean up temp files before failing
        os.unlink(config_path)
        if os.path.exists(db_path):
            os.unlink(db_path)
        pytest.fail(f"Rungate server failed to start.\nstdout: {stdout}\nstderr: {stderr}")

    yield RunningServer(url=url, process=proc, db_path=db_path)

    # Teardown
    proc.send_signal(signal.SIGTERM)
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        proc.kill()
    os.unlink(config_path)
    if os.path.exists(db_path):
        os.unlink(db_path)


@pytest.fixture
def admin(rungate_server: RunningServer) -> Admin:  # type: ignore[misc]
    """Admin client pointing at test server."""
    client = Admin(token=TEST_PASSWORD, base_url=rungate_server.url)
    yield client  # type: ignore[misc]
    client.close()


@pytest.fixture
def agent_setup(
    rungate_server: RunningServer,
    admin: Admin,
    mock_provider: MockOpenAIProvider,  # noqa: ARG001
) -> dict[str, Any]:  # type: ignore[misc]
    """Create a test agent with provider key and policy. Depends on mock_provider for ordering."""
    prefix = str(uuid.uuid4())[:8]

    key_result = admin.provider_keys.create(
        provider="openai",
        name=f"mock-{prefix}",
        api_key="sk-mock-test",
        base_url=f"http://127.0.0.1:{MOCK_PROVIDER_PORT}",
    )

    policy_result = admin.policies.create(
        name=f"test-policy-{prefix}",
        allowed_models=["gpt-4o"],
        budget={"hard_limit_usd": 1.0, "hard_limit_tokens": 100000},
        routing={
            "default_chain": ["openai"],
            "timeout_ms": 10000,
            "retry_on": [],
            "max_retries": 1,
            "on_rate_limit": "failover_first",
        },
    )

    agent_result = admin.agents.create(
        name=f"test-agent-{prefix}",
        policy_id=policy_result["data"]["id"],
        provider_keys={"openai": key_result["data"]["id"]},
    )

    return {
        "agent_id": agent_result["data"]["id"],
        "api_key": agent_result["data"]["api_key"],
        "policy_id": policy_result["data"]["id"],
        "provider_key_id": key_result["data"]["id"],
        "server_url": rungate_server.url,
    }


@pytest.fixture
def agent_client(agent_setup: dict[str, Any]) -> Rungate:  # type: ignore[misc]
    """Rungate client for the test agent."""
    client = Rungate(api_key=agent_setup["api_key"], base_url=agent_setup["server_url"])
    yield client  # type: ignore[misc]
    client.close()
