from datetime import UTC, datetime
from typing import Any

import pytest

from home_agent.contracts.types import Item, ItemKind, RetrievedMatch, UrgencyLabel
from home_agent.llm.sdk_runner import ClaudeSDKRunner


@pytest.fixture
def sample_item() -> Item:
    return Item(
        provider="gmail",
        external_id="1",
        kind=ItemKind.EMAIL,
        received_at=datetime.now(tz=UTC),
        subject="Urgent deadline",
        sender="dean@university.edu",
        metadata={},
    )


def _fake_result_message(
    structured_output: dict[str, Any],
    total_cost_usd: float = 0.05,
    usage: dict[str, Any] | None = None,
) -> Any:
    """Build a mock ResultMessage dataclass instance."""
    from claude_agent_sdk import ResultMessage

    return ResultMessage(
        subtype="result",
        duration_ms=1000,
        duration_api_ms=800,
        is_error=False,
        num_turns=1,
        session_id="test-session",
        total_cost_usd=total_cost_usd,
        usage=usage or {"input_tokens": 100, "output_tokens": 50},
        structured_output=structured_output,
    )


async def _async_iter_messages(*messages: Any):  # type: ignore[no-untyped-def]
    for msg in messages:
        yield msg


def test_sdk_runner_classify(monkeypatch: pytest.MonkeyPatch, sample_item: Item) -> None:
    result_msg = _fake_result_message(
        {"score": 0.95, "label": "critical", "reasons": ["deadline today"]}
    )

    async def fake_query(*, prompt: str, options: Any, **kw: Any):  # type: ignore[no-untyped-def]
        async for m in _async_iter_messages(result_msg):
            yield m

    monkeypatch.setattr("home_agent.llm.sdk_runner.query", fake_query)
    result = ClaudeSDKRunner().classify(sample_item)

    assert result.decision.label == UrgencyLabel.CRITICAL
    assert result.decision.score == 0.95
    assert "deadline today" in result.decision.reasons


def test_sdk_runner_parses_usage(monkeypatch: pytest.MonkeyPatch, sample_item: Item) -> None:
    result_msg = _fake_result_message(
        {"score": 0.8, "label": "high", "reasons": ["deadline"]},
        total_cost_usd=0.12,
        usage={"input_tokens": 200, "output_tokens": 75},
    )

    async def fake_query(*, prompt: str, options: Any, **kw: Any):  # type: ignore[no-untyped-def]
        async for m in _async_iter_messages(result_msg):
            yield m

    monkeypatch.setattr("home_agent.llm.sdk_runner.query", fake_query)
    result = ClaudeSDKRunner().classify(sample_item)

    assert result.usage.provider == "claude-sdk"
    assert result.usage.total_cost_usd == 0.12
    assert result.usage.input_tokens == 200
    assert result.usage.output_tokens == 75


def test_sdk_runner_raises_without_structured_output(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    result_msg = _fake_result_message({})
    # Override structured_output to None
    object.__setattr__(result_msg, "structured_output", None)

    async def fake_query(*, prompt: str, options: Any, **kw: Any):  # type: ignore[no-untyped-def]
        async for m in _async_iter_messages(result_msg):
            yield m

    monkeypatch.setattr("home_agent.llm.sdk_runner.query", fake_query)

    with pytest.raises(RuntimeError, match="did not return structured_output"):
        ClaudeSDKRunner().classify(sample_item)


def test_sdk_runner_raises_on_no_result_message(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    async def fake_query(*, prompt: str, options: Any, **kw: Any):  # type: ignore[no-untyped-def]
        return
        yield  # make it an async generator  # pyright: ignore[reportUnreachable]

    monkeypatch.setattr("home_agent.llm.sdk_runner.query", fake_query)

    with pytest.raises(RuntimeError, match="returned no ResultMessage"):
        ClaudeSDKRunner().classify(sample_item)


def test_sdk_runner_includes_retrieved_context_in_prompt(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    captured: dict[str, str] = {}
    result_msg = _fake_result_message({"score": 0.6, "label": "medium", "reasons": ["context"]})

    async def fake_query(*, prompt: str, options: Any, **kw: Any):  # type: ignore[no-untyped-def]
        captured["prompt"] = prompt
        async for m in _async_iter_messages(result_msg):
            yield m

    monkeypatch.setattr("home_agent.llm.sdk_runner.query", fake_query)
    ClaudeSDKRunner().classify(
        sample_item,
        retrieved_matches=[
            RetrievedMatch(
                provider="gmail",
                external_id="old-1",
                kind=ItemKind.EMAIL,
                summary_text="Previous urgent assignment email",
                received_at=datetime.now(tz=UTC),
                similarity=0.91,
                urgency_label=UrgencyLabel.HIGH,
            )
        ],
    )

    assert "Similar prior items:" in captured["prompt"]
    assert "Previous urgent assignment email" in captured["prompt"]


def test_sdk_runner_raises_on_process_error(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    from claude_agent_sdk import ProcessError

    async def fake_query(*, prompt: str, options: Any, **kw: Any):  # type: ignore[no-untyped-def]
        err_msg = "sdk error"
        raise ProcessError(err_msg, exit_code=1, stderr=err_msg)
        yield  # makes this an async generator  # pyright: ignore[reportUnreachable]

    monkeypatch.setattr("home_agent.llm.sdk_runner.query", fake_query)

    with pytest.raises(RuntimeError, match="claude-sdk call failed"):
        ClaudeSDKRunner().classify(sample_item)
