"""Tests for v0.47.0 features:
  - SycophancyResult dataclass
  - AgentGuard.sycophancy_score() — no-API path
  - P(True) cross-model calibration warning
  - MultiTurnGuard.zero_shot() constructor + is_factual_reliability_signal
  - SelfHealer _VALIDATED_MODES gate (LOW_RISK removed)
"""
from __future__ import annotations

import math
import sys
import warnings
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))


# ── SycophancyResult ──────────────────────────────────────────────────────────

class TestSycophancyResult:
    """SycophancyResult dataclass sanity checks."""

    def test_fields_exist(self):
        from llm_guard.scoring.agent_guard import SycophancyResult
        sr = SycophancyResult(
            consistency_coefficient=0.8,
            is_sycophantic=False,
            n_rephrasings_tested=3,
            rephrasing_answers=["same", "same", "same"],
            cost_usd=0.0003,
        )
        assert sr.consistency_coefficient == 0.8
        assert sr.is_sycophantic is False
        assert sr.n_rephrasings_tested == 3
        assert len(sr.rephrasing_answers) == 3
        assert sr.cost_usd == pytest.approx(0.0003, abs=1e-9)

    def test_exported_from_init(self):
        from llm_guard import SycophancyResult  # noqa: F401

    def test_sycophantic_flag_set_when_cc_below_threshold(self):
        from llm_guard.scoring.agent_guard import SycophancyResult
        sr = SycophancyResult(
            consistency_coefficient=0.1,
            is_sycophantic=True,
            n_rephrasings_tested=3,
            rephrasing_answers=["different", "different", "different"],
            cost_usd=0.0,
        )
        assert sr.is_sycophantic is True


# ── sycophancy_score() — no-API path ──────────────────────────────────────────

class TestSycophancyScoreNoAPI:
    """sycophancy_score() without an API key should warn and return NaN."""

    def test_no_api_returns_nan_cc(self):
        from llm_guard.scoring.agent_guard import AgentGuard
        guard = AgentGuard()  # no api_key
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            result = guard.sycophancy_score(
                question="What year was the Eiffel Tower built?",
                answer="1889",
            )
        assert math.isnan(result.consistency_coefficient)
        assert result.n_rephrasings_tested == 0
        assert result.rephrasing_answers == []
        assert result.cost_usd == 0.0
        # Should have raised UserWarning about missing API key
        warn_msgs = [str(wi.message) for wi in w if issubclass(wi.category, UserWarning)]
        assert any("api key" in m.lower() or "anthropic_api_key" in m.lower() for m in warn_msgs), (
            f"Expected missing-API-key UserWarning, got: {warn_msgs}"
        )

    def test_is_sycophantic_false_when_nan(self):
        from llm_guard.scoring.agent_guard import AgentGuard
        guard = AgentGuard()
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            result = guard.sycophancy_score("Q?", "A")
        # NaN → is_sycophantic should be False (nan-safe default)
        assert result.is_sycophantic is False

    def test_n_rephrasings_clamped_to_templates(self):
        """Requesting more rephrasings than templates should not raise."""
        from llm_guard.scoring.agent_guard import AgentGuard
        guard = AgentGuard()
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            result = guard.sycophancy_score("Q?", "A", n_rephrasings=100)
        assert result.n_rephrasings_tested == 0  # no API → 0 tested


# ── P(True) cross-model calibration warning ───────────────────────────────────

class TestPtrueCrossModelWarn:
    """score_with_ptrue() should warn if calibration was fitted on a different model."""

    def test_cross_model_warn_fires(self):
        from llm_guard.scoring.agent_guard import AgentGuard
        guard = AgentGuard()
        # Simulate calibration fitted with a different ptrue model (Haiku version mismatch)
        guard._calibrated_ptrue_model = "claude-haiku-4-5-20250307"
        # current ptrue model is "claude-haiku-4-5-20251001" (hardcoded default) — mismatch

        steps = [
            {"thought": "Let me check.", "action_type": "Search",
             "action_arg": "Q", "observation": "Ans."},
            {"thought": "Done.", "action_type": "Finish",
             "action_arg": "Ans", "observation": ""},
        ]
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            # score_with_ptrue with no API key returns behavioral fallback
            guard.score_with_ptrue("Q?", steps, "Ans")

        cross_warns = [
            wi for wi in w
            if issubclass(wi.category, UserWarning)
            and ("calibration" in str(wi.message).lower()
                 or "cross-model" in str(wi.message).lower()
                 or "fitted" in str(wi.message).lower())
        ]
        assert len(cross_warns) >= 1, (
            "Expected cross-model calibration UserWarning, got: "
            + str([str(wi.message) for wi in w])
        )

    def test_no_warn_when_models_match(self):
        from llm_guard.scoring.agent_guard import AgentGuard
        guard = AgentGuard()
        # Calibrated with the SAME ptrue model that is hardcoded as the default
        guard._calibrated_ptrue_model = "claude-haiku-4-5-20251001"  # matches hardcoded default

        steps = [
            {"thought": "Let me check.", "action_type": "Search",
             "action_arg": "Q", "observation": "Ans."},
            {"thought": "Done.", "action_type": "Finish",
             "action_arg": "Ans", "observation": ""},
        ]
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            guard.score_with_ptrue("Q?", steps, "Ans")

        cross_warns = [
            wi for wi in w
            if issubclass(wi.category, UserWarning)
            and "calibration" in str(wi.message).lower()
            and "fitted" in str(wi.message).lower()
        ]
        assert len(cross_warns) == 0


# ── MultiTurnGuard.zero_shot() + is_factual_reliability_signal ────────────────

class TestMultiTurnGuardV047:
    """MTG zero_shot() constructor and is_factual_reliability_signal field."""

    def test_zero_shot_classmethod_exists(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard
        assert hasattr(MultiTurnGuard, "zero_shot")
        assert callable(MultiTurnGuard.zero_shot)

    def test_zero_shot_returns_instance(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard
        guard = MultiTurnGuard.zero_shot()
        assert isinstance(guard, MultiTurnGuard)

    def test_multi_turn_result_has_factual_signal_field(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnResult
        import dataclasses
        field_names = {f.name for f in dataclasses.fields(MultiTurnResult)}
        assert "is_factual_reliability_signal" in field_names

    def test_is_factual_reliability_signal_always_false(self):
        """The field must always be False — it documents that this signal
        is NOT validated for factual accuracy decisions."""
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnResult
        import dataclasses
        # Instantiate with minimum required args
        sig = dataclasses.fields(MultiTurnResult)
        # Just check the default value
        for f in sig:
            if f.name == "is_factual_reliability_signal":
                assert f.default is False, (
                    "is_factual_reliability_signal must default to False"
                )
                break


# ── SelfHealer validated-mode gate ────────────────────────────────────────────

def _make_failure(primary_mode: str, signals: dict | None = None):
    from qppg_service.failure_taxonomy import FailureMode
    return FailureMode(
        primary_mode=primary_mode,
        all_modes=[primary_mode],
        confidence=0.8,
        explanation=f"Test: {primary_mode}",
        signals=signals or {},
    )


class TestSelfHealerValidatedModes:
    """SelfHealer._VALIDATED_MODES must NOT include LOW_RISK."""

    def test_low_risk_not_validated(self):
        """LOW_RISK is not in _VALIDATED_MODES — suggest() should return NO_ACTION."""
        from qppg_service.self_healer import SelfHealer
        healer = SelfHealer()
        failure = _make_failure("LOW_RISK")
        result = healer.suggest(failure=failure, question="Q?", steps=[])
        assert result.action_type == "NO_ACTION", (
            f"Expected NO_ACTION for LOW_RISK; got {result.action_type}"
        )

    def test_retrieval_failure_still_validated(self):
        from qppg_service.self_healer import SelfHealer
        healer = SelfHealer()
        steps = [{"action_type": "Search", "action_arg": "original query",
                  "observation": ""}]
        failure = _make_failure("RETRIEVAL_FAILURE",
                                {"n_search_steps": 1, "mean_sim": 0.1})
        result = healer.suggest(failure=failure, question="What year?", steps=steps)
        assert result.action_type != "UNVALIDATED_MODE"

    def test_excessive_search_still_validated(self):
        from qppg_service.self_healer import SelfHealer
        healer = SelfHealer()
        steps = [{"action_type": "Search", "action_arg": f"q{i}",
                  "observation": "o"} for i in range(7)]
        failure = _make_failure("EXCESSIVE_SEARCH",
                                {"n_search_steps": 7, "mean_sim": 0.3})
        result = healer.suggest(failure=failure, question="What year?", steps=steps)
        assert result.action_type != "UNVALIDATED_MODE"
