"""Integration tests for the sidecar process.

These tests spawn a real sidecar subprocess and a real HTTP server to verify
the full round-trip: TykoClient → IPC → sidecar → HTTP.

The sidecar runs in a separate process, so pytest_httpx does NOT intercept its
requests. Instead we run a minimal aiohttp server in a background thread.
"""

import asyncio
import os
import threading
import time
from collections import defaultdict
from typing import Any
from unittest.mock import patch

import pytest
from aiohttp import web

from tyko import TykoClient

pytestmark = pytest.mark.integration


# ---------------------------------------------------------------------------
# Minimal HTTP capture server (aiohttp)
# ---------------------------------------------------------------------------


class _CaptureServer:
    """Runs an aiohttp server that captures all requests."""

    def __init__(self) -> None:
        self.requests: list[dict[str, Any]] = []
        self._app = web.Application()
        self._app.router.add_route("*", "/{path_info:.*}", self._handler)
        self._runner: web.AppRunner | None = None
        self._url: str = ""
        self._loop: asyncio.AbstractEventLoop | None = None
        self._thread: threading.Thread | None = None
        self._ready = threading.Event()

    async def _handler(self, request: web.Request) -> web.Response:
        body: Any = {}
        try:
            body = await request.json()
        except Exception:
            pass
        self.requests.append(
            {
                "method": request.method,
                "path": request.path,
                "body": body,
            }
        )
        # Return sensible defaults
        path = request.path
        if path == "/runs" and request.method == "POST":
            return web.json_response(
                {
                    "id": "integration-run-id",
                    "name": "test-run",
                    "sequential": 1,
                    "project": {"name": "test-project"},
                    "experiment": {"name": "default"},
                }
            )
        return web.json_response({"status": "ok"})

    def _run(self) -> None:
        self._loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._loop)
        self._loop.run_until_complete(self._start())
        self._ready.set()
        self._loop.run_forever()

    async def _start(self) -> None:
        self._runner = web.AppRunner(self._app)
        await self._runner.setup()
        site = web.TCPSite(self._runner, "127.0.0.1", 0)
        await site.start()
        port = site._server.sockets[0].getsockname()[1]  # type: ignore[union-attr]
        self._url = f"http://127.0.0.1:{port}"

    def start(self) -> "str":
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        self._ready.wait(timeout=5)
        return self._url

    def stop(self) -> None:
        if self._loop:
            self._loop.call_soon_threadsafe(self._loop.stop)

    def get_requests_by_method_path(self) -> dict[str, list[dict[str, Any]]]:
        result: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for r in self.requests:
            key = f"{r['method']} {r['path']}"
            result[key].append(r)
        return result


@pytest.fixture
def capture_server():
    server = _CaptureServer()
    url = server.start()
    yield server, url
    server.stop()


# ---------------------------------------------------------------------------
# Integration tests
# ---------------------------------------------------------------------------


def test_sidecar_sends_run_log(capture_server):
    """run.log() calls arrive at the API server via the sidecar."""
    server, url = capture_server

    with patch.dict(os.environ, {"TYKO_API_KEY": "test-key"}):
        client = TykoClient(api_url=url)

        if client._ipc is None:
            pytest.skip("Sidecar failed to start — skipping integration test")

        with client.start_run(project="test-project", system_monitor_interval=None) as run:
            run.log({"train/loss": 0.5})
            run.log({"train/loss": 0.4})
            time.sleep(0.1)  # let batch window close

    # Give sidecar time to flush
    time.sleep(1.0)

    by_path = server.get_requests_by_method_path()
    entry_posts = by_path.get("POST /runs/integration-run-id/entries", [])
    assert len(entry_posts) >= 1

    all_entries = []
    for req in entry_posts:
        all_entries.extend(req["body"].get("entries", []))

    assert len(all_entries) == 2
    values = [e["values"] for e in all_entries]
    assert any(v.get("train/loss") == 0.5 for v in values)
    assert any(v.get("train/loss") == 0.4 for v in values)


def test_sidecar_sends_final_status(capture_server):
    """run.__exit__() sends the final PATCH status=completed via the sidecar."""
    server, url = capture_server

    with patch.dict(os.environ, {"TYKO_API_KEY": "test-key"}):
        client = TykoClient(api_url=url)

        if client._ipc is None:
            pytest.skip("Sidecar failed to start — skipping integration test")

        with client.start_run(project="test-project", system_monitor_interval=None):
            pass

    time.sleep(0.5)

    by_path = server.get_requests_by_method_path()
    patches = by_path.get("PATCH /runs/integration-run-id", [])
    statuses = [r["body"].get("status") for r in patches]
    assert "completed" in statuses


def test_sidecar_sends_params(capture_server):
    """run.params updates are forwarded to the API by the sidecar."""
    server, url = capture_server

    with patch.dict(os.environ, {"TYKO_API_KEY": "test-key"}):
        client = TykoClient(api_url=url)

        if client._ipc is None:
            pytest.skip("Sidecar failed to start — skipping integration test")

        with client.start_run(project="test-project", system_monitor_interval=None) as run:
            run.params["learning_rate"] = 0.001

    time.sleep(0.5)

    by_path = server.get_requests_by_method_path()
    patches = by_path.get("PATCH /runs/integration-run-id", [])
    params_patches = [r for r in patches if "params" in r["body"]]
    assert any(r["body"]["params"].get("learning_rate") == 0.001 for r in params_patches)


def test_fork_child_does_not_send_duplicate_metrics(capture_server):
    """Forked child processes must not write to the parent's sidecar socket."""
    server, url = capture_server

    with patch.dict(os.environ, {"TYKO_API_KEY": "test-key"}):
        client = TykoClient(api_url=url)

        if client._ipc is None:
            pytest.skip("Sidecar failed to start — skipping integration test")

        with client.start_run(project="test-project", system_monitor_interval=None) as run:
            pid = os.fork()
            if pid == 0:
                # Child: this should be a no-op due to PID guard
                run.log({"loss": 999.0})
                os._exit(0)
            os.waitpid(pid, 0)
            # Only the parent's log should arrive
            run.log({"loss": 0.5})
            time.sleep(0.1)

    time.sleep(0.5)

    by_path = server.get_requests_by_method_path()
    entry_posts = by_path.get("POST /runs/integration-run-id/entries", [])
    all_values = []
    for req in entry_posts:
        for entry in req["body"].get("entries", []):
            all_values.extend(entry.get("values", {}).values())

    assert 999.0 not in all_values
    assert 0.5 in all_values


def test_sidecar_system_metrics_appear(capture_server):
    """System metrics are logged when system_monitor_interval is set."""
    server, url = capture_server

    with patch.dict(os.environ, {"TYKO_API_KEY": "test-key"}):
        client = TykoClient(api_url=url)

        if client._ipc is None:
            pytest.skip("Sidecar failed to start — skipping integration test")

        with client.start_run(project="test-project", system_monitor_interval=0.5) as run:
            run.log({"step": 0})
            time.sleep(1.2)  # wait for at least one system sample

    time.sleep(0.5)

    by_path = server.get_requests_by_method_path()
    entry_posts = by_path.get("POST /runs/integration-run-id/entries", [])
    all_metric_names: set[str] = set()
    for req in entry_posts:
        for entry in req["body"].get("entries", []):
            all_metric_names.update(entry.get("values", {}).keys())

    system_metrics = {k for k in all_metric_names if k.startswith("system/")}
    assert len(system_metrics) > 0, f"No system/ metrics found. Got: {all_metric_names}"
