"""Tests for RAG answer generation and LLM-as-Judge scoring."""

import asyncio
from dataclasses import dataclass
from typing import Optional

import pytest

from cortex.eval.generation import (
    GeneratedAnswer,
    JudgeResult,
    generate_answer,
    judge_answer,
)


# ---------------------------------------------------------------------------
# Mock LLM
# ---------------------------------------------------------------------------

@dataclass
class MockResponse:
    text: str


class MockLLM:
    """Mock LLM that returns pre-configured responses based on prompt content."""

    def __init__(self, response_text: str = "Mock answer"):
        self.response_text = response_text
        self.calls = []

    async def complete(self, system: str = "", user: str = "", **kwargs):
        self.calls.append({"system": system, "user": user, **kwargs})
        return MockResponse(text=self.response_text)


# ---------------------------------------------------------------------------
# generate_answer tests
# ---------------------------------------------------------------------------

class TestGenerateAnswer:
    """Tests for generate_answer()."""

    def test_produces_coherent_response_from_claims(self):
        """generate_answer returns a GeneratedAnswer with the LLM's response."""
        llm = MockLLM(response_text="Alice enjoys hiking in the mountains.")
        claims = [
            "Alice likes outdoor activities",
            "Alice went hiking last weekend",
            "Alice's favorite mountain is Mount Fuji",
        ]

        result = asyncio.run(generate_answer(
            question="What does Alice enjoy doing?",
            retrieved_claims=claims,
            llm=llm,
        ))

        assert isinstance(result, GeneratedAnswer)
        assert result.answer == "Alice enjoys hiking in the mountains."
        assert result.question == "What does Alice enjoy doing?"
        assert result.retrieved_claims == claims
        assert len(llm.calls) == 1

    def test_empty_claims_still_calls_llm(self):
        """generate_answer with empty claims still calls LLM (which should say 'no info')."""
        llm = MockLLM(response_text="No information available.")

        result = asyncio.run(generate_answer(
            question="What is Bob's favorite color?",
            retrieved_claims=[],
            llm=llm,
        ))

        assert isinstance(result, GeneratedAnswer)
        assert result.answer == "No information available."
        assert result.retrieved_claims == []
        assert len(llm.calls) == 1

    def test_strips_whitespace_from_answer(self):
        """generate_answer strips leading/trailing whitespace."""
        llm = MockLLM(response_text="  trimmed answer  \n")

        result = asyncio.run(generate_answer(
            question="test?",
            retrieved_claims=["claim"],
            llm=llm,
        ))

        assert result.answer == "trimmed answer"

    def test_model_passthrough(self):
        """generate_answer passes model kwarg to LLM."""
        llm = MockLLM(response_text="answer")

        asyncio.run(generate_answer(
            question="test?",
            retrieved_claims=["claim"],
            llm=llm,
            model="gpt-4o",
        ))

        assert llm.calls[0]["model"] == "gpt-4o"

    def test_no_model_kwarg_when_none(self):
        """generate_answer does not pass model kwarg when model is None."""
        llm = MockLLM(response_text="answer")

        asyncio.run(generate_answer(
            question="test?",
            retrieved_claims=["claim"],
            llm=llm,
            model=None,
        ))

        assert "model" not in llm.calls[0]

    def test_fallback_str_response(self):
        """generate_answer handles LLM response without .text attribute."""

        class StringResponse:
            def __str__(self):
                return "string response"

        class StringLLM:
            async def complete(self, **kwargs):
                return StringResponse()

        result = asyncio.run(generate_answer(
            question="test?",
            retrieved_claims=["claim"],
            llm=StringLLM(),
        ))

        assert result.answer == "string response"


# ---------------------------------------------------------------------------
# judge_answer tests
# ---------------------------------------------------------------------------

class TestJudgeAnswer:
    """Tests for judge_answer()."""

    def test_correct_answer_scores_1(self):
        """judge_answer returns 1.0 for correct answers."""
        llm = MockLLM(response_text="correct\nThe answer matches the ground truth.")

        result = asyncio.run(judge_answer(
            question="What is Alice's favorite color?",
            prediction="Alice's favorite color is blue.",
            ground_truth="Blue",
            llm=llm,
        ))

        assert isinstance(result, JudgeResult)
        assert result.score == 1.0
        assert result.reasoning == "The answer matches the ground truth."
        assert result.question == "What is Alice's favorite color?"
        assert result.prediction == "Alice's favorite color is blue."
        assert result.ground_truth == "Blue"

    def test_incorrect_answer_scores_0(self):
        """judge_answer returns 0.0 for wrong answers."""
        llm = MockLLM(response_text="incorrect\nThe answer says red but the truth is blue.")

        result = asyncio.run(judge_answer(
            question="What is Alice's favorite color?",
            prediction="Alice's favorite color is red.",
            ground_truth="Blue",
            llm=llm,
        ))

        assert result.score == 0.0
        assert "red" in result.reasoning.lower() or "blue" in result.reasoning.lower()

    def test_partial_match_correct(self):
        """judge_answer can score partial matches as correct if LLM says so."""
        llm = MockLLM(response_text="correct\nPartially matches but conveys the key facts.")

        result = asyncio.run(judge_answer(
            question="Where does Bob work?",
            prediction="Bob works at a tech company in San Francisco.",
            ground_truth="Bob works at Google in SF.",
            llm=llm,
        ))

        assert result.score == 1.0

    def test_partial_match_incorrect(self):
        """judge_answer can score partial matches as incorrect if LLM says so."""
        llm = MockLLM(response_text="incorrect\nMissing key information about the company name.")

        result = asyncio.run(judge_answer(
            question="Where does Bob work?",
            prediction="Bob works somewhere.",
            ground_truth="Bob works at Google.",
            llm=llm,
        ))

        assert result.score == 0.0

    def test_verdict_case_insensitive(self):
        """judge_answer handles mixed case in verdict."""
        llm = MockLLM(response_text="CORRECT\nMatches.")

        result = asyncio.run(judge_answer(
            question="test?",
            prediction="answer",
            ground_truth="answer",
            llm=llm,
        ))

        assert result.score == 1.0

    def test_no_reasoning_line(self):
        """judge_answer handles response with only verdict, no reasoning."""
        llm = MockLLM(response_text="correct")

        result = asyncio.run(judge_answer(
            question="test?",
            prediction="answer",
            ground_truth="answer",
            llm=llm,
        ))

        assert result.score == 1.0
        assert result.reasoning == ""

    def test_ambiguous_verdict_defaults_to_incorrect(self):
        """judge_answer defaults to 0.0 for ambiguous/unexpected verdicts."""
        llm = MockLLM(response_text="maybe\nUncertain about this one.")

        result = asyncio.run(judge_answer(
            question="test?",
            prediction="answer",
            ground_truth="other",
            llm=llm,
        ))

        assert result.score == 0.0

    def test_model_passthrough(self):
        """judge_answer passes model kwarg to LLM."""
        llm = MockLLM(response_text="correct\nOK")

        asyncio.run(judge_answer(
            question="test?",
            prediction="a",
            ground_truth="a",
            llm=llm,
            model="claude-3-haiku",
        ))

        assert llm.calls[0]["model"] == "claude-3-haiku"


# ---------------------------------------------------------------------------
# Adversarial scenario
# ---------------------------------------------------------------------------

class TestAdversarialScoring:
    """Tests for adversarial questions (category 5) via RAG pipeline."""

    def test_no_info_generation_scores_correct(self):
        """When generation says 'no info', adversarial judge should score 1.0."""
        from cortex.eval.metrics import adversarial_accuracy

        generated_text = "No information available."
        score = adversarial_accuracy(generated_text)
        assert score == 1.0

    def test_hallucinated_answer_scores_incorrect(self):
        """When generation hallucinates an answer for adversarial Q, score should be 0.0."""
        from cortex.eval.metrics import adversarial_accuracy

        generated_text = "Alice's favorite restaurant is Olive Garden."
        score = adversarial_accuracy(generated_text)
        assert score == 0.0

    def test_not_mentioned_variant(self):
        """Adversarial: 'not mentioned' variant also scores 1.0."""
        from cortex.eval.metrics import adversarial_accuracy

        generated_text = "This information is not mentioned in the conversation."
        score = adversarial_accuracy(generated_text)
        assert score == 1.0


# ---------------------------------------------------------------------------
# Integration: harness with RAG mode
# ---------------------------------------------------------------------------

class TestHarnessRAGMode:
    """Test EvalHarness with mode='rag' using mocks."""

    def test_harness_accepts_mode_parameter(self):
        """EvalHarness accepts mode parameter."""
        from cortex.eval.harness import EvalHarness

        class FakePlugin:
            pass

        harness = EvalHarness(plugin=FakePlugin(), mode="rag")
        assert harness.mode == "rag"

    def test_harness_rejects_invalid_mode(self):
        """EvalHarness rejects invalid mode."""
        from cortex.eval.harness import EvalHarness

        class FakePlugin:
            pass

        with pytest.raises(ValueError, match="Unknown eval mode"):
            EvalHarness(plugin=FakePlugin(), mode="invalid")

    def test_harness_full_mode(self):
        """EvalHarness accepts mode='full'."""
        from cortex.eval.harness import EvalHarness

        class FakePlugin:
            pass

        harness = EvalHarness(plugin=FakePlugin(), mode="full")
        assert harness.mode == "full"

    def test_harness_retrieval_only_default(self):
        """EvalHarness defaults to retrieval-only mode."""
        from cortex.eval.harness import EvalHarness

        class FakePlugin:
            pass

        harness = EvalHarness(plugin=FakePlugin())
        assert harness.mode == "retrieval-only"
