"""Tests for confidence scoring and low-confidence callbacks."""

from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional

import pytest

# Import directly from the module to avoid loading torch via learned.py
from hieromem_core.retrieval.confidence import (
    ConfidenceWeights,
    ConfidenceConfig,
    ConfidenceEvaluator,
    ConfidenceResult,
    ConfidenceComponents,
    LowConfidenceAction,
    compute_confidence,
    freshness_score,
    create_ask_clarification_callback,
    create_expand_search_callback,
)


class TestConfidenceWeights:
    def test_default_weights(self) -> None:
        weights = ConfidenceWeights()
        assert weights.alpha == 0.4
        assert weights.beta == 0.3
        assert weights.gamma == 0.2
        assert weights.delta == 0.1

    def test_normalised(self) -> None:
        weights = ConfidenceWeights(alpha=2.0, beta=2.0, gamma=2.0, delta=2.0)
        norm = weights.normalised()
        assert norm.alpha == 0.25
        assert norm.beta == 0.25
        assert norm.gamma == 0.25
        assert norm.delta == 0.25

    def test_normalised_zero_sum_raises(self) -> None:
        weights = ConfidenceWeights(alpha=0, beta=0, gamma=0, delta=0)
        with pytest.raises(ValueError, match="positive value"):
            weights.normalised()


class TestFreshnessScore:
    def test_recent_timestamp_high_score(self) -> None:
        now = datetime.now()
        score = freshness_score(timestamp=now, now=now)
        assert score == 1.0

    def test_old_timestamp_low_score(self) -> None:
        now = datetime.now()
        old = now - timedelta(days=14)
        score = freshness_score(timestamp=old, now=now, horizon=timedelta(days=7))
        assert score == 0.0

    def test_halfway_timestamp(self) -> None:
        now = datetime.now()
        half = now - timedelta(days=3.5)
        score = freshness_score(timestamp=half, now=now, horizon=timedelta(days=7))
        assert 0.4 < score < 0.6


class TestComputeConfidence:
    def test_all_perfect_scores(self) -> None:
        confidence = compute_confidence(
            vector_similarity=1.0,
            graph_path_score=1.0,
            trust_score=1.0,
            freshness=1.0,
        )
        assert abs(confidence - 1.0) < 0.0001  # Allow floating point tolerance

    def test_all_zero_scores(self) -> None:
        confidence = compute_confidence(
            vector_similarity=0.0,
            graph_path_score=0.0,
            trust_score=0.0,
            freshness=0.0,
        )
        assert confidence == 0.0

    def test_weighted_computation(self) -> None:
        # Only vector similarity is high
        confidence = compute_confidence(
            vector_similarity=1.0,
            graph_path_score=0.0,
            trust_score=0.0,
            freshness=0.0,
            weights=ConfidenceWeights(alpha=1.0, beta=0.0, gamma=0.0, delta=0.0),
        )
        assert confidence == 1.0

    def test_custom_weights(self) -> None:
        weights = ConfidenceWeights(alpha=0.5, beta=0.5, gamma=0.0, delta=0.0)
        confidence = compute_confidence(
            vector_similarity=0.8,
            graph_path_score=0.6,
            trust_score=1.0,
            freshness=1.0,
            weights=weights,
        )
        # (0.5 * 0.8 + 0.5 * 0.6) = 0.7
        assert abs(confidence - 0.7) < 0.001


class TestConfidenceConfig:
    def test_default_threshold(self) -> None:
        config = ConfidenceConfig()
        assert config.threshold == 0.5

    def test_namespace_threshold_override(self) -> None:
        config = ConfidenceConfig(
            threshold=0.5,
            namespace_thresholds={"strict": 0.8, "lenient": 0.2},
        )
        assert config.get_threshold() == 0.5
        assert config.get_threshold("strict") == 0.8
        assert config.get_threshold("lenient") == 0.2
        assert config.get_threshold("unknown") == 0.5


class TestConfidenceEvaluator:
    def test_high_confidence_no_callbacks(self) -> None:
        callback_called = []

        def test_callback(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            callback_called.append(True)
            return {"action": "test"}

        config = ConfidenceConfig(threshold=0.5, callbacks=[test_callback])
        evaluator = ConfidenceEvaluator(config)

        result = evaluator.evaluate(
            query="test query",
            results=["result1", "result2"],
            vector_similarity=0.9,
            graph_path_score=0.8,
            trust_score=0.9,
            freshness=0.9,
        )

        assert result.confidence > 0.5
        assert not result.is_low_confidence
        assert len(callback_called) == 0
        assert len(result.callback_results) == 0

    def test_low_confidence_triggers_callbacks(self) -> None:
        callback_called = []

        def test_callback(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            callback_called.append({
                "query": query,
                "confidence": confidence,
                "threshold": threshold,
            })
            return {
                "action": "ask_clarification",
                "prompt": f"Please clarify: {query}",
            }

        config = ConfidenceConfig(threshold=0.8, callbacks=[test_callback])
        evaluator = ConfidenceEvaluator(config)

        result = evaluator.evaluate(
            query="ambiguous query",
            results=[],
            vector_similarity=0.3,
            graph_path_score=0.2,
            trust_score=0.5,
            freshness=0.5,
        )

        assert result.confidence < 0.8
        assert result.is_low_confidence
        assert len(callback_called) == 1
        assert callback_called[0]["query"] == "ambiguous query"
        assert len(result.callback_results) == 1
        assert result.callback_results[0].action == "ask_clarification"
        assert "ambiguous query" in (result.callback_results[0].prompt or "")

    def test_multiple_callbacks(self) -> None:
        def callback1(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            return {"action": "action1"}

        def callback2(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            return {"action": "action2", "prompt": "Test prompt"}

        config = ConfidenceConfig(threshold=0.9, callbacks=[callback1, callback2])
        evaluator = ConfidenceEvaluator(config)

        result = evaluator.evaluate(
            query="test",
            results=[],
            vector_similarity=0.5,
            graph_path_score=0.5,
            trust_score=0.5,
            freshness=0.5,
        )

        assert len(result.callback_results) == 2
        actions = [r.action for r in result.callback_results]
        assert "action1" in actions
        assert "action2" in actions

    def test_callback_exception_handled(self) -> None:
        def failing_callback(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            raise ValueError("Callback failed!")

        def working_callback(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            return {"action": "worked"}

        config = ConfidenceConfig(
            threshold=0.9,
            callbacks=[failing_callback, working_callback],
        )
        evaluator = ConfidenceEvaluator(config)

        # Should not raise, failing callback is caught
        result = evaluator.evaluate(
            query="test",
            results=[],
            vector_similarity=0.5,
            graph_path_score=0.5,
            trust_score=0.5,
            freshness=0.5,
        )

        # Working callback should still execute
        assert len(result.callback_results) == 1
        assert result.callback_results[0].action == "worked"

    def test_namespace_specific_threshold(self) -> None:
        def simple_callback(
            query: str,
            results: List[Any],
            confidence: float,
            threshold: float,
            context: Dict[str, Any],
        ) -> Optional[Dict[str, Any]]:
            return {"action": "triggered"}

        config = ConfidenceConfig(
            threshold=0.5,
            namespace_thresholds={"strict_ns": 0.9},
            callbacks=[simple_callback],
        )
        evaluator = ConfidenceEvaluator(config)

        # Same scores, different namespaces
        result_default = evaluator.evaluate(
            query="test",
            results=[],
            vector_similarity=0.7,
            graph_path_score=0.7,
            trust_score=0.7,
            freshness=0.7,
            namespace="default_ns",
        )

        result_strict = evaluator.evaluate(
            query="test",
            results=[],
            vector_similarity=0.7,
            graph_path_score=0.7,
            trust_score=0.7,
            freshness=0.7,
            namespace="strict_ns",
        )

        # Default namespace: 0.7 > 0.5, not low confidence
        assert not result_default.is_low_confidence
        assert len(result_default.callback_results) == 0

        # Strict namespace: 0.7 < 0.9, low confidence
        assert result_strict.is_low_confidence
        assert len(result_strict.callback_results) == 1

    def test_register_callback(self) -> None:
        evaluator = ConfidenceEvaluator(ConfidenceConfig(threshold=0.9))
        assert len(evaluator.config.callbacks) == 0

        evaluator.register_callback(
            lambda q, r, c, t, ctx: {"action": "registered"}
        )
        assert len(evaluator.config.callbacks) == 1

    def test_set_threshold(self) -> None:
        evaluator = ConfidenceEvaluator(ConfidenceConfig(threshold=0.5))

        evaluator.set_threshold(0.7)
        assert evaluator.config.threshold == 0.7

        evaluator.set_threshold(0.9, namespace="special")
        assert evaluator.config.threshold == 0.7  # Global unchanged
        assert evaluator.config.namespace_thresholds["special"] == 0.9

    def test_set_threshold_validates_range(self) -> None:
        evaluator = ConfidenceEvaluator()

        with pytest.raises(ValueError, match="between 0.0 and 1.0"):
            evaluator.set_threshold(1.5)

        with pytest.raises(ValueError, match="between 0.0 and 1.0"):
            evaluator.set_threshold(-0.1)


class TestConfidenceResult:
    def test_should_ask_followup(self) -> None:
        result = ConfidenceResult(
            confidence=0.4,
            threshold=0.5,
            is_low_confidence=True,
            callback_results=[
                LowConfidenceAction(action="ask_clarification", prompt="Clarify?"),
            ],
            components=ConfidenceComponents(0.4, 0.4, 0.4, 0.4),
        )
        assert result.should_ask_followup is True

        result2 = ConfidenceResult(
            confidence=0.4,
            threshold=0.5,
            is_low_confidence=True,
            callback_results=[
                LowConfidenceAction(action="expand_search"),
            ],
            components=ConfidenceComponents(0.4, 0.4, 0.4, 0.4),
        )
        assert result2.should_ask_followup is False

    def test_suggested_prompt(self) -> None:
        result = ConfidenceResult(
            confidence=0.4,
            threshold=0.5,
            is_low_confidence=True,
            callback_results=[
                LowConfidenceAction(action="expand_search"),
                LowConfidenceAction(action="ask_clarification", prompt="First prompt"),
                LowConfidenceAction(action="ask_followup", prompt="Second prompt"),
            ],
            components=ConfidenceComponents(0.4, 0.4, 0.4, 0.4),
        )
        assert result.suggested_prompt == "First prompt"

    def test_suggested_prompt_none(self) -> None:
        result = ConfidenceResult(
            confidence=0.4,
            threshold=0.5,
            is_low_confidence=True,
            callback_results=[],
            components=ConfidenceComponents(0.4, 0.4, 0.4, 0.4),
        )
        assert result.suggested_prompt is None


class TestCallbackFactories:
    def test_create_ask_clarification_callback(self) -> None:
        callback = create_ask_clarification_callback()
        result = callback(
            query="user preferences",
            results=[],
            confidence=0.3,
            threshold=0.5,
            context={},
        )

        assert result["action"] == "ask_clarification"
        assert "user preferences" in result["prompt"]
        assert result["metadata"]["confidence"] == 0.3
        assert result["metadata"]["threshold"] == 0.5

    def test_create_ask_clarification_custom_template(self) -> None:
        callback = create_ask_clarification_callback(
            prompt_template="What did you mean by '{query}'? Please be specific."
        )
        result = callback(
            query="test query",
            results=[],
            confidence=0.3,
            threshold=0.5,
            context={},
        )

        assert "What did you mean by 'test query'?" in result["prompt"]

    def test_create_expand_search_callback(self) -> None:
        callback = create_expand_search_callback(expansion_factor=2.0)
        result = callback(
            query="test",
            results=["r1", "r2", "r3"],
            confidence=0.3,
            threshold=0.5,
            context={},
        )

        assert result["action"] == "expand_search"
        assert result["metadata"]["expansion_factor"] == 2.0
        assert result["metadata"]["original_result_count"] == 3
