from __future__ import annotations

import threading
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

import wzrd.router as router_module
from wzrd.client import PickResult
from wzrd.router import WZRDRouter


# ---------------------------------------------------------------------------
# Fakes
# ---------------------------------------------------------------------------


class FakeCompletions:
    def __init__(self) -> None:
        self.calls: list[dict] = []

    def create(self, **kwargs):
        self.calls.append(kwargs)
        return SimpleNamespace(**kwargs)


class FakeChat:
    def __init__(self) -> None:
        self.completions = FakeCompletions()


class FakeClient:
    def __init__(self) -> None:
        self.chat = FakeChat()


class NoChatClient:
    pass


def _make_pick_result(model: str = "Qwen/Qwen3.5-35B-A3B", task: str = "code") -> PickResult:
    return PickResult(
        model=model,
        signal_model=model,
        task=task,
        policy="strict",
        score=0.72,
        raw_score=0.61,
        trend="accelerating",
        confidence="normal",
        action="candidate",
        platform="github",
        reason="strict feed signal from github",
    )


# ---------------------------------------------------------------------------
# Existing behaviour tests (updated to use pick_details)
# ---------------------------------------------------------------------------


def test_explicit_model_passes_through(monkeypatch):
    def fail_pick_details(*args, **kwargs):  # pragma: no cover
        raise AssertionError("pick_details() should not be called for explicit model names")

    monkeypatch.setattr(router_module, "pick_details", fail_pick_details)

    client = WZRDRouter(FakeClient(), task="code")
    result = client.chat.completions.create(
        model="anthropic/claude-sonnet-4.6",
        messages=[{"role": "user", "content": "hello"}],
    )

    assert result.model == "anthropic/claude-sonnet-4.6"


def test_task_sentinel_routes_and_forwards_candidates(monkeypatch):
    calls: list[tuple[str, list[str] | None, str | None]] = []

    def fake_pick_details(task, candidates=None, fallback=None):
        calls.append((task, candidates, fallback))
        return _make_pick_result("Qwen/Qwen3.5-35B-A3B", task)

    monkeypatch.setattr(router_module, "pick_details", fake_pick_details)

    client = WZRDRouter(
        FakeClient(),
        task="general",
        candidates=["openrouter/qwen/qwen3.5-9b", "openrouter/qwen/qwen3.5-35b-a3b"],
    )
    result = client.chat.completions.create(
        model="code",
        messages=[{"role": "user", "content": "route this"}],
    )

    assert result.model == "Qwen/Qwen3.5-35B-A3B"
    assert calls == [
        ("code", ["openrouter/qwen/qwen3.5-9b", "openrouter/qwen/qwen3.5-35b-a3b"], None)
    ]


def test_missing_model_routes_using_default_task(monkeypatch):
    calls: list[tuple[str, list[str] | None, str | None]] = []

    def fake_pick_details(task, candidates=None, fallback=None):
        calls.append((task, candidates, fallback))
        return _make_pick_result("Qwen/Qwen3.5-9B", task)

    monkeypatch.setattr(router_module, "pick_details", fake_pick_details)

    client = WZRDRouter(FakeClient(), task="chat")
    result = client.chat.completions.create(messages=[{"role": "user", "content": "hello"}])

    assert result.model == "Qwen/Qwen3.5-9B"
    assert calls == [("chat", None, None)]


def test_unsupported_client_errors():
    client = WZRDRouter(NoChatClient())

    with pytest.raises(TypeError, match="client\\.chat\\.completions\\.create"):
        _ = client.chat


# ---------------------------------------------------------------------------
# Auto-report tests
# ---------------------------------------------------------------------------


def test_report_false_does_not_call_report_pick(monkeypatch):
    """Default report=False should never call report_pick."""
    agent = MagicMock()

    def fake_pick_details(task, candidates=None, fallback=None):
        return _make_pick_result("Qwen/Qwen3.5-9B", task)

    monkeypatch.setattr(router_module, "pick_details", fake_pick_details)

    client = WZRDRouter(FakeClient(), task="code", report=False, agent=agent)
    result = client.chat.completions.create(
        model="code",
        messages=[{"role": "user", "content": "hello"}],
    )

    assert result.model == "Qwen/Qwen3.5-9B"
    agent.report_pick.assert_not_called()


def test_report_true_calls_report_pick_after_create(monkeypatch):
    """report=True with a mock agent should call report_pick in a background thread."""
    agent = MagicMock()
    reported_choices: list[PickResult] = []

    # Capture what report_pick receives
    def capture_report(choice):
        reported_choices.append(choice)

    agent.report_pick.side_effect = capture_report

    pick_result = _make_pick_result("Qwen/Qwen3.5-35B-A3B", "code")

    def fake_pick_details(task, candidates=None, fallback=None):
        return pick_result

    monkeypatch.setattr(router_module, "pick_details", fake_pick_details)

    client = WZRDRouter(FakeClient(), task="code", report=True, agent=agent)
    result = client.chat.completions.create(
        model="code",
        messages=[{"role": "user", "content": "hello"}],
    )

    # Wait briefly for the background thread
    for t in threading.enumerate():
        if t.daemon and t.is_alive():
            t.join(timeout=2.0)

    assert result.model == "Qwen/Qwen3.5-35B-A3B"
    agent.report_pick.assert_called_once()
    assert len(reported_choices) == 1
    assert reported_choices[0].model == "Qwen/Qwen3.5-35B-A3B"
    assert reported_choices[0].task == "code"
    assert reported_choices[0].policy == "strict"


def test_report_failure_does_not_crash_create(monkeypatch):
    """If report_pick raises, create() should still return normally."""
    agent = MagicMock()
    agent.report_pick.side_effect = RuntimeError("network down")

    def fake_pick_details(task, candidates=None, fallback=None):
        return _make_pick_result("Qwen/Qwen3.5-9B", task)

    monkeypatch.setattr(router_module, "pick_details", fake_pick_details)

    client = WZRDRouter(FakeClient(), task="code", report=True, agent=agent)
    result = client.chat.completions.create(
        model="code",
        messages=[{"role": "user", "content": "hello"}],
    )

    # Wait for background thread to finish
    for t in threading.enumerate():
        if t.daemon and t.is_alive():
            t.join(timeout=2.0)

    # create() succeeded despite report failure
    assert result.model == "Qwen/Qwen3.5-9B"
    agent.report_pick.assert_called_once()


def test_report_not_fired_for_explicit_model(monkeypatch):
    """Explicit model names bypass routing so no report should be sent."""
    agent = MagicMock()

    def fail_pick_details(*args, **kwargs):  # pragma: no cover
        raise AssertionError("pick_details() should not be called for explicit models")

    monkeypatch.setattr(router_module, "pick_details", fail_pick_details)

    client = WZRDRouter(FakeClient(), task="code", report=True, agent=agent)
    result = client.chat.completions.create(
        model="anthropic/claude-sonnet-4.6",
        messages=[{"role": "user", "content": "hello"}],
    )

    assert result.model == "anthropic/claude-sonnet-4.6"
    agent.report_pick.assert_not_called()


def test_from_env_report_enabled(monkeypatch):
    """from_env reads WZRD_REPORT and WZRD_AGENT_KEYPAIR_PATH."""
    monkeypatch.setenv("WZRD_REPORT", "true")
    monkeypatch.setenv("WZRD_AGENT_KEYPAIR_PATH", "/fake/keypair.json")

    # Stub _make_agent so it doesn't actually do network calls
    mock_agent = MagicMock()
    monkeypatch.setattr(WZRDRouter, "_make_agent", staticmethod(lambda kp: mock_agent))

    client = WZRDRouter.from_env(FakeClient(), task="code")

    assert client._report is True
    assert client._agent is mock_agent


def test_from_env_report_disabled_by_default(monkeypatch):
    """from_env with no WZRD_REPORT should not enable reporting."""
    monkeypatch.delenv("WZRD_REPORT", raising=False)
    monkeypatch.delenv("WZRD_AGENT_KEYPAIR_PATH", raising=False)
    monkeypatch.delenv("WZRD_AGENT_KEYPAIR", raising=False)

    client = WZRDRouter.from_env(FakeClient())

    assert client._report is False
    assert client._agent is None


def test_keypair_creates_and_authenticates_agent(monkeypatch):
    """Passing keypair= with report=True should auto-create and auth an agent."""
    mock_agent = MagicMock()
    monkeypatch.setattr(WZRDRouter, "_make_agent", staticmethod(lambda kp: mock_agent))

    client = WZRDRouter(FakeClient(), report=True, keypair="/fake/path.json")

    assert client._report is True
    assert client._agent is mock_agent


def test_report_true_without_agent_or_keypair_does_not_crash(monkeypatch):
    """report=True but no agent/keypair should not raise -- just no reports."""
    def fake_pick_details(task, candidates=None, fallback=None):
        return _make_pick_result("Qwen/Qwen3.5-9B", task)

    monkeypatch.setattr(router_module, "pick_details", fake_pick_details)

    client = WZRDRouter(FakeClient(), task="code", report=True)
    result = client.chat.completions.create(
        model="code",
        messages=[{"role": "user", "content": "hello"}],
    )

    # No agent, so no report -- but create() works fine
    assert result.model == "Qwen/Qwen3.5-9B"
