"""
Tests for ChatGuard — chatbot response reliability scoring.
===========================================================
Run with:  pytest tests/test_chat_guard.py -v

Categories:
  Unit  — pure Python, no API calls, no mocking required for native mode
"""

import pytest

from llm_guard.multi_turn.chat_guard import ChatGuard, ChatResult, _risk_to_tier

# ---------------------------------------------------------------------------
# Shared constants
# ---------------------------------------------------------------------------

_VALID_FAILURE_MODES = {
    "clean", "hallucination", "hedge_heavy", "off_topic",
    "over_verbose", "unknown",
}

_VALID_TIERS = {"HIGH", "MEDIUM", "LOW"}

_HEDGE_RESPONSE = (
    "I think probably this might possibly be correct, I'm not sure though, "
    "it seems like perhaps maybe it could be right, but I'm unsure."
)

_CLEAN_RESPONSE = "Paris is the capital of France."

_QUERY = "What is the capital of France?"


# ---------------------------------------------------------------------------
# TestChatGuardInit
# ---------------------------------------------------------------------------

class TestChatGuardInit:
    def test_default_mode_is_native(self):
        guard = ChatGuard()
        assert guard.mode == "native"

    def test_invalid_mode_raises(self):
        with pytest.raises(ValueError):
            ChatGuard(mode="invalid")

    def test_valid_modes_accepted(self):
        for mode in ("native", "synthetic_trace", "hybrid"):
            guard = ChatGuard(mode=mode)
            assert guard.mode == mode

    def test_lazy_components_are_none(self):
        guard = ChatGuard()
        assert guard._extractor is None
        assert guard._judge is None
        assert guard._client is None


# ---------------------------------------------------------------------------
# TestChatGuardNativeMode
# ---------------------------------------------------------------------------

class TestChatGuardNativeMode:
    def _guard(self) -> ChatGuard:
        return ChatGuard(mode="native")

    def test_returns_chat_result(self):
        guard = self._guard()
        result = guard.score_chat(_QUERY, _CLEAN_RESPONSE)
        assert isinstance(result, ChatResult)

    def test_result_fields_present(self):
        guard = self._guard()
        result = guard.score_chat(_QUERY, _CLEAN_RESPONSE)

        assert isinstance(result.risk_score, float)
        assert 0.0 <= result.risk_score <= 1.0

        assert result.confidence_tier in _VALID_TIERS

        assert isinstance(result.needs_alert, bool)

        assert result.failure_mode in _VALID_FAILURE_MODES

        assert isinstance(result.native_features, dict)

        # native mode — trace_risk must be None
        assert result.trace_risk is None

    def test_hedge_heavy_gets_high_risk(self):
        guard = self._guard()
        result = guard.score_chat(_QUERY, _HEDGE_RESPONSE)
        assert result.risk_score > 0.5, (
            f"Hedge-heavy response should score > 0.5, got {result.risk_score}"
        )

    def test_clean_answer_lower_than_hedge(self):
        guard = self._guard()
        clean_result = guard.score_chat(_QUERY, _CLEAN_RESPONSE)
        hedge_result = guard.score_chat(_QUERY, _HEDGE_RESPONSE)
        assert clean_result.risk_score < hedge_result.risk_score, (
            f"Clean response ({clean_result.risk_score}) should score lower than "
            f"hedge-heavy response ({hedge_result.risk_score})"
        )

    def test_conversation_history_accepted(self):
        guard = self._guard()
        history = [
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi there!"},
        ]
        # Should not raise
        result = guard.score_chat(_QUERY, _CLEAN_RESPONSE, conversation_history=history)
        assert isinstance(result, ChatResult)

    def test_needs_alert_consistent_with_risk(self):
        guard = self._guard()
        result = guard.score_chat(_QUERY, _HEDGE_RESPONSE)
        assert result.needs_alert == (result.risk_score >= 0.70)

    def test_confidence_tier_consistent_with_risk(self):
        guard = self._guard()
        result = guard.score_chat(_QUERY, _CLEAN_RESPONSE)
        expected_tier = _risk_to_tier(result.risk_score)
        assert result.confidence_tier == expected_tier


# ---------------------------------------------------------------------------
# TestChatResult
# ---------------------------------------------------------------------------

class TestChatResult:
    def test_to_dict(self):
        result = ChatResult(
            risk_score=0.42,
            confidence_tier="MEDIUM",
            needs_alert=False,
            failure_mode="clean",
            native_features={"hedge_density": 0.0},
            trace_risk=None,
        )
        d = result.to_dict()
        assert "risk_score" in d
        assert "confidence_tier" in d
        assert "needs_alert" in d
        assert "failure_mode" in d
        assert "native_features" in d
        assert "trace_risk" in d
        assert "metadata" in d
        assert len(d) == 9  # updated for v0.33: behavioral_components, processing_time_ms, metadata added

    def test_to_dict_values(self):
        result = ChatResult(
            risk_score=0.80,
            confidence_tier="LOW",
            needs_alert=True,
            failure_mode="hallucination",
            native_features={"hedge_density": 0.1},
            trace_risk=0.75,
        )
        d = result.to_dict()
        assert d["risk_score"] == 0.80
        assert d["confidence_tier"] == "LOW"
        assert d["needs_alert"] is True
        assert d["failure_mode"] == "hallucination"
        assert d["trace_risk"] == 0.75


# ---------------------------------------------------------------------------
# TestRiskToTier helper
# ---------------------------------------------------------------------------

class TestRiskToTier:
    def test_high_confidence_low_risk(self):
        assert _risk_to_tier(0.10) == "HIGH"
        assert _risk_to_tier(0.49) == "HIGH"

    def test_medium_confidence(self):
        assert _risk_to_tier(0.50) == "MEDIUM"
        assert _risk_to_tier(0.69) == "MEDIUM"

    def test_low_confidence_high_risk(self):
        assert _risk_to_tier(0.70) == "LOW"
        assert _risk_to_tier(1.00) == "LOW"


# ---------------------------------------------------------------------------
# TestChatGuardFit
# ---------------------------------------------------------------------------

class TestChatGuardFit:
    def test_fit_raises_on_empty(self):
        guard = ChatGuard(mode="native")
        with pytest.raises(ValueError, match="fit()"):
            guard.fit([])

    def test_fit_warns_on_small_dataset(self, caplog):
        import logging
        guard = ChatGuard(mode="native")
        # Use 2 samples (one per class) to satisfy sklearn's 2-class requirement
        # while still being well below the 20-pair threshold that triggers the warning.
        pairs = [
            {"query": "q1", "response": "Clear answer.", "label": 0},
            {"query": "q2", "response": "I think maybe possibly.", "label": 1},
        ]
        with caplog.at_level(logging.WARNING):
            guard.fit(pairs)
        assert any("20" in msg for msg in caplog.messages)

    def test_fit_sets_judge(self):
        guard = ChatGuard(mode="native")
        pairs = [
            {"query": f"question {i}", "response": "Paris is the capital.", "label": 0}
            for i in range(10)
        ] + [
            {"query": f"question {i}", "response": "I think maybe possibly.", "label": 1}
            for i in range(10)
        ]
        guard.fit(pairs)
        assert guard._judge is not None

    def test_post_fit_returns_chat_result(self):
        guard = ChatGuard(mode="native")
        pairs = [
            {"query": f"q{i}", "response": "Clear answer.", "label": 0}
            for i in range(10)
        ] + [
            {"query": f"q{i}", "response": "I'm not sure maybe.", "label": 1}
            for i in range(10)
        ]
        guard.fit(pairs)
        result = guard.score_chat("What is Paris?", "Paris is a city.")
        assert isinstance(result, ChatResult)
        assert 0.0 <= result.risk_score <= 1.0
