"""
tests/test_sentinel_articulation.py — Unit tests for sentinel.articulation (CoArticulator).

Coverage:
  - update_from_incident() — phrase extraction, merge, trim
  - get_enhanced_picker_options() — base enrichment, acceptance gate
  - record_acceptance() / record_shown() — counter increments
  - _extract_distinctive_phrase() — stop-word filtering
  - Export/import state round-trip (List[Dict])
  - _MAX_PHRASES_PER_TAX cap enforcement
"""

from __future__ import annotations

import unittest

from sentinel.articulation import (
    CoArticulator,
    _MAX_PHRASES_PER_TAX,
    _MIN_ACCEPTANCE_RATE,
)
from sentinel.taxonomy import PICKER_OPTIONS
from sentinel.types import CoSuggestion, Incident


class TestUpdateFromIncident(unittest.TestCase):
    """CoArticulator.update_from_incident()."""

    def setUp(self):
        self.ca = CoArticulator()

    def test_creates_suggestion_from_incident(self):
        inc = Incident(
            id="INC-001",
            taxonomy_id="CD-1",
            user_description="context window was exhausted mid-task",
        )
        self.ca.update_from_incident(inc)
        self.assertEqual(len(self.ca.suggestions), 1)
        self.assertEqual(self.ca.suggestions[0].taxonomy_id, "CD-1")
        self.assertIn(
            "context window was exhausted mid-task",
            self.ca.suggestions[0].suggested_phrases,
        )

    def test_merges_phrases_for_same_taxonomy(self):
        inc1 = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="context lost",
        )
        inc2 = Incident(
            id="INC-002", taxonomy_id="CD-1",
            user_description="window ran out during analysis",
        )
        self.ca.update_from_incident(inc1)
        self.ca.update_from_incident(inc2)
        self.assertEqual(len(self.ca.suggestions), 1)
        self.assertGreaterEqual(len(self.ca.suggestions[0].suggested_phrases), 2)

    def test_different_taxonomy_separate_entries(self):
        inc1 = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="context lost",
        )
        inc2 = Incident(
            id="INC-002", taxonomy_id="FM-3",
            user_description="hallucinated facts",
        )
        self.ca.update_from_incident(inc1)
        self.ca.update_from_incident(inc2)
        self.assertEqual(len(self.ca.suggestions), 2)

    def test_no_description_no_snippet(self):
        inc = Incident(id="INC-001", taxonomy_id="CD-1")
        self.ca.update_from_incident(inc)
        # No phrases → no suggestion created
        self.assertEqual(len(self.ca.suggestions), 0)

    def test_snippet_extraction(self):
        inc = Incident(
            id="INC-001",
            taxonomy_id="CD-1",
            context_snippet="The architecture analysis requires understanding dependency graphs and integration patterns",
        )
        self.ca.update_from_incident(inc)
        self.assertEqual(len(self.ca.suggestions), 1)
        self.assertTrue(len(self.ca.suggestions[0].suggested_phrases) >= 1)


class TestPhraseCap(unittest.TestCase):
    """_MAX_PHRASES_PER_TAX enforcement."""

    def test_trim_at_cap(self):
        ca = CoArticulator()
        for i in range(_MAX_PHRASES_PER_TAX + 5):
            inc = Incident(
                id=f"INC-{i:04d}",
                taxonomy_id="CD-1",
                user_description=f"unique description number {i} with extra words",
            )
            ca.update_from_incident(inc)
        suggestions = ca.suggestions
        self.assertEqual(len(suggestions), 1)
        self.assertLessEqual(
            len(suggestions[0].suggested_phrases), _MAX_PHRASES_PER_TAX,
        )


class TestEnhancedPicker(unittest.TestCase):
    """get_enhanced_picker_options()."""

    def test_returns_base_when_no_suggestions(self):
        ca = CoArticulator()
        options = ca.get_enhanced_picker_options()
        self.assertEqual(len(options), len(PICKER_OPTIONS))
        for (tax_id, text), (base_id, base_text) in zip(options, PICKER_OPTIONS):
            self.assertEqual(tax_id, base_id)
            self.assertEqual(text, base_text)

    def test_enrichment_with_sufficient_acceptance(self):
        ca = CoArticulator()
        tax_id = PICKER_OPTIONS[0][0]
        inc = Incident(
            id="INC-001",
            taxonomy_id=tax_id,
            user_description="custom phrase by user describing failure mode",
        )
        ca.update_from_incident(inc)
        # Simulate acceptance > threshold by manipulating internal state
        sug = ca._get_suggestion(tax_id)
        sug.usage_count = 10
        sug.acceptance_count = 5  # 50% > 30% (_MIN_ACCEPTANCE_RATE)
        options = ca.get_enhanced_picker_options()
        enriched_text = dict(options)[tax_id]
        self.assertIn("e.g.", enriched_text)

    def test_no_enrichment_below_threshold(self):
        ca = CoArticulator()
        tax_id = PICKER_OPTIONS[0][0]
        inc = Incident(
            id="INC-001",
            taxonomy_id=tax_id,
            user_description="custom phrase",
        )
        ca.update_from_incident(inc)
        sug = ca._get_suggestion(tax_id)
        sug.usage_count = 10
        sug.acceptance_count = 1  # 10% < 30%
        options = ca.get_enhanced_picker_options()
        enriched_text = dict(options)[tax_id]
        self.assertNotIn("e.g.", enriched_text)


class TestAcceptanceTracking(unittest.TestCase):
    """record_acceptance() / record_shown()."""

    def test_counters(self):
        ca = CoArticulator()
        inc = Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="some context failure description",
        )
        ca.update_from_incident(inc)
        ca.record_shown("CD-1")
        ca.record_shown("CD-1")
        ca.record_acceptance("CD-1")
        sug = ca._get_suggestion("CD-1")
        self.assertEqual(sug.usage_count, 2)
        self.assertEqual(sug.acceptance_count, 1)

    def test_nonexistent_taxonomy_no_crash(self):
        ca = CoArticulator()
        ca.record_shown("NONEXIST")
        ca.record_acceptance("NONEXIST")


class TestDistinctivePhrase(unittest.TestCase):
    """_extract_distinctive_phrase()."""

    def test_returns_none_for_empty(self):
        ca = CoArticulator()
        self.assertIsNone(ca._extract_distinctive_phrase(""))
        self.assertIsNone(ca._extract_distinctive_phrase(None))

    def test_returns_none_for_too_short(self):
        ca = CoArticulator()
        self.assertIsNone(ca._extract_distinctive_phrase("is the"))

    def test_filters_stop_words(self):
        ca = CoArticulator()
        result = ca._extract_distinctive_phrase(
            "the context window was completely exhausted during analysis"
        )
        # Should have some distinctive content
        if result:
            self.assertGreater(len(result), 0)


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

    def test_roundtrip(self):
        ca = CoArticulator()
        ca.update_from_incident(Incident(
            id="INC-001", taxonomy_id="CD-1",
            user_description="desc 1",
        ))
        ca.update_from_incident(Incident(
            id="INC-002", taxonomy_id="FM-3",
            user_description="desc 2",
        ))
        state = ca.export_state()
        self.assertIsInstance(state, list)

        ca2 = CoArticulator()
        ca2.import_state(state)
        self.assertEqual(len(ca2.suggestions), len(ca.suggestions))

    def test_import_empty(self):
        ca = CoArticulator()
        ca.import_state([])
        self.assertEqual(len(ca.suggestions), 0)

    def test_import_corrupt(self):
        ca = CoArticulator()
        ca.import_state([{"bad": "data"}])
        # Should silently skip corrupt entries
        self.assertEqual(len(ca.suggestions), 0)


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