"""Tests for the check plugin framework and built-in checks."""

from unittest.mock import patch

import pluggy
from pydantic import BaseModel

from noisegate.builtins import BuiltinChecks
from noisegate.checks import (
    _HANDLES_ATTR,
    CheckPlugin,
    create_plugin_manager,
    handles,
)
from noisegate.models import Check, CheckResult, Program, Report


class _MockOutput(BaseModel):
    passed: bool
    reason: str


def _check(id: str) -> Check:
    return Check(id=id)


def _report(text: str = "Some report text.") -> Report:
    return Report(raw_text=text, source_path="test.md", char_count=len(text))


def _program() -> Program:
    return Program(name="Test", platform="HackerOne", source="https://example.com")


def _mock_llm_return(passed: bool = True, reason: str = "ok") -> tuple[BaseModel, int, int]:
    return _MockOutput(passed=passed, reason=reason), 10, 5


# ---------------------------------------------------------------------------
# @handles decorator
# ---------------------------------------------------------------------------

def test_handles_sets_metadata_on_function():
    @handles("my.check")
    def fn(self, check, report, program):
        pass

    meta = getattr(fn, _HANDLES_ATTR)
    assert meta["check_id"] == "my.check"


# ---------------------------------------------------------------------------
# CheckPlugin dispatch
# ---------------------------------------------------------------------------

def test_check_plugin_dispatches_by_check_id():
    class MyPlugin(CheckPlugin):
        @handles("my.check")
        def _do_check(self, check, report, program):
            return CheckResult(check_id="my.check", passed=True, reason="ok")

    plugin = MyPlugin()
    result = plugin.run_check(check=_check("my.check"), report=_report(), program=_program())
    assert result is not None
    assert result.passed


def test_check_plugin_returns_none_for_unhandled_id():
    class MyPlugin(CheckPlugin):
        @handles("my.check")
        def _do_check(self, check, report, program):
            return CheckResult(check_id="my.check", passed=True, reason="ok")

    plugin = MyPlugin()
    result = plugin.run_check(check=_check("other.check"), report=_report(), program=_program())
    assert result is None


# ---------------------------------------------------------------------------
# BuiltinChecks — calls generic LLM function
# ---------------------------------------------------------------------------

def test_builtin_check_calls_llm_and_returns_result():
    with patch(
        "noisegate.builtins.run_structured_llm_call", return_value=_mock_llm_return()
    ) as mock_fn:
        plugin = BuiltinChecks()
        result = plugin.run_check(
            check=_check("asset_in_scope"), report=_report(), program=_program()
        )
    assert result is not None
    assert result.check_id == "asset_in_scope"
    assert result.passed
    mock_fn.assert_called_once()


def test_builtin_unknown_check_id_returns_none():
    plugin = BuiltinChecks()
    result = plugin.run_check(
        check=_check("my_org.custom"), report=_report(), program=_program()
    )
    assert result is None


def test_each_builtin_check_has_a_handler():
    plugin = BuiltinChecks()
    for check_id in [
        "asset_in_scope", "finding_ineligible", "required_sections_present",
        "reproduction_quality", "impact_quality",
    ]:
        assert check_id in plugin._dispatch, f"No handler for {check_id}"


def test_builtin_tracks_token_counts():
    with patch("noisegate.builtins.run_structured_llm_call", return_value=_mock_llm_return()):
        plugin = BuiltinChecks()
        result = plugin.run_check(
            check=_check("asset_in_scope"), report=_report(), program=_program()
        )
    assert result is not None
    assert result.prompt_tokens == 10
    assert result.completion_tokens == 5


def test_custom_model_forwarded_to_llm():
    mock_return = _mock_llm_return()
    with patch("noisegate.builtins.run_structured_llm_call", return_value=mock_return) as mock_fn:
        plugin = BuiltinChecks(model="openai:gpt-4o")
        plugin.run_check(check=_check("asset_in_scope"), report=_report(), program=_program())
    assert mock_fn.call_args.kwargs["model"] == "openai:gpt-4o"


# ---------------------------------------------------------------------------
# Plugin manager
# ---------------------------------------------------------------------------

def test_create_plugin_manager_returns_plugin_manager():
    pm = create_plugin_manager()
    assert isinstance(pm, pluggy.PluginManager)


def test_create_plugin_manager_hook_callable():
    with patch("noisegate.builtins.run_structured_llm_call", return_value=_mock_llm_return()):
        pm = create_plugin_manager(BuiltinChecks())
        result = pm.hook.run_check(
            check=_check("reproduction_quality"), report=_report(), program=_program(),
        )
    assert result is not None
    assert result.passed
