"""P12 Neuroscience Refinements — source-level verification tests.

Tests verify that each P12 item is correctly wired by inspecting source
and exercising pure-Python logic. No live brain DB required (CI-safe).

Items:
  41 - Triplet STDP (3-node motif capture)
  42 - Working memory capacity enforcement (max 3 concurrent claims)
  43 - Somatic marker retroactive update on task outcome
  44 - Schema distortion audit on consolidation
  45 - Depotentiation vs LTD distinction
"""
import inspect
import json
import sys
import time
import types
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 41: Triplet STDP — 3-node motif capture
# ---------------------------------------------------------------------------

class TestTripletSTDP(unittest.TestCase):

    def _get_src(self):
        import pulse.brain.plasticity.stdp as mod
        return inspect.getsource(mod), mod

    def test_triplet_sql_present(self):
        """apply_stdp must query for CAUSAL edges on the to_node (triplet shortcut)."""
        src, _ = self._get_src()
        assert "CAUSAL" in src, "apply_stdp missing CAUSAL edge type for triplet"
        assert "INSERT OR IGNORE INTO graph_edges" in src, \
            "Triplet shortcut INSERT missing in apply_stdp"

    def test_triplet_no_self_loops(self):
        """Triplet shortcut must guard against self-loops (c_node != from_node)."""
        src, _ = self._get_src()
        assert "!= _pair_pre" in src or "!= from_node" in src, \
            "No self-loop guard in triplet STDP"

    def test_triplet_confidence_value(self):
        """Triplet shortcuts must be created at exactly 0.3 confidence."""
        src, _ = self._get_src()
        assert "0.3" in src, "Triplet shortcut confidence 0.3 not found"

    def test_triplet_wrapped_in_try_except(self):
        """Triplet STDP logic must be fail-open (try/except)."""
        src, _ = self._get_src()
        # The triplet block must have a try and an except
        # Count occurrences near the triplet comment
        idx = src.find("Item 41")
        assert idx != -1, "Item 41 marker not found in stdp.py"
        snippet = src[idx:idx + 1200]
        assert "try:" in snippet, "Triplet STDP not wrapped in try block"
        assert "except Exception:" in snippet, "Triplet STDP missing except Exception"

    def test_triplet_limit_on_shortcuts(self):
        """Shortcut query must use LIMIT to prevent runaway expansion."""
        src, _ = self._get_src()
        assert "LIMIT" in src, "No LIMIT on triplet shortcut query"

    def test_triplet_causal_edge_type_used(self):
        """Inserted shortcut edge must be of type CAUSAL."""
        src, _ = self._get_src()
        assert "'CAUSAL', 0.3" in src or "\"CAUSAL\", 0.3" in src, \
            "Triplet shortcut not inserted with edge_type='CAUSAL'"


# ---------------------------------------------------------------------------
# Item 42: Working memory capacity enforcement
# ---------------------------------------------------------------------------

class TestWorkingMemoryCapacity(unittest.TestCase):

    def _make_board(self, claimed_count, agent="worker_1"):
        """Create a board with N active tasks for the agent."""
        tasks = [
            {"task_id": f"t{i}", "status": "active", "claimed_by": agent,
             "recommended_tier": "standard"}
            for i in range(claimed_count)
        ]
        tasks.append({
            "task_id": "new_task", "status": "open", "claimed_by": None,
            "recommended_tier": "standard",
        })
        return {"dispatches": [{"tasks": tasks}]}

    def _src(self):
        import pulse.tasks.task_claiming as mod
        return inspect.getsource(mod), mod

    def test_capacity_check_present(self):
        """claim_task must count active claims before allowing a new claim."""
        src, _ = self._src()
        assert "working_memory_capacity" in src, \
            "Working memory capacity rejection reason not found"

    def test_capacity_rejection_at_3(self):
        """The rejection threshold must be exactly 3."""
        src, _ = self._src()
        assert ">= 3" in src, "Capacity threshold != 3 in task_claiming.py"

    def test_capacity_fail_open_comment(self):
        """Capacity check must have fail-open (try/except pass)."""
        src, _ = self._src()
        idx = src.find("working_memory_capacity")
        assert idx != -1
        # There must be a try block before the rejection
        snippet = src[max(0, idx - 400):idx + 200]
        assert "try:" in snippet, "Capacity check not inside try block"
        assert "except Exception:" in snippet, "Capacity check missing except"

    def test_rejection_returns_status_dict(self):
        """Rejection must set claimed[0] with status='rejected'."""
        src, _ = self._src()
        assert "\"rejected\"" in src or "'rejected'" in src, \
            "Rejection status value missing"

    def test_counts_active_status(self):
        """Capacity check must count tasks with status 'active' or 'claimed'."""
        src, _ = self._src()
        # Either 'active' or both 'active' and 'claimed' must be checked
        assert "\"active\"" in src or "'active'" in src, \
            "Capacity check does not look for 'active' tasks"

    def test_capacity_check_uses_agent_id(self):
        """Capacity check must filter by agent_id."""
        src, _ = self._src()
        idx = src.find("working_memory_capacity")
        snippet = src[max(0, idx - 500):idx + 100]
        assert "agent_id" in snippet, "Capacity check does not filter by agent_id"


# ---------------------------------------------------------------------------
# Item 43: Somatic marker retroactive update
# ---------------------------------------------------------------------------

class TestSomaticMarker(unittest.TestCase):

    def _src(self):
        import pulse.core.reward.reward_signal as mod
        return inspect.getsource(mod.propagate_reward), mod

    def test_somatic_marker_present(self):
        """propagate_reward must contain Item 43 somatic marker code."""
        src, _ = self._src()
        assert "Item 43" in src or "Somatic marker" in src, \
            "Item 43 somatic marker not found in propagate_reward"

    def test_positive_delta_is_point2(self):
        """Positive outcome delta must be 0.2."""
        src, _ = self._src()
        assert "0.2" in src, "Somatic marker positive delta 0.2 not found"

    def test_negative_delta_is_point3(self):
        """Failure delta must be 0.3 (more memorable)."""
        src, _ = self._src()
        assert "0.3" in src, "Somatic marker failure delta 0.3 not found"

    def test_updates_all_three_tables(self):
        """Somatic marker must update lessons, failures, and patterns."""
        src, _ = self._src()
        assert "lessons" in src, "Somatic marker misses lessons table"
        assert "failures" in src, "Somatic marker misses failures table"
        assert "patterns" in src, "Somatic marker misses patterns table"

    def test_salience_capped_at_10(self):
        """Salience update must be capped (audit fix P21: unified to 7.0)."""
        src, _ = self._src()
        assert "7.0" in src or "MIN(7.0" in src, \
            "Somatic marker salience cap 7.0 not found"

    def test_somatic_fail_open(self):
        """Somatic marker must be fail-open."""
        src, _ = self._src()
        idx = src.find("Somatic marker")
        assert idx != -1
        snippet = src[idx:idx + 500]
        assert "except Exception:" in snippet, "Somatic marker not fail-open"

    def test_propagate_reward_returns_int(self):
        """propagate_reward must return an integer count."""
        import pulse.core.reward.reward_signal as mod
        result = mod.propagate_reward({})  # empty credits — should return 0 fast
        assert isinstance(result, int), f"Expected int, got {type(result)}"


# ---------------------------------------------------------------------------
# Item 44: Schema distortion audit
# ---------------------------------------------------------------------------

class TestSchemaDistortionAudit(unittest.TestCase):

    def _src(self):
        return open("pulse/daemons/consolidation/stages/stage_mining.py", encoding="utf-8").read()

    def test_function_exists(self):
        assert "stage_schema_distortion_audit" in self._src()

    def test_returns_dict(self):
        src = self._src()
        assert "return" in src and "flagged" in src

    def test_result_has_flagged_key(self):
        assert "flagged" in self._src()

    def test_result_has_status_key(self):
        assert "status" in self._src()

    def test_distortion_threshold(self):
        assert "0.3" in self._src()

    def test_distortion_uses_abs_check(self):
        assert "ABS" in self._src()

    def test_fail_open_on_no_db(self):
        assert "except" in self._src()

class TestDepotentiationVsLTD(unittest.TestCase):

    def _src(self):
        import pulse.brain.plasticity.stdp as mod
        return inspect.getsource(mod._upsert_temporal_edge), mod

    def test_depotentiation_marker_present(self):
        """_upsert_temporal_edge must contain Item 45 marker."""
        src, _ = self._src()
        assert "Item 45" in src or "Depotentiation" in src, \
            "Item 45 depotentiation/LTD logic not found in _upsert_temporal_edge"

    def test_ltd_formula_present(self):
        """LTD path must use confidence * 0.7."""
        src, _ = self._src()
        assert "0.7" in src, "LTD reduction factor 0.7 not found"

    def test_depotentiation_sets_zero(self):
        """Depotentiation path must set confidence to 0.0."""
        src, _ = self._src()
        assert "= 0.0" in src, "Depotentiation new_conf = 0.0 not found"

    def test_potentiation_tracks_timestamp(self):
        """Positive updates must record last_potentiated timestamp."""
        src, _ = self._src()
        assert "last_potentiated" in src, \
            "last_potentiated timestamp not tracked on potentiation"

    def test_five_minute_window(self):
        """Depotentiation window must be 5 minutes (300 seconds)."""
        src, _ = self._src()
        assert "300" in src, "5-minute window (300s) not found for depotentiation"

    def test_depotentiation_fail_open(self):
        """Depotentiation path must have except fallback to LTD."""
        src, _ = self._src()
        idx = src.find("Item 45")
        if idx == -1:
            idx = src.find("Depotentiation")
        assert idx != -1
        snippet = src[idx:idx + 700]
        assert "except Exception:" in snippet, \
            "Depotentiation block missing except Exception fallback"

    def test_only_weakening_path_has_depotentiation(self):
        """Depotentiation must only apply when delta < 0 (weakening)."""
        src, _ = self._src()
        idx = src.find("delta < 0")
        assert idx != -1, "delta < 0 guard not found for depotentiation path"


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