"""tests/test_adversarial_sycophancy_detector.py — Unit tests for SycophancyPremiseFlipDetector."""
import pytest
import warnings
from unittest.mock import MagicMock, patch

pytestmark = pytest.mark.unit


class TestImport:
    def test_import_detector(self):
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        assert SycophancyPremiseFlipDetector is not None

    def test_import_jaccard(self):
        from llm_guard.adversarial.sycophancy_detector import _jaccard
        assert _jaccard is not None


class TestJaccard:
    def test_identical(self):
        from llm_guard.adversarial.sycophancy_detector import _jaccard
        assert _jaccard("hello world", "hello world") == 1.0

    def test_disjoint(self):
        from llm_guard.adversarial.sycophancy_detector import _jaccard
        assert _jaccard("apple banana", "cherry grape") == 0.0

    def test_empty_returns_zero(self):
        from llm_guard.adversarial.sycophancy_detector import _jaccard
        assert _jaccard("", "hello") == 0.0
        assert _jaccard("hello", "") == 0.0

    def test_partial_overlap(self):
        from llm_guard.adversarial.sycophancy_detector import _jaccard
        score = _jaccard("hello world", "hello there")
        assert 0.0 < score < 1.0


class TestInstantiation:
    def test_no_key_no_experimental_warns(self):
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="", experimental=False)
            # Should warn about experimental gate
            assert any("experimental" in str(x.message).lower() for x in w)

    def test_experimental_true_no_experimental_warn(self):
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="", experimental=True)
            # Should not warn about experimental gate
            assert not any("experimental" in str(x.message).lower() for x in w)

    def test_no_key_returns_neutral(self):
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="", experimental=True)
        score = det.flip_score("What is 2+2?")
        assert score == 0.5

    def test_flip_threshold_stored(self):
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="", flip_threshold=0.4, experimental=True)
        assert det._flip_threshold == 0.4


class TestFlipScoreWithMockedAPI:
    def test_flip_score_no_flip(self):
        """When neutral and pressure answers are the same, flip_score should be ~0."""
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="sk-ant-fake", experimental=True)
        with patch.object(det, "_call", return_value="Paris is the capital of France"):
            score = det.flip_score("What is the capital of France?", "Paris")
        assert score == pytest.approx(0.0, abs=0.1)

    def test_flip_score_full_flip(self):
        """When neutral answer is totally different from pressure answer."""
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="sk-ant-fake", experimental=True)
        call_results = iter(["Paris is the capital", "Berlin is actually the answer"])
        with patch.object(det, "_call", side_effect=call_results):
            score = det.flip_score("What is the capital?", "Paris")
        assert 0.0 <= score <= 1.0

    def test_flip_score_empty_neutral_returns_half(self):
        """When neutral call fails (returns empty), should return 0.5."""
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="sk-ant-fake", experimental=True)
        with patch.object(det, "_call", return_value=""):
            score = det.flip_score("What is Paris?")
        assert score == 0.5

    def test_flip_score_batch(self):
        from llm_guard.adversarial.sycophancy_detector import SycophancyPremiseFlipDetector
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det = SycophancyPremiseFlipDetector(api_key="", experimental=True)
        pairs = [("Q1", "A1"), ("Q2", "A2")]
        scores = det.flip_score_batch(pairs)
        assert len(scores) == 2
        assert all(s == 0.5 for s in scores)  # no key → all 0.5
