#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Tests for P8 neuroscience refinements (Items 21-25)."""
import sys, time, unittest
from pathlib import Path
from unittest.mock import MagicMock, patch

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


def _build_entry(description="test knowledge item", source="agent_x",
                 domain="coding", confidence=0.6, salience=1.5,
                 knowledge_type="lesson", provenance=None):
    """Helper: build a JSONL entry with all stamps mocked out."""
    from pulse.brain.save_writers import jsonl_store as js
    with patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, *a, **k: e), \
         patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e), \
         patch("pulse.brain.metacognition.metamemory.get_metamemory", return_value={}):
        return js.build_jsonl_entry(
            description=description, source=source,
            timestamp="2026-01-01T00:00:00Z", domain=domain,
            confidence=confidence, salience=salience,
            knowledge_type=knowledge_type, provenance=provenance,
        )


# ---------------------------------------------------------------------------
# Item 21: Generation effect
# ---------------------------------------------------------------------------
class TestGenerationEffect(unittest.TestCase):
    """Item 21: self-generated items get salience +0.3 boost."""

    def test_self_generated_flag_in_provenance(self):
        """Matching saving_agent in provenance triggers generation effect."""
        entry = _build_entry(source="agent_x", provenance={"saving_agent": "agent_x"})
        self.assertTrue(entry.get("_generation_effect"), "Expected _generation_effect flag")
        self.assertGreaterEqual(entry["salience"], 1.5 + 0.3)

    def test_source_contains_self(self):
        """Source containing 'self' triggers generation effect."""
        entry = _build_entry(source="self_generated_knowledge")
        self.assertTrue(entry.get("_generation_effect"))
        self.assertGreaterEqual(entry["salience"], 1.5 + 0.3)

    def test_source_contains_generated(self):
        """Source containing 'generated' triggers generation effect."""
        entry = _build_entry(source="llm_generated")
        self.assertTrue(entry.get("_generation_effect"))
        self.assertGreaterEqual(entry["salience"], 1.5 + 0.3)

    def test_external_source_no_boost(self):
        """External source does not trigger generation effect."""
        entry = _build_entry(source="external_feed", provenance={"saving_agent": "other_agent"})
        self.assertFalse(entry.get("_generation_effect", False))
        self.assertAlmostEqual(entry["salience"], 1.5, places=2)

    def test_mismatched_agent_no_boost(self):
        """Different saving_agent and source = no boost."""
        entry = _build_entry(source="agent_x", provenance={"saving_agent": "agent_y"})
        self.assertFalse(entry.get("_generation_effect", False))

    def test_no_provenance_no_crash(self):
        """Missing provenance should not crash."""
        entry = _build_entry(source="regular_source", provenance=None)
        self.assertIn("salience", entry)  # just check it ran


# ---------------------------------------------------------------------------
# Item 22: Near-miss detection
# ---------------------------------------------------------------------------
class TestNearMissDetection(unittest.TestCase):
    """Item 22: task success with low-confidence brain items flagged as near-miss."""

    def _finalize(self, brain_items_used, outcome="Task done"):
        """Run finalize_task over an in-memory board and return the task dict."""
        import json, tempfile
        from pulse.tasks.task_claiming import finalize_task
        task = {
            "task_id": "T-test-001",
            "status": "active",
            "brain_items_used": brain_items_used,
        }
        board = {"dispatches": [{"tasks": [task]}]}
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".json", delete=False, encoding="utf-8"
        ) as f:
            json.dump(board, f)
            tmp_path = Path(f.name)
        try:
            finalize_task(tmp_path, "T-test-001", "worker_1",
                          outcome=outcome, status="done")
            updated = json.loads(tmp_path.read_text(encoding="utf-8"))
            tasks = updated["dispatches"][0]["tasks"]
            return tasks[0] if tasks else {}
        finally:
            tmp_path.unlink(missing_ok=True)

    def test_low_confidence_flagged_as_near_miss(self):
        """Task with avg brain confidence < 0.5 is flagged near_miss=True."""
        items = [{"confidence": 0.2}, {"confidence": 0.3}]
        result = self._finalize(items)
        self.assertTrue(result.get("near_miss"), f"Expected near_miss=True, got {result}")

    def test_near_miss_avg_confidence_stored(self):
        """near_miss_avg_confidence should be the mean of the brain item confidences."""
        items = [{"confidence": 0.2}, {"confidence": 0.4}]
        result = self._finalize(items)
        self.assertIn("near_miss_avg_confidence", result)
        self.assertAlmostEqual(result["near_miss_avg_confidence"], 0.3, places=2)

    def test_high_confidence_no_near_miss(self):
        """Task with avg brain confidence >= 0.5 should NOT be flagged near_miss."""
        items = [{"confidence": 0.7}, {"confidence": 0.8}]
        result = self._finalize(items)
        self.assertFalse(result.get("near_miss", False))

    def test_empty_brain_items_no_crash(self):
        """No brain items used should not crash (defaults to avg 0.5)."""
        result = self._finalize([])
        self.assertEqual(result.get("status"), "done")

    def test_poison_pill_not_flagged(self):
        """POISON_PILL outcomes should not be flagged as near-miss (they're failures)."""
        items = [{"confidence": 0.1}]
        result = self._finalize(items, outcome="POISON_PILL: force-closed")
        # near_miss logic only runs for clean done tasks
        self.assertFalse(result.get("near_miss", False))


# ---------------------------------------------------------------------------
# Item 23: Attention fatigue
# ---------------------------------------------------------------------------
class TestAttentionFatigue(unittest.TestCase):
    """Item 23: sources queried 50+ times get 20% confidence penalty."""

    def setUp(self):
        from pulse.brain.brain_search import search_engine as se
        self._se = se
        # Reset counts before each test
        se._SOURCE_QUERY_COUNTS.clear()

    def tearDown(self):
        self._se._SOURCE_QUERY_COUNTS.clear()

    def test_counter_increments_on_search(self):
        """_SOURCE_QUERY_COUNTS should increment when futures complete."""
        se = self._se
        se._SOURCE_QUERY_COUNTS["wins"] = 10
        se._SOURCE_QUERY_COUNTS["wins"] += 1
        self.assertEqual(se._SOURCE_QUERY_COUNTS["wins"], 11)

    def test_fatigue_penalty_applied_above_threshold(self):
        """Confidence is reduced by 20% when count exceeds 50."""
        se = self._se
        se._SOURCE_QUERY_COUNTS["test_src"] = 51
        h = {"confidence": 0.8, "content": "test", "type": "lesson"}
        # Simulate the fatigue check
        if se._SOURCE_QUERY_COUNTS.get("test_src", 0) > 50:
            h["confidence"] = h.get("confidence", 0.5) * 0.8
            h["_attention_fatigue"] = True
        self.assertAlmostEqual(h["confidence"], 0.64, places=3)
        self.assertTrue(h["_attention_fatigue"])

    def test_no_penalty_below_threshold(self):
        """No penalty when count <= 50."""
        se = self._se
        se._SOURCE_QUERY_COUNTS["test_src"] = 30
        h = {"confidence": 0.8}
        if se._SOURCE_QUERY_COUNTS.get("test_src", 0) > 50:
            h["confidence"] *= 0.8
            h["_attention_fatigue"] = True
        self.assertAlmostEqual(h["confidence"], 0.8, places=3)
        self.assertFalse(h.get("_attention_fatigue", False))

    def test_counter_dict_exists(self):
        """_SOURCE_QUERY_COUNTS must be a dict at module level."""
        se = self._se
        self.assertIsInstance(se._SOURCE_QUERY_COUNTS, dict)

    def test_counter_initialises_at_zero(self):
        """New source name should default to 0 on get."""
        se = self._se
        self.assertEqual(se._SOURCE_QUERY_COUNTS.get("brand_new_source", 0), 0)


# ---------------------------------------------------------------------------
# Item 24: Eligibility traces
# ---------------------------------------------------------------------------
class TestEligibilityTraces(unittest.TestCase):
    """Item 24: reward-modulated STDP via eligibility traces."""

    def setUp(self):
        import importlib
        import pulse.brain.plasticity.stdp as stdp_mod
        importlib.reload(stdp_mod)
        self._stdp = stdp_mod

    def test_trace_list_exists(self):
        """_ELIGIBILITY_TRACE must be a list at module level."""
        self.assertIsInstance(self._stdp._ELIGIBILITY_TRACE, list)

    def test_trace_constants_defined(self):
        """_MAX_TRACE and _TRACE_WINDOW must be defined."""
        self.assertGreater(self._stdp._MAX_TRACE, 0)
        self.assertGreater(self._stdp._TRACE_WINDOW, 0)

    def test_apply_reward_modulation_callable(self):
        """_apply_reward_modulation must be callable."""
        self.assertTrue(callable(self._stdp._apply_reward_modulation))

    def test_apply_reward_clears_trace(self):
        """_apply_reward_modulation must clear trace after applying."""
        stdp = self._stdp
        stdp._ELIGIBILITY_TRACE.append(("edge-001", 0.05, time.time()))
        stdp._ELIGIBILITY_TRACE.append(("edge-002", 0.03, time.time()))
        # Mock out DB
        with patch("pulse.core.brain_db.get_conn") as mock_conn:
            conn = MagicMock()
            mock_conn.return_value = conn
            stdp._apply_reward_modulation(0.5)
        self.assertEqual(len(stdp._ELIGIBILITY_TRACE), 0)

    def test_negative_reward_skips_updates(self):
        """Negative reward should not strengthen edges (reward > 0 guard)."""
        stdp = self._stdp
        stdp._ELIGIBILITY_TRACE.append(("edge-001", 0.05, time.time()))
        with patch("pulse.core.brain_db.get_conn") as mock_conn:
            conn = MagicMock()
            mock_conn.return_value = conn
            stdp._apply_reward_modulation(-0.5)
            conn.execute.assert_not_called()
        stdp._ELIGIBILITY_TRACE.clear()

    def test_trace_capped_at_max(self):
        """Eligibility trace should not exceed _MAX_TRACE entries."""
        stdp = self._stdp
        stdp._ELIGIBILITY_TRACE.clear()
        # Simulate adding more than _MAX_TRACE entries with the capping logic
        for i in range(stdp._MAX_TRACE + 5):
            stdp._ELIGIBILITY_TRACE.append((f"edge-{i}", 0.01, time.time()))
            if len(stdp._ELIGIBILITY_TRACE) > stdp._MAX_TRACE:
                stdp._ELIGIBILITY_TRACE.pop(0)
        self.assertLessEqual(len(stdp._ELIGIBILITY_TRACE), stdp._MAX_TRACE)
        stdp._ELIGIBILITY_TRACE.clear()

    def test_apply_reward_fail_open(self):
        """_apply_reward_modulation must not raise even if DB is unavailable."""
        stdp = self._stdp
        stdp._ELIGIBILITY_TRACE.append(("edge-bad", 0.05, time.time()))
        with patch("pulse.core.brain_db.get_conn", side_effect=RuntimeError("no db")):
            try:
                stdp._apply_reward_modulation(1.0)
            except Exception as e:
                self.fail(f"Expected fail-open, got exception: {e}")


# ---------------------------------------------------------------------------
# Item 25: Emotional salience boost
# ---------------------------------------------------------------------------
class TestEmotionalSalienceBoost(unittest.TestCase):
    """Item 25: high emotional_intensity boosts salience proportionally."""

    def test_high_emotion_boosts_salience(self):
        """emotional_intensity >= 0.6 should boost salience by emotion * 0.5."""
        entry = _build_entry(salience=1.0, provenance={"emotional_intensity": 0.8})
        self.assertTrue(entry.get("_emotional_salience"), "Expected _emotional_salience flag")
        expected = 1.0 + (0.8 * 0.5)
        self.assertAlmostEqual(entry["salience"], expected, places=2)

    def test_exact_threshold_triggers_boost(self):
        """emotional_intensity == 0.6 (boundary) should trigger boost."""
        entry = _build_entry(salience=1.0, provenance={"emotional_intensity": 0.6})
        self.assertTrue(entry.get("_emotional_salience"))

    def test_low_emotion_no_boost(self):
        """emotional_intensity < 0.6 should NOT boost salience."""
        entry = _build_entry(salience=1.0, provenance={"emotional_intensity": 0.4})
        self.assertFalse(entry.get("_emotional_salience", False))
        self.assertAlmostEqual(entry["salience"], 1.0, places=2)

    def test_zero_emotion_no_boost(self):
        """Zero emotional_intensity should not boost salience."""
        entry = _build_entry(salience=2.0, provenance={})
        self.assertFalse(entry.get("_emotional_salience", False))

    def test_no_provenance_no_crash(self):
        """Missing provenance should not crash."""
        entry = _build_entry(salience=1.5, provenance=None)
        self.assertIn("salience", entry)

    def test_salience_accumulates_with_generation_effect(self):
        """Both generation effect and emotional boost can stack."""
        entry = _build_entry(
            source="agent_x",
            salience=1.0,
            provenance={"saving_agent": "agent_x", "emotional_intensity": 0.8},
        )
        # generation +0.3, emotion +0.4 = 1.7 minimum
        self.assertGreaterEqual(entry["salience"], 1.0 + 0.3 + (0.8 * 0.5))


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