"""
tests/test_sentinel_detection.py — Unit tests for sentinel.detection arms.

Coverage:
  - ReactiveArm.scan (regex matching, no false positives on clean text)
  - ProactiveArm.scan (hedging, sycophancy, turn degradation)
  - StructuralArm.scan (pattern-matched signals)
  - All arms return Optional[SentinelSignal]
  - KV₄: confidence always ≤ CONFIDENCE_CEILING
"""

from __future__ import annotations

import unittest
from unittest.mock import MagicMock

from sentinel.detection import ReactiveArm, ProactiveArm, StructuralArm
from sentinel.types import (
    CONFIDENCE_CEILING,
    DetectionArm,
    PatternRule,
    SentinelSignal,
)
from sentinel.taxonomy import ALL_TAXONOMY


class TestReactiveArm(unittest.TestCase):
    """ReactiveArm regex-based detection."""

    def setUp(self):
        self.arm = ReactiveArm()

    def test_clean_text_no_signals(self):
        signal = self.arm.scan(user_msg="Hello world")
        self.assertIsNone(signal)

    def test_wrong_trigger(self):
        """'wrong' matches reactive trigger → FM-1."""
        signal = self.arm.scan(
            user_msg="that's wrong, you're making things up",
        )
        self.assertIsNotNone(signal)
        self.assertIsInstance(signal, SentinelSignal)
        self.assertEqual(signal.arm, DetectionArm.REACTIVE)
        self.assertIn(signal.taxonomy_id, ALL_TAXONOMY)

    def test_not_what_i_meant_trigger(self):
        signal = self.arm.scan(
            user_msg="that's not what I meant at all",
        )
        self.assertIsNotNone(signal)
        self.assertEqual(signal.taxonomy_id, "CD-1")

    def test_confidence_ceiling(self):
        """All reactive signals must obey KV₄."""
        signal = self.arm.scan(
            user_msg="that's wrong, you're making things up",
        )
        if signal is not None:
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)

    def test_empty_input(self):
        signal = self.arm.scan(user_msg="")
        self.assertIsNone(signal)


class TestProactiveArm(unittest.TestCase):
    """ProactiveArm session-aware heuristics."""

    def setUp(self):
        self.arm = ProactiveArm()

    def _mock_ledger(self, call_count=0, anomalies=None):
        ledger = MagicMock()
        ledger.call_count = call_count
        ledger.calls = []
        ledger.get_anomaly_history.return_value = anomalies or []
        return ledger

    def test_clean_response_no_signals(self):
        # Use call_count=0 to avoid H2 (constraint amnesia) firing
        signal = self.arm.scan(
            ai_response="Here is a clear, direct answer.",
            session_ledger=self._mock_ledger(call_count=0),
            turn_number=1,
        )
        self.assertIsNone(signal)

    def test_high_hedging_density(self):
        hedged = (
            "It might be possible that perhaps this could potentially "
            "be something that maybe works, though it's not entirely clear "
            "if this is the right approach. It seems like it could possibly "
            "be valid, but I'm not sure."
        )
        signal = self.arm.scan(
            ai_response=hedged,
            session_ledger=self._mock_ledger(call_count=0),
            turn_number=1,
        )
        # May detect hedging pattern
        if signal is not None:
            self.assertIsInstance(signal, SentinelSignal)
            self.assertEqual(signal.arm, DetectionArm.PROACTIVE)
            self.assertIn(signal.taxonomy_id, ALL_TAXONOMY)

    def test_sycophancy_detection(self):
        syc = (
            "That's an excellent point! You're absolutely right, that's a "
            "great observation. I completely agree with your brilliant insight."
        )
        signal = self.arm.scan(
            ai_response=syc,
            session_ledger=self._mock_ledger(call_count=0),
            turn_number=1,
        )
        if signal is not None:
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)

    def test_empty_response(self):
        signal = self.arm.scan(
            ai_response="",
            session_ledger=self._mock_ledger(call_count=0),
            turn_number=1,
        )
        self.assertIsNone(signal)

    def test_no_ledger(self):
        """Proactive arm must handle None ledger gracefully."""
        signal = self.arm.scan(
            ai_response="Some text",
            session_ledger=None,
            turn_number=1,
        )
        self.assertTrue(signal is None or isinstance(signal, SentinelSignal))

    def test_constraint_amnesia_fires_with_high_call_count(self):
        """H2: 0 framework terms + call_count >= 3 → CD-5."""
        signal = self.arm.scan(
            ai_response="This is a plain response with no framework terms.",
            session_ledger=self._mock_ledger(call_count=5),
            turn_number=1,
        )
        if signal is not None:
            self.assertEqual(signal.arm, DetectionArm.PROACTIVE)
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)


class TestStructuralArm(unittest.TestCase):
    """StructuralArm pattern-based detection."""

    def setUp(self):
        self.arm = StructuralArm()

    def test_no_rules_no_signals(self):
        signal = self.arm.scan("any", "thing", [])
        self.assertIsNone(signal)

    def test_matching_rule(self):
        rule = PatternRule(
            id="PR-001",
            name="Context Window Rule",
            taxonomy_ids=["CD-1"],
            trigger_heuristics=["context window"],
            hit_count=5,
            false_positive_count=0,
            active=True,
        )
        signal = self.arm.scan(
            "user text",
            "The context window is getting very large",
            [rule],
        )
        # May or may not match depending on trigger logic
        if signal is not None:
            self.assertEqual(signal.arm, DetectionArm.STRUCTURAL)
            self.assertIn(signal.taxonomy_id, ALL_TAXONOMY)
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)

    def test_inactive_rule_ignored(self):
        rule = PatternRule(
            id="PR-002",
            name="Error Rule",
            taxonomy_ids=["FM-1"],
            trigger_heuristics=["error"],
            hit_count=5,
            false_positive_count=0,
            active=False,
        )
        signal = self.arm.scan("user", "there was an error", [rule])
        self.assertIsNone(signal)


if __name__ == "__main__":
    unittest.main()
