"""
Tests for execution_efficiency.py — wasteful pattern detection and scoring.
"""

import pytest

from api.services.complete.execution_efficiency import (
    EfficiencyReport,
    compute_efficiency_score,
    build_efficiency_hint,
    detect_duplicate_reads,
    detect_duplicate_searches,
    detect_edit_redundancy,
    detect_diff_size_mismatch,
    detect_serial_tool_patterns,
    detect_round_trip_inflation,
    _extract_tool_calls,
    _count_assistant_turns,
)


def _make_msg(role, tool_calls=None):
    class FakeMsg:
        pass
    m = FakeMsg()
    m.role = role
    m.tool_calls = tool_calls
    m.content = None
    return m


def _tc(name, args=None):
    return {"function": {"name": name, "arguments": args or {}}}


# ===========================================================================
# Individual detectors
# ===========================================================================

class TestDuplicateReads:
    def test_no_duplicates(self):
        calls = [
            {"name": "read", "args": {"path": "a.py"}, "raw": {}},
            {"name": "read", "args": {"path": "b.py"}, "raw": {}},
        ]
        count, findings = detect_duplicate_reads(calls)
        assert count == 0
        assert findings == []

    def test_duplicate_detected(self):
        calls = [
            {"name": "read", "args": {"path": "a.py"}, "raw": {}},
            {"name": "read", "args": {"path": "a.py"}, "raw": {}},
            {"name": "read", "args": {"path": "a.py"}, "raw": {}},
        ]
        count, findings = detect_duplicate_reads(calls)
        assert count == 2
        assert len(findings) == 1
        assert "a.py" in findings[0]

    def test_non_read_tools_ignored(self):
        calls = [
            {"name": "grep", "args": {"path": "a.py"}, "raw": {}},
            {"name": "grep", "args": {"path": "a.py"}, "raw": {}},
        ]
        count, findings = detect_duplicate_reads(calls)
        assert count == 0


class TestDuplicateSearches:
    def test_no_duplicates(self):
        calls = [
            {"name": "grep", "args": {"pattern": "foo"}, "raw": {}},
            {"name": "grep", "args": {"pattern": "bar"}, "raw": {}},
        ]
        count, findings = detect_duplicate_searches(calls)
        assert count == 0

    def test_duplicate_grep(self):
        calls = [
            {"name": "grep", "args": {"pattern": "foo"}, "raw": {}},
            {"name": "grep", "args": {"pattern": "foo"}, "raw": {}},
        ]
        count, findings = detect_duplicate_searches(calls)
        assert count == 1
        assert "foo" in findings[0]

    def test_case_insensitive_match(self):
        calls = [
            {"name": "grep", "args": {"pattern": "FOO"}, "raw": {}},
            {"name": "grep", "args": {"pattern": "foo"}, "raw": {}},
        ]
        count, findings = detect_duplicate_searches(calls)
        assert count == 1


class TestEditRedundancy:
    def test_no_churn(self):
        calls = [
            {"name": "strreplace", "args": {"path": "a.py"}, "raw": {}},
            {"name": "strreplace", "args": {"path": "b.py"}, "raw": {}},
        ]
        count, findings = detect_edit_redundancy(calls)
        assert count == 0

    def test_churn_detected(self):
        calls = [
            {"name": "strreplace", "args": {"path": "a.py"}, "raw": {}},
        ] * 5
        count, findings = detect_edit_redundancy(calls)
        assert count == 2  # 5 - 3 threshold
        assert len(findings) == 1
        assert "a.py" in findings[0]


class TestDiffSizeMismatch:
    def test_simple_task_large_diff(self):
        calls = [
            {"name": "write", "args": {"contents": "x" * 4000}, "raw": {}},
        ]
        triggered, findings = detect_diff_size_mismatch(calls, is_simple=True)
        assert triggered is True
        assert len(findings) == 1

    def test_simple_task_small_diff_ok(self):
        calls = [
            {"name": "write", "args": {"contents": "x = 1"}, "raw": {}},
        ]
        triggered, findings = detect_diff_size_mismatch(calls, is_simple=True)
        assert triggered is False

    def test_complex_task_not_checked(self):
        calls = [
            {"name": "write", "args": {"contents": "x" * 10000}, "raw": {}},
        ]
        triggered, findings = detect_diff_size_mismatch(calls, is_simple=False)
        assert triggered is False

    def test_too_many_edits(self):
        calls = [
            {"name": "strreplace", "args": {"new_string": "x"}, "raw": {}},
        ] * 6
        triggered, findings = detect_diff_size_mismatch(calls, is_simple=True)
        assert triggered is True


class TestRoundTripInflation:
    def test_simple_task_within_budget(self):
        triggered, findings = detect_round_trip_inflation(3, is_simple=True, tool_call_count=2)
        assert triggered is False

    def test_simple_task_over_budget(self):
        triggered, findings = detect_round_trip_inflation(6, is_simple=True, tool_call_count=2)
        assert triggered is True
        assert "simple" in findings[0].lower()

    def test_complex_task_high_budget(self):
        triggered, findings = detect_round_trip_inflation(18, is_simple=False, tool_call_count=20)
        assert triggered is False

    def test_complex_task_over_budget(self):
        triggered, findings = detect_round_trip_inflation(25, is_simple=False, tool_call_count=20)
        assert triggered is True


class TestSerialToolPatterns:
    def test_no_serial_pattern(self):
        msgs = [
            _make_msg("assistant", [_tc("read"), _tc("grep")]),
            _make_msg("assistant", [_tc("read"), _tc("grep")]),
        ]
        count, findings = detect_serial_tool_patterns(msgs)
        assert count == 0

    def test_serial_reads_detected(self):
        msgs = [
            _make_msg("assistant", [_tc("read", {"path": "a.py"})]),
            _make_msg("assistant", [_tc("read", {"path": "b.py"})]),
            _make_msg("assistant", [_tc("read", {"path": "c.py"})]),
        ]
        count, findings = detect_serial_tool_patterns(msgs, min_streak=3)
        assert count >= 1


# ===========================================================================
# Aggregate scorer
# ===========================================================================

class TestComputeEfficiencyScore:
    def test_perfect_session(self):
        msgs = [
            _make_msg("user"),
            _make_msg("assistant", [_tc("read", {"path": "a.py"}), _tc("grep", {"pattern": "foo"})]),
            _make_msg("assistant", [_tc("strreplace", {"path": "a.py", "new_string": "bar"})]),
        ]
        report = compute_efficiency_score(msgs, is_simple=False)
        assert isinstance(report, EfficiencyReport)
        assert report.score >= 0.8
        assert report.findings == []

    def test_wasteful_session_penalized(self):
        msgs = [_make_msg("user")]
        for i in range(8):
            msgs.append(_make_msg("assistant", [_tc("read", {"path": "a.py"})]))
        report = compute_efficiency_score(msgs, is_simple=True)
        assert report.score < 0.8
        assert len(report.findings) > 0

    def test_score_never_negative(self):
        msgs = [_make_msg("user")]
        for i in range(30):
            msgs.append(_make_msg("assistant", [_tc("read", {"path": "a.py"})]))
        report = compute_efficiency_score(msgs, is_simple=True)
        assert report.score >= 0.0


class TestBuildEfficiencyHint:
    def test_efficient_session_no_hint(self):
        report = EfficiencyReport(score=0.9, turn_count=3, tool_call_count=5, findings=[], metrics={})
        assert build_efficiency_hint(report) is None

    def test_inefficient_session_has_hint(self):
        report = EfficiencyReport(
            score=0.5, turn_count=10, tool_call_count=15,
            findings=["REDUNDANT_READ: a.py was Read 5 times."],
            metrics={},
        )
        hint = build_efficiency_hint(report)
        assert hint is not None
        assert "EFFICIENCY" in hint
        assert "REDUNDANT_READ" in hint


# ===========================================================================
# Helper extraction
# ===========================================================================

class TestHelpers:
    def test_extract_tool_calls(self):
        msgs = [
            _make_msg("user"),
            _make_msg("assistant", [_tc("read", {"path": "a.py"})]),
            _make_msg("assistant", [_tc("write", {"path": "b.py"})]),
        ]
        calls = _extract_tool_calls(msgs)
        assert len(calls) == 2
        assert calls[0]["name"] == "read"
        assert calls[1]["name"] == "write"

    def test_count_assistant_turns(self):
        msgs = [
            _make_msg("user"),
            _make_msg("assistant"),
            _make_msg("user"),
            _make_msg("assistant"),
        ]
        assert _count_assistant_turns(msgs) == 2

    def test_empty_messages(self):
        assert _extract_tool_calls([]) == []
        assert _count_assistant_turns([]) == 0
