"""Tests for ToolParameterMetric."""

from __future__ import annotations

import asyncio

import pytest

from evalkit.metrics.tool_parameter import ToolParameterMetric
from evalkit.models import MetricScore, ToolCall
from evalkit.models import TestCase as EvalTestCase


def _make_agent_tc(
    expected: list[ToolCall],
    actual: list[ToolCall],
) -> EvalTestCase:
    return EvalTestCase(
        question="agent task",
        expected_tool_calls=expected,
        actual_tool_calls=actual,
    )


class TestToolParameterMetric:
    def test_name(self) -> None:
        assert ToolParameterMetric.name == "tool_parameter"

    def test_perfect_match(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={"q": "Paris"})],
            [ToolCall(name="search", parameters={"q": "Paris"})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(1.0)
        assert mock_judge.call_count == 0

    def test_extra_actual_param(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={"q": "Paris"})],
            [ToolCall(name="search", parameters={"q": "Paris", "lang": "en"})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.5)

    def test_missing_actual_param(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={"q": "Paris", "lang": "en"})],
            [ToolCall(name="search", parameters={"q": "Paris"})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.5)

    def test_wrong_value(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={"q": "Paris"})],
            [ToolCall(name="search", parameters={"q": "London"})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.0)

    def test_both_empty_params(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={})],
            [ToolCall(name="search", parameters={})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(1.0)

    def test_no_matching_tool_names(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={"q": "Paris"})],
            [ToolCall(name="cancel", parameters={"q": "Paris"})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.0)

    def test_mixed_matching_and_mismatched_names(self, mock_judge) -> None:
        # search pair: names match, params match → 1.0
        # second pair: names differ → skipped (not counted)
        # average = 1.0 / 1 matched pair = 1.0
        tc = _make_agent_tc(
            [
                ToolCall(name="search", parameters={"q": "Paris"}),
                ToolCall(name="book", parameters={"city": "Paris"}),
            ],
            [
                ToolCall(name="search", parameters={"q": "Paris"}),
                ToolCall(name="cancel", parameters={"id": "123"}),
            ],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(1.0)

    def test_multiple_tools_averaged(self, mock_judge) -> None:
        # search pair: perfect match → 1.0
        # book pair: city matches, date differs → 1/2 = 0.5
        # average = (1.0 + 0.5) / 2 = 0.75
        tc = _make_agent_tc(
            [
                ToolCall(name="search", parameters={"q": "Paris"}),
                ToolCall(name="book", parameters={"city": "Paris", "date": "04-01"}),
            ],
            [
                ToolCall(name="search", parameters={"q": "Paris"}),
                ToolCall(name="book", parameters={"city": "Paris", "date": "05-01"}),
            ],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.75)

    def test_non_string_values(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="calc", parameters={"x": 42, "flag": True})],
            [ToolCall(name="calc", parameters={"x": 42, "flag": True})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(1.0)

    def test_skip_when_expected_is_none(self, mock_judge) -> None:
        tc = EvalTestCase(
            question="agent task",
            expected_tool_calls=None,
            actual_tool_calls=[ToolCall(name="search", parameters={"q": "Paris"})],
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.0)

    def test_skip_when_actual_is_none(self, mock_judge) -> None:
        tc = EvalTestCase(
            question="agent task",
            expected_tool_calls=[ToolCall(name="search", parameters={"q": "Paris"})],
            actual_tool_calls=None,
        )
        metric = ToolParameterMetric()
        result = metric.score(tc, mock_judge)
        assert result.score == pytest.approx(0.0)

    def test_async_score(self, mock_judge) -> None:
        tc = _make_agent_tc(
            [ToolCall(name="search", parameters={"q": "Paris"})],
            [ToolCall(name="search", parameters={"q": "Paris"})],
        )
        metric = ToolParameterMetric()
        result = asyncio.run(metric.score_async(tc, mock_judge))
        assert isinstance(result, MetricScore)
        assert result.score == pytest.approx(1.0)
        assert mock_judge.call_count == 0
