"""
tests/test_sentinel_types.py — Unit tests for sentinel.types.

Coverage:
  - Hard limit constants (CONFIDENCE_CEILING, MAX_REGISTRY_ENTRIES, etc.)
  - Enums (DetectionArm, Severity, ConfidenceBasis)
  - Dataclass construction, KV₄ clamping, to_dict/from_dict round-trip
  - SentinelState composition
"""

from __future__ import annotations

import unittest

from sentinel.types import (
    CONFIDENCE_CEILING,
    MAX_PATTERN_RULES,
    MAX_REGISTRY_ENTRIES,
    NOISE_FLOOR,
    ConfidenceBasis,
    CoSuggestion,
    DetectionArm,
    Incident,
    PatternRule,
    SentinelSignal,
    SentinelState,
    Severity,
)


class TestConstants(unittest.TestCase):
    """Hard limit constants."""

    def test_confidence_ceiling(self):
        self.assertEqual(CONFIDENCE_CEILING, 0.90)

    def test_noise_floor(self):
        self.assertEqual(NOISE_FLOOR, 0.25)

    def test_max_registry(self):
        self.assertIsInstance(MAX_REGISTRY_ENTRIES, int)
        self.assertGreater(MAX_REGISTRY_ENTRIES, 0)

    def test_max_pattern_rules(self):
        self.assertIsInstance(MAX_PATTERN_RULES, int)
        self.assertGreater(MAX_PATTERN_RULES, 0)


class TestEnums(unittest.TestCase):
    """Enum members and values."""

    def test_detection_arm_members(self):
        self.assertIn("REACTIVE", DetectionArm.__members__)
        self.assertIn("PROACTIVE", DetectionArm.__members__)
        self.assertIn("STRUCTURAL", DetectionArm.__members__)

    def test_detection_arm_values(self):
        self.assertEqual(DetectionArm.REACTIVE.value, "reactive")
        self.assertEqual(DetectionArm.PROACTIVE.value, "proactive")
        self.assertEqual(DetectionArm.STRUCTURAL.value, "structural")

    def test_severity_members(self):
        self.assertIn("INFO", Severity.__members__)
        self.assertIn("WARNING", Severity.__members__)
        self.assertIn("CRITICAL", Severity.__members__)

    def test_confidence_basis_members(self):
        self.assertIn("HEURISTIC", ConfidenceBasis.__members__)
        self.assertIn("PATTERN_MATCH", ConfidenceBasis.__members__)
        self.assertIn("USER_CONFIRMED", ConfidenceBasis.__members__)
        self.assertIn("COMBINED", ConfidenceBasis.__members__)


class TestSentinelSignal(unittest.TestCase):
    """SentinelSignal dataclass."""

    def test_minimal_construction(self):
        sig = SentinelSignal(
            taxonomy_id="CD-1",
            arm=DetectionArm.REACTIVE,
            confidence=0.6,
        )
        self.assertEqual(sig.taxonomy_id, "CD-1")
        self.assertEqual(sig.arm, DetectionArm.REACTIVE)
        self.assertEqual(sig.confidence, 0.6)
        self.assertEqual(sig.severity, Severity.INFO)  # default

    def test_kv4_ceiling_clamping(self):
        """Confidence > CONFIDENCE_CEILING must be clamped."""
        sig = SentinelSignal(
            taxonomy_id="FM-1",
            arm=DetectionArm.PROACTIVE,
            confidence=0.99,
        )
        self.assertLessEqual(sig.confidence, CONFIDENCE_CEILING)

    def test_negative_confidence_clamped(self):
        sig = SentinelSignal(taxonomy_id="FM-1", confidence=-0.5)
        self.assertGreater(sig.confidence, 0.0)

    def test_to_dict_roundtrip(self):
        sig = SentinelSignal(
            taxonomy_id="MA-1",
            arm=DetectionArm.STRUCTURAL,
            confidence=0.55,
            severity=Severity.CRITICAL,
            context_snippet="memory loss",
        )
        d = sig.to_dict()
        self.assertEqual(d["taxonomy_id"], "MA-1")
        self.assertEqual(d["arm"], "structural")       # lowercase enum .value
        self.assertEqual(d["severity"], "critical")     # lowercase enum .value
        self.assertIsInstance(d, dict)

    def test_from_dict(self):
        d = {
            "taxonomy_id": "RG-1",
            "arm": "reactive",
            "confidence": 0.5,
            "severity": "info",
            "basis": "heuristic",
        }
        sig = SentinelSignal.from_dict(d)
        self.assertEqual(sig.taxonomy_id, "RG-1")
        self.assertEqual(sig.arm, DetectionArm.REACTIVE)
        self.assertEqual(sig.severity, Severity.INFO)


class TestIncident(unittest.TestCase):
    """Incident dataclass."""

    def test_construction(self):
        inc = Incident(
            id="INC-001",
            taxonomy_id="FM-3",
            user_description="hallucinates",
        )
        self.assertEqual(inc.id, "INC-001")
        self.assertIsNotNone(inc.timestamp)
        self.assertIn("FM-3", inc.taxonomy_ids)  # __post_init__ adds primary

    def test_kv4_confidence_clamping(self):
        inc = Incident(id="INC-002", taxonomy_id="CD-1", confidence=0.99)
        self.assertLessEqual(inc.confidence, CONFIDENCE_CEILING)

    def test_to_dict_from_dict(self):
        inc = Incident(
            id="INC-002",
            taxonomy_id="CD-5",
            user_description="context lost",
            context_snippet="lorem",
        )
        d = inc.to_dict()
        inc2 = Incident.from_dict(d)
        self.assertEqual(inc2.id, "INC-002")
        self.assertEqual(inc2.taxonomy_id, "CD-5")
        self.assertEqual(inc2.user_description, "context lost")


class TestPatternRule(unittest.TestCase):
    """PatternRule dataclass."""

    def test_construction(self):
        rule = PatternRule(
            id="PR-001",
            name="Test Rule",
            taxonomy_ids=["CD-1", "CD-2"],
            trigger_heuristics=["repetition"],
        )
        self.assertEqual(rule.id, "PR-001")
        self.assertTrue(rule.active)
        # precision() is a method, default = 0.5 (no hits, no false positives)
        self.assertEqual(rule.precision(), 0.5)

    def test_precision_with_hits(self):
        rule = PatternRule(
            id="PR-002",
            name="Test Rule 2",
            taxonomy_ids=["FM-1"],
            trigger_heuristics=[],
            hit_count=9,
            false_positive_count=1,
        )
        self.assertAlmostEqual(rule.precision(), 0.9)

    def test_to_dict_from_dict(self):
        rule = PatternRule(id="PR-003", name="RoundTrip", taxonomy_ids=["CD-1"])
        d = rule.to_dict()
        rule2 = PatternRule.from_dict(d)
        self.assertEqual(rule2.id, "PR-003")
        self.assertEqual(rule2.name, "RoundTrip")


class TestCoSuggestion(unittest.TestCase):
    """CoSuggestion dataclass."""

    def test_construction(self):
        cs = CoSuggestion(
            taxonomy_id="CD-1",
            suggested_phrases=["context window", "lost thread"],
        )
        self.assertEqual(cs.taxonomy_id, "CD-1")
        self.assertEqual(len(cs.suggested_phrases), 2)

    def test_acceptance_rate(self):
        cs = CoSuggestion(taxonomy_id="CD-1", usage_count=10, acceptance_count=4)
        self.assertAlmostEqual(cs.acceptance_rate(), 0.4)

    def test_acceptance_rate_zero_usage(self):
        cs = CoSuggestion(taxonomy_id="CD-1")
        self.assertEqual(cs.acceptance_rate(), 0.0)


class TestSentinelState(unittest.TestCase):
    """SentinelState dataclass for persistence."""

    def test_empty_state(self):
        state = SentinelState()
        d = state.to_dict()
        self.assertIn("incidents", d)
        self.assertIn("pattern_rules", d)
        self.assertEqual(len(d["incidents"]), 0)

    def test_to_dict(self):
        state = SentinelState(
            incidents=[{"id": "INC-001", "taxonomy_id": "FM-1"}],
            pattern_rules=[{"id": "PR-001", "name": "Rule"}],
        )
        d = state.to_dict()
        self.assertEqual(len(d["incidents"]), 1)
        self.assertEqual(len(d["pattern_rules"]), 1)
        self.assertEqual(d["schema_version"], 1)


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