#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Integration tests for P24 neuroscience refinements (Items 96-100)."""
import sys
import time
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch, call

ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))


# ---------------------------------------------------------------------------
# Item 96: Temporal context reinstatement on boot
# ---------------------------------------------------------------------------
class TestTemporalContextReinstatement(unittest.TestCase):
    """Item 96 -- load_recent_brain() injects working_memory_context from last session."""

    def test_working_memory_context_injected_when_items_exist(self):
        """get_active_contexts is called and result stored in brain_data."""
        wm_items = [
            {"text": "deployed sqlite migration successfully"},
            {"text": "fixed hebbian boot sequence"},
        ]
        with patch("pulse.brain.boot.loaders._get_recent_titles", return_value=[]):
            with patch("pulse.brain.boot.loaders._load_jsonl_titles", return_value=[]):
                with patch("pulse.brain.boot.loaders.load_json", return_value=[]):
                    with patch("pulse.brain.boot.loaders._get_confirmed_titles", return_value=[]):
                        with patch("pulse.brain.working_memory.get_active_contexts", return_value=wm_items) as mock_wm:
                            from pulse.brain.boot.loaders import load_recent_brain
                            result = load_recent_brain()
                            mock_wm.assert_called_once_with("search")
                            self.assertIn("working_memory_context", result)
                            self.assertEqual(len(result["working_memory_context"]), 2)

    def test_working_memory_context_truncated_to_5(self):
        """Only the first 5 working memory items are included."""
        wm_items = [{"text": "item %d" % i} for i in range(10)]
        with patch("pulse.brain.boot.loaders._get_recent_titles", return_value=[]):
            with patch("pulse.brain.boot.loaders._load_jsonl_titles", return_value=[]):
                with patch("pulse.brain.boot.loaders.load_json", return_value=[]):
                    with patch("pulse.brain.boot.loaders._get_confirmed_titles", return_value=[]):
                        with patch("pulse.brain.working_memory.get_active_contexts", return_value=wm_items):
                            from pulse.brain.boot.loaders import load_recent_brain
                            result = load_recent_brain()
                            ctx = result.get("working_memory_context", [])
                            self.assertLessEqual(len(ctx), 5)

    def test_text_truncated_to_100_chars(self):
        """Each working memory text is capped at 100 characters."""
        long_text = "x" * 200
        wm_items = [{"text": long_text}]
        with patch("pulse.brain.boot.loaders._get_recent_titles", return_value=[]):
            with patch("pulse.brain.boot.loaders._load_jsonl_titles", return_value=[]):
                with patch("pulse.brain.boot.loaders.load_json", return_value=[]):
                    with patch("pulse.brain.boot.loaders._get_confirmed_titles", return_value=[]):
                        with patch("pulse.brain.working_memory.get_active_contexts", return_value=wm_items):
                            from pulse.brain.boot.loaders import load_recent_brain
                            result = load_recent_brain()
                            ctx = result.get("working_memory_context", [])
                            self.assertTrue(all(len(t) <= 100 for t in ctx))

    def test_fail_open_on_exception(self):
        """Exception in working memory does not break load_recent_brain."""
        with patch("pulse.brain.boot.loaders._get_recent_titles", return_value=[]):
            with patch("pulse.brain.boot.loaders._load_jsonl_titles", return_value=[]):
                with patch("pulse.brain.boot.loaders.load_json", return_value=[]):
                    with patch("pulse.brain.boot.loaders._get_confirmed_titles", return_value=[]):
                        with patch("pulse.brain.working_memory.get_active_contexts", side_effect=RuntimeError("boom")):
                            from pulse.brain.boot.loaders import load_recent_brain
                            result = load_recent_brain()
                            self.assertIsInstance(result, dict)
                            self.assertIn("lessons", result)

    def test_empty_working_memory_produces_empty_context(self):
        """If no WM items exist, working_memory_context key is absent or empty."""
        with patch("pulse.brain.boot.loaders._get_recent_titles", return_value=[]):
            with patch("pulse.brain.boot.loaders._load_jsonl_titles", return_value=[]):
                with patch("pulse.brain.boot.loaders.load_json", return_value=[]):
                    with patch("pulse.brain.boot.loaders._get_confirmed_titles", return_value=[]):
                        with patch("pulse.brain.working_memory.get_active_contexts", return_value=[]):
                            from pulse.brain.boot.loaders import load_recent_brain
                            result = load_recent_brain()
                            ctx = result.get("working_memory_context", [])
                            self.assertEqual(ctx, [])


# ---------------------------------------------------------------------------
# Item 97: Forgetting curve visualization data
# ---------------------------------------------------------------------------
class TestForgettingCurveVisualization(unittest.TestCase):
    """Item 97 -- _compute_domain_decay_stats returns avg confidence and count."""

    def test_returns_stats_from_db(self):
        """DB query result is reflected in the returned dict."""
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = (0.72, 14)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            from pulse.brain.metacognition.metamemory import _compute_domain_decay_stats
            result = _compute_domain_decay_stats("python")
            self.assertEqual(result["domain"], "python")
            self.assertAlmostEqual(result["avg_confidence"], 0.72, places=2)
            self.assertEqual(result["item_count"], 14)

    def test_db_call_uses_correct_domain_param(self):
        """SQL query is parameterised with the domain string."""
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = (0.5, 0)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            from pulse.brain.metacognition.metamemory import _compute_domain_decay_stats
            _compute_domain_decay_stats("testing")
            args = mock_conn.execute.call_args[0]
            self.assertIn("testing", args[1])

    def test_fail_open_returns_defaults(self):
        """Exception returns safe defaults rather than raising."""
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")):
            from pulse.brain.metacognition.metamemory import _compute_domain_decay_stats
            result = _compute_domain_decay_stats("unknown")
            self.assertEqual(result["avg_confidence"], 0.5)
            self.assertEqual(result["item_count"], 0)

    def test_null_confidence_falls_back_to_half(self):
        """None from DB averages to 0.5 default."""
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = (None, 0)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            from pulse.brain.metacognition.metamemory import _compute_domain_decay_stats
            result = _compute_domain_decay_stats("empty_domain")
            self.assertEqual(result["avg_confidence"], 0.5)


# ---------------------------------------------------------------------------
# Item 98: Anti-pattern severity scoring
# ---------------------------------------------------------------------------
class TestAntiPatternSeverityScoring(unittest.TestCase):
    """Item 98 -- NEVER rules get 2x effective confidence in gate1."""

    def _make_ap(self, rule, confidence=0.5, evidence_count=1):
        return {"id": "ap1", "rule": rule, "description": rule,
                "confidence": confidence, "evidence_count": evidence_count,
                "domain": "general", "status": "active"}

    def test_never_rule_triggers_at_half_threshold(self):
        """NEVER rule at conf 0.45 passes gate (0.45*2 = 0.9 >= 0.8)."""
        from pulse.brain.integrity.gate_checks import gate1_anti_pattern
        ap = self._make_ap("NEVER use sys.exit in library code", confidence=0.45)
        mock_path = MagicMock(spec=Path)
        mock_path.exists.return_value = True
        with patch("pulse.brain.integrity.gate_checks._query_anti_patterns", return_value=[ap]):
            with patch("pulse.brain.integrity.gate_checks._is_relevant_to_content", return_value=True):
                blocked, msg = gate1_anti_pattern(["sysexit"], "test content", mock_path)
                self.assertTrue(blocked)

    def test_avoid_rule_below_threshold_not_blocked(self):
        """An avoid rule at conf 0.45 does NOT trigger (1x, below 0.8)."""
        from pulse.brain.integrity.gate_checks import gate1_anti_pattern
        ap = self._make_ap("avoid using print in production", confidence=0.45)
        mock_path = MagicMock(spec=Path)
        mock_path.exists.return_value = True
        with patch("pulse.brain.integrity.gate_checks._query_anti_patterns", return_value=[ap]):
            with patch("pulse.brain.integrity.gate_checks._is_relevant_to_content", return_value=True):
                blocked, _ = gate1_anti_pattern(["print"], "test content", mock_path)
                self.assertFalse(blocked)

    def test_never_keyword_case_insensitive(self):
        """Lowercase 'never' in rule is treated same as 'NEVER'."""
        from pulse.brain.integrity.gate_checks import gate1_anti_pattern
        ap = self._make_ap("never hardcode API keys", confidence=0.45)
        mock_path = MagicMock(spec=Path)
        mock_path.exists.return_value = True
        with patch("pulse.brain.integrity.gate_checks._query_anti_patterns", return_value=[ap]):
            with patch("pulse.brain.integrity.gate_checks._is_relevant_to_content", return_value=True):
                blocked, _ = gate1_anti_pattern(["apikey"], "test", mock_path)
                self.assertTrue(blocked)

    def test_evidence_threshold_triggers_independently(self):
        """evidence_count >= 5 triggers regardless of confidence/severity."""
        from pulse.brain.integrity.gate_checks import gate1_anti_pattern
        ap = self._make_ap("avoid this", confidence=0.1, evidence_count=6)
        mock_path = MagicMock(spec=Path)
        mock_path.exists.return_value = True
        with patch("pulse.brain.integrity.gate_checks._query_anti_patterns", return_value=[ap]):
            with patch("pulse.brain.integrity.gate_checks._is_relevant_to_content", return_value=True):
                blocked, _ = gate1_anti_pattern(["pattern"], "test", mock_path)
                self.assertTrue(blocked)


# ---------------------------------------------------------------------------
# Item 99: Consolidation progress tracking
# ---------------------------------------------------------------------------
class TestConsolidationProgressTracking(unittest.TestCase):
    """Item 99 -- _save_stage_progress persists per-stage timing data."""

    def test_save_stage_progress_calls_save_json(self):
        """_save_stage_progress delegates to save_json with timestamp + stages."""
        from pulse.daemons.consolidation.consolidation_daemon import _save_stage_progress
        with patch("pulse.daemons.consolidation.consolidation_daemon.save_json") as mock_save:
            _save_stage_progress({"stage_1": {"status": "ok"}})
            mock_save.assert_called_once()
            data = mock_save.call_args[0][1]
            self.assertIn("timestamp", data)
            self.assertIn("stages", data)

    def test_save_stage_progress_stores_results_verbatim(self):
        """Stages dict is stored exactly under the 'stages' key."""
        from pulse.daemons.consolidation.consolidation_daemon import _save_stage_progress
        stages = {"s1": {"status": "ok"}, "s2": {"status": "error"}}
        with patch("pulse.daemons.consolidation.consolidation_daemon.save_json") as mock_save:
            _save_stage_progress(stages)
            data = mock_save.call_args[0][1]
            self.assertEqual(data["stages"], stages)

    def test_save_stage_progress_fail_open(self):
        """Exception in save_json does not propagate out of _save_stage_progress."""
        from pulse.daemons.consolidation.consolidation_daemon import _save_stage_progress
        with patch("pulse.daemons.consolidation.consolidation_daemon.save_json",
                   side_effect=Exception("disk full")):
            # Must not raise
            _save_stage_progress({"s1": {}})

    def test_stage_progress_file_in_metrics_dir(self):
        """_STAGE_PROGRESS_FILE path contains 'Metrics' subdirectory."""
        from pulse.daemons.consolidation import consolidation_daemon as cd
        self.assertIn("Metrics", str(cd._STAGE_PROGRESS_FILE))


# ---------------------------------------------------------------------------
# Item 100: Boot briefing cache
# ---------------------------------------------------------------------------
class TestBootBriefingCache(unittest.TestCase):
    """Item 100 -- get_boot_briefing_cached reuses result within 5-minute TTL."""

    def _reset_cache(self):
        import pulse.brain.pulse_boot as pb
        pb._BOOT_CACHE["briefing"] = None
        pb._BOOT_CACHE["timestamp"] = 0.0
        pb._BOOT_CACHE["cache_key"] = ""

    def test_first_call_populates_cache(self):
        """First call invokes boot() and stores the result."""
        self._reset_cache()
        with patch("pulse.brain.pulse_boot.boot", return_value="briefing_text") as mock_boot:
            from pulse.brain.pulse_boot import get_boot_briefing_cached
            result = get_boot_briefing_cached("claude", "standard")
            mock_boot.assert_called_once()
            self.assertEqual(result, "briefing_text")

    def test_second_call_hits_cache(self):
        """Second call within TTL does not call boot() again."""
        self._reset_cache()
        with patch("pulse.brain.pulse_boot.boot", return_value="briefing_text") as mock_boot:
            from pulse.brain.pulse_boot import get_boot_briefing_cached
            get_boot_briefing_cached("claude", "standard")
            get_boot_briefing_cached("claude", "standard")
            self.assertEqual(mock_boot.call_count, 1)

    def test_different_tier_bypasses_cache(self):
        """Different tier produces a separate boot() call."""
        self._reset_cache()
        with patch("pulse.brain.pulse_boot.boot", return_value="text") as mock_boot:
            from pulse.brain.pulse_boot import get_boot_briefing_cached
            get_boot_briefing_cached("claude", "standard")
            get_boot_briefing_cached("claude", "premium")
            self.assertGreaterEqual(mock_boot.call_count, 2)

    def test_cache_returns_string(self):
        """Return value is always a string."""
        self._reset_cache()
        with patch("pulse.brain.pulse_boot.boot", return_value="hello"):
            from pulse.brain.pulse_boot import get_boot_briefing_cached
            result = get_boot_briefing_cached("gemini", "fast")
            self.assertIsInstance(result, str)

    def test_cache_module_level_dict_exists(self):
        """_BOOT_CACHE and _BOOT_CACHE_TTL are defined at module level."""
        import pulse.brain.pulse_boot as pb
        self.assertIsInstance(pb._BOOT_CACHE, dict)
        self.assertEqual(pb._BOOT_CACHE_TTL, 300)


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