import json
import subprocess
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.runner import ClaudeHeadlessRunner, CodexExecRunner
from home_agent.utils.subprocess_logging import LoggedSubprocessError


@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 test_claude_runner_uses_structured_output(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    """Claude CLI with --json-schema returns structured_output field."""
    payload = {
        "structured_output": {"score": 0.95, "label": "critical", "reasons": ["deadline today"]},
        "result": "",
        "total_cost_usd": 0.13,
        "model": "claude-opus",
        "input_tokens": 200,
        "output_tokens": 50,
    }

    def fake_run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        _ = args, kwargs
        return subprocess.CompletedProcess(
            args=[],
            returncode=0,
            stdout=json.dumps(payload),
            stderr="",
        )

    monkeypatch.setattr(subprocess, "run", fake_run)
    result = ClaudeHeadlessRunner().classify(sample_item)

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


def test_claude_runner_raises_without_structured_output(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    """Claude runner raises if structured_output is missing."""
    payload = {"result": "some text", "total_cost_usd": 0.05}

    def fake_run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        _ = args, kwargs
        return subprocess.CompletedProcess(
            args=[], returncode=0, stdout=json.dumps(payload), stderr=""
        )

    monkeypatch.setattr(subprocess, "run", fake_run)

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


def test_claude_runner_parses_usage(monkeypatch: pytest.MonkeyPatch, sample_item: Item) -> None:
    payload = {
        "structured_output": {"score": 0.8, "label": "high", "reasons": ["deadline"]},
        "total_cost_usd": 0.12,
        "model": "claude-sonnet",
        "input_tokens": 100,
        "output_tokens": 50,
    }

    def fake_run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        _ = args, kwargs
        return subprocess.CompletedProcess(
            args=[],
            returncode=0,
            stdout=json.dumps(payload),
            stderr="",
        )

    monkeypatch.setattr(subprocess, "run", fake_run)
    result = ClaudeHeadlessRunner().classify(sample_item)

    assert result.decision.label == UrgencyLabel.HIGH
    assert result.usage.total_cost_usd == 0.12
    assert result.usage.input_tokens == 100


def test_codex_runner_parses_jsonl(monkeypatch: pytest.MonkeyPatch, sample_item: Item) -> None:
    lines = "\n".join(
        [
            json.dumps({"type": "status", "message": "working"}),
            json.dumps(
                {
                    "type": "final",
                    "result": '{"score":0.9,"label":"critical","reasons":["test"]}',
                    "total_cost_usd": 0.05,
                }
            ),
        ]
    )

    def fake_run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        _ = args, kwargs
        return subprocess.CompletedProcess(args=[], returncode=0, stdout=lines, stderr="")

    monkeypatch.setattr(subprocess, "run", fake_run)
    result = CodexExecRunner().classify(sample_item)

    assert result.decision.label == UrgencyLabel.CRITICAL
    assert result.usage.total_cost_usd == 0.05


def test_claude_runner_includes_retrieved_context_in_prompt(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    captured: dict[str, list[str]] = {}

    def fake_run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        _ = kwargs
        command = args[0]
        if isinstance(command, list):
            captured["command"] = command
        payload = {
            "structured_output": {"score": 0.6, "label": "medium", "reasons": ["context"]},
            "total_cost_usd": 0.01,
        }
        return subprocess.CompletedProcess(
            args=[],
            returncode=0,
            stdout=json.dumps(payload),
            stderr="",
        )

    monkeypatch.setattr(subprocess, "run", fake_run)
    runner = ClaudeHeadlessRunner()
    runner.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,
            )
        ],
    )

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


def test_codex_runner_surfaces_logged_subprocess_output_on_failure(
    monkeypatch: pytest.MonkeyPatch, sample_item: Item
) -> None:
    def fake_run_logged_subprocess(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        _ = args, kwargs
        raise LoggedSubprocessError(
            command=["codex", "exec"],
            returncode=1,
            stdout="codex stdout failure detail",
            stderr="",
        )

    monkeypatch.setattr("home_agent.llm.runner.run_logged_subprocess", fake_run_logged_subprocess)

    with pytest.raises(RuntimeError, match="codex stdout failure detail"):
        CodexExecRunner().classify(sample_item)


def test_claude_runner_surfaces_timeout_message_and_uses_configured_timeout(
    monkeypatch: pytest.MonkeyPatch,
    sample_item: Item,
) -> None:
    captured: dict[str, Any] = {}

    def fake_run_logged_subprocess(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]:
        captured["timeout"] = kwargs["timeout"]
        raise LoggedSubprocessError(
            command=["claude", "-p"],
            returncode=-1,
            stdout="partial out",
            stderr="",
            timeout_seconds=120,
        )

    monkeypatch.setattr("home_agent.llm.runner.run_logged_subprocess", fake_run_logged_subprocess)

    with pytest.raises(RuntimeError, match="timed out after 120"):
        ClaudeHeadlessRunner(timeout_seconds=120).classify(sample_item)

    assert captured["timeout"] == 120
