"""Tests for Spec 0008 Phase 2 local checks and reports."""

from __future__ import annotations

import pytest

from agentbyte.eval import (
    CheckResult,
    EvalNotPassedError,
    EvalReport,
    EvalTask,
    EvalTrajectory,
    ExactMatchJudge,
    ExpectedToolCall,
    LocalEvaluator,
    evaluator,
    keyword_check,
    threshold_gate,
    tool_call_args_match,
    tool_call_args_match_check,
    tool_calls_present,
    tool_calls_present_check,
)
from agentbyte.messages import AssistantMessage, ToolCallRequest, ToolMessage, Usage, UserMessage


def _trajectory_with_tool_call() -> EvalTrajectory:
    task = EvalTask(
        name="weather",
        input="What's the weather in Paris?",
        expected_output="sunny",
        expected_tool_calls=[ExpectedToolCall(name="get_weather", arguments={"location": "Paris"})],
    )
    return EvalTrajectory(
        task=task,
        messages=[
            UserMessage(content=task.input, source="user"),
            AssistantMessage(
                content="I'll check.",
                source="assistant",
                tool_calls=[
                    ToolCallRequest(
                        tool_name="get_weather",
                        parameters={"location": "Paris", "unit": "celsius"},
                        call_id="call-1",
                    )
                ],
            ),
            AssistantMessage(content="It is sunny.", source="assistant"),
        ],
        success=True,
        usage=Usage(tokens_input=10, tokens_output=5),
    )


@pytest.mark.asyncio
async def test_tool_calls_present_uses_task_expected_tool_calls() -> None:
    result = await tool_calls_present(_trajectory_with_tool_call())

    assert result.passed is True


@pytest.mark.asyncio
async def test_tool_call_args_match_checks_assistant_tool_call_parameters() -> None:
    result = await tool_call_args_match(_trajectory_with_tool_call())

    assert result.passed is True


@pytest.mark.asyncio
async def test_tool_call_args_match_reports_mismatch() -> None:
    trajectory = _trajectory_with_tool_call().model_copy(
        update={
            "task": _trajectory_with_tool_call().task.model_copy(
                update={
                    "expected_tool_calls": [
                        ExpectedToolCall(
                            name="get_weather",
                            arguments={"location": "Berlin"},
                        )
                    ]
                }
            )
        }
    )

    result = await tool_call_args_match(trajectory)

    assert result.passed is False
    assert "Berlin" in result.reason


@pytest.mark.asyncio
async def test_explicit_tool_check_factories_work() -> None:
    trajectory = _trajectory_with_tool_call()

    present_result = await tool_calls_present_check("get_weather")(trajectory)
    args_result = await tool_call_args_match_check(
        [ExpectedToolCall(name="get_weather", arguments={"location": "Paris"})]
    )(trajectory)

    assert present_result.passed is True
    assert args_result.passed is True


@pytest.mark.asyncio
async def test_keyword_check_uses_extracted_response() -> None:
    result = await keyword_check("sunny")(_trajectory_with_tool_call())

    assert result.passed is True


@pytest.mark.asyncio
async def test_keyword_check_answer_strategy_is_honored() -> None:
    """answer_strategy controls which message keyword_check extracts from.

    Trajectory ends with a ToolMessage so that last_non_empty and last_assistant
    disagree — the keyword "sunny" lives in the AssistantMessage, not the tool result.
    """
    task = EvalTask(name="weather", input="Weather?")
    trajectory = EvalTrajectory(
        task=task,
        messages=[
            UserMessage(content="Weather?", source="user"),
            AssistantMessage(content="It is sunny today.", source="assistant"),
            ToolMessage(
                content="API result: 22C",
                source="tool",
                tool_call_id="call-1",
                tool_name="get_weather",
                success=True,
            ),
        ],
        success=True,
        usage=Usage(),
    )

    # default "last_assistant" — picks AssistantMessage → "sunny" found
    assert (await keyword_check("sunny")(trajectory)).passed is True

    # explicit "last_non_empty" — picks ToolMessage → "sunny" not found
    assert (await keyword_check("sunny", answer_strategy="last_non_empty")(trajectory)).passed is False


@pytest.mark.asyncio
async def test_evaluator_decorator_supports_sync_and_async_functions() -> None:
    @evaluator
    def contains_weather(query: str, response: str) -> bool:
        return "weather" in query.lower() and "sunny" in response.lower()

    @evaluator(name="score_check")
    async def score_check(response: str) -> float:
        return 0.9 if "sunny" in response.lower() else 0.1

    trajectory = _trajectory_with_tool_call()

    sync_result = await contains_weather(trajectory)
    async_result = await score_check(trajectory)

    assert sync_result.passed is True
    assert async_result.passed is True
    assert async_result.score == pytest.approx(0.9)


def test_evaluator_rejects_unknown_required_parameters_at_decoration_time() -> None:
    with pytest.raises(TypeError):

        @evaluator
        def bad_check(not_supported: str) -> bool:
            return True


@pytest.mark.asyncio
async def test_local_evaluator_builds_report_and_avg_score_from_threshold_gate() -> None:
    trajectory = EvalTrajectory(
        task=EvalTask(name="capital", input="Capital?", expected_output="Paris"),
        messages=[
            UserMessage(content="Capital?", source="user"),
            AssistantMessage(content="Paris", source="assistant"),
        ],
        success=True,
        usage=Usage(),
    )
    report = await LocalEvaluator(
        threshold_gate(ExactMatchJudge(), threshold=7.0),
        keyword_check("Paris"),
    ).evaluate([trajectory])

    assert report.passed == 1
    assert report.failed == 0
    assert report.total == 1
    assert report.all_passed is True
    assert report.avg_score == 10.0


def _orchestrator_trajectory() -> EvalTrajectory:
    """Flat message list from a Researcher→Writer→Reviewer orchestrator run."""
    task = EvalTask(name="draft", input="Write about AI", expected_output="draft")
    return EvalTrajectory(
        task=task,
        messages=[
            UserMessage(content="Write about AI", source="user"),
            AssistantMessage(content="Here are the facts about AI.", source="researcher"),
            AssistantMessage(content="Here is the draft: AI is transforming the world.", source="writer"),
            AssistantMessage(content="The draft is approved. No changes needed.", source="reviewer"),
        ],
        success=True,
        usage=Usage(),
    )


@pytest.mark.asyncio
async def test_source_filter_extracts_from_named_agent_only() -> None:
    """source_filter restricts extraction to the specified agent's messages."""
    judge = ExactMatchJudge(source_filter="writer")
    answer = judge.extract_answer(_orchestrator_trajectory())

    assert "draft" in answer.lower()
    assert "approved" not in answer.lower()


@pytest.mark.asyncio
async def test_source_filter_returns_empty_for_unknown_source() -> None:
    """source_filter with a name that matches no message returns an empty string."""
    judge = ExactMatchJudge(source_filter="nonexistent-agent")
    answer = judge.extract_answer(_orchestrator_trajectory())

    assert answer == ""


@pytest.mark.asyncio
async def test_keyword_check_with_source_filter_passes_on_target_agent() -> None:
    """keyword_check(source_filter=...) passes when the keyword is in the target agent's message."""
    result = await keyword_check("draft", source_filter="writer")(_orchestrator_trajectory())

    assert result.passed is True


@pytest.mark.asyncio
async def test_keyword_check_with_source_filter_fails_when_keyword_only_in_other_agent() -> None:
    """keyword_check(source_filter=...) fails when the keyword exists only in a different agent's message."""
    result = await keyword_check("approved", source_filter="writer")(_orchestrator_trajectory())

    assert result.passed is False
    assert "approved" in result.reason


@pytest.mark.asyncio
async def test_evaluator_source_filter_injects_target_agent_response() -> None:
    """evaluator(source_filter=...) injects only the target agent's last response."""

    @evaluator(source_filter="writer")
    def check_writer(response: str) -> bool:
        return "draft" in response.lower() and "approved" not in response.lower()

    result = await check_writer(_orchestrator_trajectory())

    assert result.passed is True


def test_eval_report_raise_for_status_raises_on_failures() -> None:
    report = EvalReport(
        items=[
            {
                "task_name": "weather",
                "passed": False,
                "check_results": [
                    CheckResult(
                        passed=False,
                        reason="missing keyword",
                        check_name="keyword_check",
                    )
                ],
            }
        ]
    )

    with pytest.raises(EvalNotPassedError):
        report.raise_for_status()
