"""
tests/test_sentinel_patterns.py — Unit tests for sentinel.patterns (PatternEngine).

Coverage:
  - generalize() — create rules from confirmed incidents (confidence >= 0.7)
  - match() — match rules against (user_msg, ai_response), returns Optional[Tuple[PatternRule, float]]
  - report_false_positive() — precision decay + auto-deactivation
  - Rule lifecycle: creation, merging, deactivation
  - Export/import state round-trip (List[Dict])
  - Rule cap at MAX_PATTERN_RULES
"""

from __future__ import annotations

import unittest

from sentinel.patterns import PatternEngine
from sentinel.types import (
    CONFIDENCE_CEILING,
    MAX_PATTERN_RULES,
    Incident,
    PatternRule,
)


class TestGeneralize(unittest.TestCase):
    """PatternEngine.generalize()."""

    def setUp(self):
        self.pe = PatternEngine()

    def test_creates_rule_from_high_confidence_incident(self):
        inc = Incident(
            id="INC-001",
            taxonomy_id="CD-1",
            user_description="context window exhausted mid-analysis",
            context_snippet="The earlier discussion about architecture...",
            confidence=0.85,  # >= 0.7 threshold
        )
        rule = self.pe.generalize(inc)
        self.assertIsInstance(rule, PatternRule)
        self.assertIn("CD-1", rule.taxonomy_ids)
        self.assertTrue(rule.active)
        self.assertEqual(len(self.pe.rules), 1)

    def test_low_confidence_incident_returns_none(self):
        """generalize() requires confidence >= 0.7."""
        inc = Incident(
            id="INC-002",
            taxonomy_id="CD-1",
            user_description="maybe context issue",
            confidence=0.4,
        )
        rule = self.pe.generalize(inc)
        self.assertIsNone(rule)

    def test_merges_into_existing_rule(self):
        inc1 = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="context lost", confidence=0.85,
        )
        inc2 = Incident(
            id="INC-002", taxonomy_id="CD-1",
            user_description="context window problem", confidence=0.85,
        )
        self.pe.generalize(inc1)
        self.pe.generalize(inc2)
        # Same taxonomy → may merge into 1 rule
        self.assertLessEqual(len(self.pe.rules), 2)

    def test_different_taxonomy_creates_separate(self):
        inc1 = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="context lost", confidence=0.85,
        )
        inc2 = Incident(
            id="INC-002", taxonomy_id="FM-3",
            user_description="hallucinated facts", confidence=0.85,
        )
        self.pe.generalize(inc1)
        self.pe.generalize(inc2)
        self.assertEqual(len(self.pe.rules), 2)


class TestMatch(unittest.TestCase):
    """PatternEngine.match()."""

    def setUp(self):
        self.pe = PatternEngine()

    def test_no_rules_returns_none(self):
        result = self.pe.match(
            user_msg="any text",
            ai_response="any response",
        )
        # Returns Optional[Tuple[PatternRule, float]]
        self.assertIsNone(result)

    def test_matching_after_generalize(self):
        inc = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="context window limit", confidence=0.85,
        )
        self.pe.generalize(inc)
        result = self.pe.match(
            user_msg="the context window is nearing limit",
            ai_response="some response",
        )
        # May or may not match depending on trigger extraction
        if result is not None:
            rule, score = result
            self.assertIsInstance(rule, PatternRule)
            self.assertLessEqual(score, CONFIDENCE_CEILING)


class TestFalsePositive(unittest.TestCase):
    """report_false_positive() precision decay."""

    def setUp(self):
        self.pe = PatternEngine()

    def test_precision_decreases(self):
        inc = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="context issue", confidence=0.85,
        )
        rule = self.pe.generalize(inc)
        initial_precision = rule.precision()  # precision() is a METHOD
        self.pe.report_false_positive(rule.id)  # rule.id, not rule_id
        updated = next(r for r in self.pe.rules if r.id == rule.id)
        self.assertLess(updated.precision(), initial_precision)

    def test_auto_deactivation(self):
        """Rule deactivates when precision < 0.3."""
        inc = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="desc", confidence=0.85,
        )
        rule = self.pe.generalize(inc)
        # Drive precision below threshold
        for _ in range(20):
            self.pe.report_false_positive(rule.id)
        updated = next(r for r in self.pe.rules if r.id == rule.id)
        self.assertFalse(updated.active)

    def test_nonexistent_rule(self):
        """report_false_positive on unknown rule_id should not crash."""
        self.pe.report_false_positive("PR-XXXX")  # no-op


class TestRuleCap(unittest.TestCase):
    """Rule count must not exceed MAX_PATTERN_RULES."""

    def test_cap_enforced(self):
        pe = PatternEngine()
        for i in range(MAX_PATTERN_RULES + 10):
            inc = Incident(
                id=f"INC-{i:05d}",
                taxonomy_id=f"CD-{i % 11 + 1}",
                user_description=f"unique description {i}",
                confidence=0.85,
            )
            pe.generalize(inc)
        self.assertLessEqual(len(pe.rules), MAX_PATTERN_RULES)


class TestExportImport(unittest.TestCase):
    """State round-trip — export/import use List[Dict]."""

    def test_roundtrip(self):
        pe = PatternEngine()
        pe.generalize(Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="a", confidence=0.85,
        ))
        pe.generalize(Incident(
            id="INC-002", taxonomy_id="FM-3",
            user_description="b", confidence=0.85,
        ))
        state = pe.export_state()
        self.assertIsInstance(state, list)

        pe2 = PatternEngine()
        pe2.import_state(state)
        self.assertEqual(len(pe2.rules), len(pe.rules))

    def test_import_empty(self):
        pe = PatternEngine()
        pe.import_state([])  # List[Dict], not dict
        self.assertEqual(len(pe.rules), 0)

    def test_import_corrupt(self):
        pe = PatternEngine()
        pe.import_state([{"bad": "data"}])  # List of bad dicts
        self.assertEqual(len(pe.rules), 0)


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