"""
tests/test_sentinel_integration.py — Integration tests for the Sentinel subsystem.

Coverage:
  - SentinelEngine end-to-end: scan → report → export → import → verify
  - SentinelResponse wire format
  - build_framework_context with sentinel_status parameter
  - Picker options format (List[Dict] with "id"/"label")
  - Merge convergence across arms
"""

from __future__ import annotations

import json
import unittest

from sentinel.engine import SentinelEngine, get_sentinel, reset_sentinel
from sentinel.taxonomy import ALL_TAXONOMY, PICKER_OPTIONS
from sentinel.types import (
    CONFIDENCE_CEILING,
    SentinelSignal,
    Incident,
)
from fw_server.types import SentinelResponse


class TestEndToEnd(unittest.TestCase):
    """Full lifecycle: scan → report → export → fresh restore → verify."""

    def setUp(self):
        reset_sentinel()
        self.engine = get_sentinel()

    def tearDown(self):
        reset_sentinel()

    def test_scan_clean_text(self):
        signal = self.engine.scan(
            user_msg="What is the weather today?",
            ai_response="This is a well-formed response with clear structure.",
        )
        # Signal may be None or low confidence
        if signal is not None:
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)

    def test_scan_problematic_text(self):
        signal = self.engine.scan(
            user_msg="that's wrong, you made an error",
            ai_response=(
                "I apologize for the confusion. I'm sorry, let me start over. "
                "I apologize, I think I made an error earlier."
            ),
        )
        # May or may not trigger depending on arm logic
        if signal is not None:
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)
            self.assertIn(signal.taxonomy_id, ALL_TAXONOMY)

    def test_report_creates_incident(self):
        inc = self.engine.report_incident(
            taxonomy_id="CD-1",
            description="Context window exhausted during analysis",
        )
        self.assertIsInstance(inc, Incident)
        self.assertEqual(inc.taxonomy_id, "CD-1")
        stats = self.engine.get_stats()
        self.assertGreaterEqual(stats["incident_count"], 1)

    def test_full_lifecycle_roundtrip(self):
        # Step 1: Report several incidents
        self.engine.report_incident(taxonomy_id="CD-1", description="first context loss")
        self.engine.report_incident(taxonomy_id="FM-3", description="hallucinated reference")
        self.engine.report_incident(taxonomy_id="CD-1", description="second context loss")

        # Step 2: Export
        state = self.engine.export_state()
        self.assertIsInstance(state, dict)

        # Step 3: Fresh engine + import
        reset_sentinel()
        engine2 = get_sentinel()
        engine2.import_state(state)

        # Step 4: Verify
        stats = engine2.get_stats()
        self.assertEqual(stats["incident_count"], 3)

    def test_scan_after_report_uses_patterns(self):
        """After reporting, structural arm should have a pattern rule."""
        self.engine.report_incident(
            taxonomy_id="CD-1",
            description="context window limit reached",
        )
        # Now scan — structural arm may match if triggers were extracted
        signal = self.engine.scan(
            user_msg="The context window limit is approaching",
            ai_response="Let me continue the analysis.",
        )
        # We don't mandate a match, but the engine should not crash


class TestSentinelResponseWireFormat(unittest.TestCase):
    """SentinelResponse.to_dict() wire format."""

    def test_empty_response(self):
        resp = SentinelResponse(action="status")
        d = resp.to_dict()
        self.assertEqual(d["action"], "status")
        # None fields should be omitted
        self.assertNotIn("signal", d)
        self.assertNotIn("incident", d)

    def test_scan_response(self):
        resp = SentinelResponse(
            action="scan",
            signal={"taxonomy_id": "CD-1", "confidence": 0.6},
            stats={"incident_count": 5},
        )
        d = resp.to_dict()
        self.assertEqual(d["action"], "scan")
        self.assertIn("signal", d)
        self.assertIn("stats", d)
        # JSON serialisable
        serialised = json.dumps(d)
        self.assertIsInstance(serialised, str)

    def test_report_response(self):
        resp = SentinelResponse(
            action="report",
            incident={"id": "INC-001", "taxonomy_id": "CD-1"},
            stats={"incident_count": 1},
        )
        d = resp.to_dict()
        self.assertEqual(d["action"], "report")
        self.assertIn("incident", d)

    def test_error_response(self):
        resp = SentinelResponse(action="scan", error="SentinelEngine not available")
        d = resp.to_dict()
        self.assertIn("error", d)


class TestFrameworkContextIntegration(unittest.TestCase):
    """build_framework_context sentinel_status parameter."""

    def test_sentinel_status_included(self):
        from fw_server.context import build_framework_context
        ctx = build_framework_context(
            "sentinel_scan",  # first arg is tool_name, not path
            composite_score=0.7,
            grade="Tasdik",
            sentinel_status={"incident_count": 3, "active_patterns": 1},
        )
        # sentinel_status should appear in the context dict
        self.assertIsInstance(ctx, dict)


class TestPickerOptions(unittest.TestCase):
    """Picker options should be well-formed and usable."""

    def setUp(self):
        reset_sentinel()
        self.engine = get_sentinel()

    def tearDown(self):
        reset_sentinel()

    def test_picker_options_match_taxonomy(self):
        options = self.engine.get_picker_options()
        # Options are List[Dict] with "id" and "label" keys
        for opt in options:
            self.assertIsInstance(opt, dict)
            self.assertIn(opt["id"], ALL_TAXONOMY, f"{opt['id']} not in taxonomy")
            self.assertIsInstance(opt["label"], str)
            self.assertTrue(len(opt["label"]) > 0)

    def test_picker_options_count(self):
        options = self.engine.get_picker_options()
        self.assertGreaterEqual(len(options), 8)


class TestMergeConvergence(unittest.TestCase):
    """When multiple arms detect the same taxonomy_id, confidence should boost."""

    def setUp(self):
        reset_sentinel()
        self.engine = get_sentinel()

    def tearDown(self):
        reset_sentinel()

    def test_multi_arm_boost(self):
        """Text triggering reactive arm + structural rule should converge."""
        # Add a structural rule via an incident
        self.engine.report_incident(
            taxonomy_id="CD-1",
            description="context window running out",
        )
        # Scan text that triggers both reactive + structural
        signal = self.engine.scan(
            user_msg="that's wrong, the context window is exhausted",
            ai_response="I apologize for the confusion, the context seems lost",
        )
        if signal is not None:
            self.assertLessEqual(signal.confidence, CONFIDENCE_CEILING)


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