"""Tests for Runner execution logic."""

from unittest.mock import MagicMock, patch

from ai_qa.cache import Cache
from ai_qa.core.check import BaseCheck
from ai_qa.core.context import Context
from ai_qa.core.result import CheckResult
from ai_qa.core.runner import Runner


class MockCheck(BaseCheck):
    """Mock check for testing."""

    requires_llm = False
    version = "1.0.0"

    def __init__(self, name: str = "mock", **kwargs):
        """Init mock check."""
        super().__init__(**kwargs)
        self.name = name

    def validate_context(self, ctx: Context) -> None:
        """No validation needed."""
        pass

    def evaluate(self, ctx: Context) -> CheckResult:
        """Return success."""
        return CheckResult(
            name=self.name,
            score=1.0,
            passed=True,
            threshold=self.threshold,
            evidence="Mock check passed",
            cost_usd=0.0,
            tokens_used={},
            check_version=self.version,
        )


class FailingCheck(BaseCheck):
    """Check that always fails."""

    requires_llm = False
    version = "1.0.0"

    def validate_context(self, ctx: Context) -> None:
        """No validation needed."""
        pass

    def evaluate(self, ctx: Context) -> CheckResult:
        """Return failure."""
        return CheckResult(
            name="failing",
            score=0.0,
            passed=False,
            threshold=0.5,
            evidence="Check failed intentionally",
            cost_usd=0.0,
            tokens_used={},
            check_version=self.version,
        )


class LLMCheck(BaseCheck):
    """Mock LLM check."""

    requires_llm = True
    version = "1.0.0"

    def validate_context(self, ctx: Context) -> None:
        """No validation needed."""
        pass

    def evaluate(self, ctx: Context) -> CheckResult:
        """Return LLM result."""
        return CheckResult(
            name="llm_check",
            score=0.8,
            passed=True,
            threshold=0.5,
            evidence="LLM check result",
            cost_usd=0.01,
            tokens_used={"input": 10, "output": 5},
            check_version=self.version,
        )


def test_runner_executes_all_checks():
    """Test that runner executes all checks."""
    runner = Runner()
    ctx = Context(output="test output")
    checks = [
        MockCheck("check1"),
        MockCheck("check2"),
        MockCheck("check3"),
    ]

    result = runner.run(ctx, checks)

    assert len(result.checks) == 3
    assert all(r.passed for r in result.checks)


def test_runner_individual_failure():
    """Test that one check failure doesn't stop others."""
    runner = Runner()
    ctx = Context(output="test output")
    checks = [
        MockCheck("check1"),
        FailingCheck(),
        MockCheck("check3"),
    ]

    result = runner.run(ctx, checks)

    assert len(result.checks) == 3
    assert result.checks[0].passed
    assert not result.checks[1].passed
    assert result.checks[2].passed


def test_runner_cache_hit():
    """Test that cached result is returned with cached=True."""
    cache = Cache()
    cache.clear()  # Clear any previous cache
    runner = Runner(cache=cache)
    ctx = Context(output="test output")
    check = LLMCheck()

    # First run - should compute
    result1 = runner.run(ctx, [check])
    assert not result1.checks[0].cached

    # Second run - should use cache
    result2 = runner.run(ctx, [check])
    assert result2.checks[0].cached
    assert result2.checks[0].score == result1.checks[0].score


def test_runner_cache_miss():
    """Test that cache miss triggers computation."""
    cache = Cache()
    cache.clear()  # Clear any previous cache
    runner = Runner(cache=cache)
    ctx1 = Context(output="test output 1")
    ctx2 = Context(output="test output 2")
    check = LLMCheck()

    # Different contexts should not hit cache
    result1 = runner.run(ctx1, [check])
    result2 = runner.run(ctx2, [check])

    assert not result1.checks[0].cached
    assert not result2.checks[0].cached


def test_runner_parallelism_respected():
    """Test that runner respects parallelism setting."""
    runner = Runner(parallelism=2)
    ctx = Context(output="test output")
    checks = [LLMCheck() for _ in range(5)]

    # Should not raise - parallelism is respected
    result = runner.run(ctx, checks)
    assert len(result.checks) == 5


def test_runner_check_exception_handling():
    """Test that runner catches check exceptions."""

    class ExceptionCheck(BaseCheck):
        requires_llm = False
        version = "1.0.0"

        def validate_context(self, ctx: Context) -> None:
            pass

        def evaluate(self, ctx: Context) -> CheckResult:
            raise ValueError("Check error")

    runner = Runner()
    ctx = Context(output="test output")
    check = ExceptionCheck()

    result = runner.run(ctx, [check])

    # Should return failed result instead of raising
    assert len(result.checks) == 1
    assert not result.checks[0].passed
    assert "Check error" in result.checks[0].evidence


def test_runner_deterministic_before_llm():
    """Test that deterministic checks run before LLM checks."""
    call_order = []

    class OrderedDeterministic(BaseCheck):
        requires_llm = False
        version = "1.0.0"

        def validate_context(self, ctx: Context) -> None:
            pass

        def evaluate(self, ctx: Context) -> CheckResult:
            call_order.append("deterministic")
            return CheckResult(
                name="det",
                score=1.0,
                passed=True,
                threshold=0.5,
                evidence="",
                cost_usd=0.0,
                tokens_used={},
                check_version=self.version,
            )

    class OrderedLLM(BaseCheck):
        requires_llm = True
        version = "1.0.0"

        def validate_context(self, ctx: Context) -> None:
            pass

        def evaluate(self, ctx: Context) -> CheckResult:
            call_order.append("llm")
            return CheckResult(
                name="llm",
                score=1.0,
                passed=True,
                threshold=0.5,
                evidence="",
                cost_usd=0.0,
                tokens_used={},
                check_version=self.version,
            )

    runner = Runner()
    ctx = Context(output="test")
    checks = [
        OrderedLLM(),
        OrderedDeterministic(),
        OrderedLLM(),
    ]

    runner.run(ctx, checks)

    # Deterministic check should come first
    assert call_order[0] == "deterministic"
