from __future__ import annotations

import http.client
import json
import socketserver
import sys
import threading
from collections.abc import Iterator
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from claudex_router import HybridRouter, route_target


class _StubUpstream(socketserver.ThreadingMixIn, HTTPServer):
    daemon_threads = True

    def __init__(self, label: str) -> None:
        super().__init__(("127.0.0.1", 0), _StubHandler)
        self.label = label
        self.seen: list[dict[str, object]] = []
        threading.Thread(target=self.serve_forever, daemon=True).start()

    @property
    def port(self) -> int:
        return int(self.server_address[1])


class _StubHandler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    server: _StubUpstream

    def do_GET(self) -> None:
        self._record(b"")
        self._respond(json.dumps({"upstream": self.server.label}).encode())

    def do_POST(self) -> None:
        length = int(self.headers.get("content-length") or 0)
        body = self.rfile.read(length)
        self._record(body)
        if self.path.endswith("stream"):
            self._respond_stream()
            return
        payload = json.loads(body or b"{}")
        self._respond(
            json.dumps({"upstream": self.server.label, "model": payload.get("model")}).encode()
        )

    def _record(self, body: bytes) -> None:
        try:
            model = json.loads(body).get("model") if body else None
        except json.JSONDecodeError:
            model = "<unparsed>"
        self.server.seen.append({
            "path": self.path,
            "model": model,
            "host": self.headers.get("host"),
            "authorization": self.headers.get("authorization"),
        })

    def _respond(self, payload: bytes) -> None:
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def _respond_stream(self) -> None:
        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Transfer-Encoding", "chunked")
        self.end_headers()
        for index in range(3):
            chunk = f"data: event-{index}\n\n".encode()
            self.wfile.write(f"{len(chunk):x}\r\n".encode() + chunk + b"\r\n")
            self.wfile.flush()
        self.wfile.write(b"0\r\n\r\n")
        self.wfile.flush()

    def log_message(self, format: str, *args: object) -> None:
        return


@pytest.fixture
def rig() -> Iterator[tuple[HybridRouter, _StubUpstream, _StubUpstream]]:
    codex = _StubUpstream("codex")
    anthropic = _StubUpstream("anthropic")
    router = HybridRouter(codex.port)
    router.connect_anthropic = lambda: http.client.HTTPConnection(  # type: ignore[method-assign]
        "127.0.0.1", anthropic.port, timeout=10
    )
    router.start()
    try:
        yield router, codex, anthropic
    finally:
        router.stop()
        codex.shutdown()
        codex.server_close()
        anthropic.shutdown()
        anthropic.server_close()


def post(router: HybridRouter, path: str, payload: dict[str, object]) -> tuple[int, bytes]:
    connection = http.client.HTTPConnection("127.0.0.1", router.port, timeout=10)
    try:
        connection.request(
            "POST",
            path,
            body=json.dumps(payload).encode(),
            headers={
                "Content-Type": "application/json",
                "Authorization": "Bearer sk-ant-oat01-test",
            },
        )
        response = connection.getresponse()
        return response.status, response.read()
    finally:
        connection.close()


@pytest.mark.parametrize(
    ("model", "codex", "rewritten"),
    [
        ("anthropic.gpt-5.6-sol", True, "gpt-5.6-sol"),
        ("gpt-5.6-luna", True, "gpt-5.6-luna"),
        ("claude-opus-5", False, None),
        ("fable", False, None),
        (None, False, None),
    ],
)
def test_route_target_selects_codex_only_for_gpt_models(
    model: object, codex: bool, rewritten: str | None
) -> None:
    target = route_target(model)

    assert target.codex is codex
    assert target.model == rewritten


def test_gpt_request_reaches_codex_with_prefix_stripped(
    rig: tuple[HybridRouter, _StubUpstream, _StubUpstream],
) -> None:
    router, codex, anthropic = rig

    status, body = post(router, "/v1/messages?beta=true", {"model": "anthropic.gpt-5.6-terra"})

    assert status == 200
    assert json.loads(body) == {"upstream": "codex", "model": "gpt-5.6-terra"}
    assert codex.seen[0]["model"] == "gpt-5.6-terra"
    assert codex.seen[0]["path"] == "/v1/messages?beta=true"
    assert codex.seen[0]["authorization"] == "Bearer sk-ant-oat01-test"
    assert anthropic.seen == []


def test_claude_request_reaches_anthropic_untouched(
    rig: tuple[HybridRouter, _StubUpstream, _StubUpstream],
) -> None:
    router, codex, anthropic = rig

    status, body = post(router, "/v1/messages", {"model": "claude-fable-5"})

    assert status == 200
    assert json.loads(body) == {"upstream": "anthropic", "model": "claude-fable-5"}
    assert anthropic.seen[0]["model"] == "claude-fable-5"
    assert anthropic.seen[0]["host"] == "api.anthropic.com"
    assert anthropic.seen[0]["authorization"] == "Bearer sk-ant-oat01-test"
    assert codex.seen == []


def test_bodyless_request_defaults_to_anthropic(
    rig: tuple[HybridRouter, _StubUpstream, _StubUpstream],
) -> None:
    router, codex, anthropic = rig
    connection = http.client.HTTPConnection("127.0.0.1", router.port, timeout=10)
    try:
        connection.request("GET", "/v1/models")
        response = connection.getresponse()
        body = response.read()
    finally:
        connection.close()

    assert json.loads(body) == {"upstream": "anthropic"}
    assert anthropic.seen[0]["path"] == "/v1/models"
    assert codex.seen == []


def test_streaming_response_is_relayed_chunk_by_chunk(
    rig: tuple[HybridRouter, _StubUpstream, _StubUpstream],
) -> None:
    router, _, _ = rig
    connection = http.client.HTTPConnection("127.0.0.1", router.port, timeout=10)
    try:
        connection.request(
            "POST",
            "/v1/messages/stream",
            body=json.dumps({"model": "claude-sonnet-5"}).encode(),
            headers={"Content-Type": "application/json"},
        )
        response = connection.getresponse()
        assert response.getheader("content-type") == "text/event-stream"
        assert response.getheader("content-length") is None
        body = response.read()
    finally:
        connection.close()

    assert body == b"data: event-0\n\ndata: event-1\n\ndata: event-2\n\n"


def test_unreachable_upstream_returns_anthropic_shaped_error(
    rig: tuple[HybridRouter, _StubUpstream, _StubUpstream],
) -> None:
    router, codex, _ = rig
    codex.shutdown()
    codex.server_close()

    status, body = post(router, "/v1/messages", {"model": "anthropic.gpt-5.6-sol"})

    assert status == 502
    assert json.loads(body)["error"]["type"] == "api_error"


def test_custom_anthropic_base_url_preserves_route_prefix_and_host() -> None:
    codex = _StubUpstream("codex")
    anthropic = _StubUpstream("subrouter-claude")
    router = HybridRouter(
        codex.port,
        anthropic_base_url=f"http://127.0.0.1:{anthropic.port}/r/routefixture1234567890",
    )
    router.start()
    try:
        status, body = post(router, "/v1/messages?beta=true", {"model": "claude-opus-5"})
        assert status == 200
        assert json.loads(body) == {"upstream": "subrouter-claude", "model": "claude-opus-5"}
        assert anthropic.seen[0]["path"] == "/r/routefixture1234567890/v1/messages?beta=true"
        assert anthropic.seen[0]["host"] == f"127.0.0.1:{anthropic.port}"
        assert anthropic.seen[0]["authorization"] == "Bearer sk-ant-oat01-test"
        assert codex.seen == []
    finally:
        router.stop()
        codex.shutdown()
        codex.server_close()
        anthropic.shutdown()
        anthropic.server_close()


def test_custom_anthropic_base_url_rejects_credentials() -> None:
    with pytest.raises(ValueError, match="invalid Anthropic upstream"):
        HybridRouter(12345, anthropic_base_url="http://user:pass@127.0.0.1:31415/r/route")
