#!/usr/bin/env python3
"""Tests for P6 neuroscience refinements (items 11-15)."""
import json
import sys
import os
import re
import threading
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# Ensure project root is on path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))


# ---------------------------------------------------------------------------
# Item 11: Reconsolidation per-table TTL (memory F45)
# ---------------------------------------------------------------------------
class TestReconsolidationPerTableTTL:
    def test_table_ttl_constant_exists(self):
        from pulse.brain.plasticity.reconsolidation import _TABLE_TTL
        assert isinstance(_TABLE_TTL, dict)
        assert "lessons" in _TABLE_TTL
        assert "patterns" in _TABLE_TTL
        assert "facts" in _TABLE_TTL

    def test_ttl_values_correct(self):
        from pulse.brain.plasticity.reconsolidation import _TABLE_TTL
        assert _TABLE_TTL["lessons"] == 300
        assert _TABLE_TTL["patterns"] == 600
        assert _TABLE_TTL["facts"] == 120

    def test_patterns_ttl_greater_than_lessons(self):
        from pulse.brain.plasticity.reconsolidation import _TABLE_TTL
        assert _TABLE_TTL["patterns"] > _TABLE_TTL["lessons"]

    def test_lessons_ttl_greater_than_facts(self):
        from pulse.brain.plasticity.reconsolidation import _TABLE_TTL
        assert _TABLE_TTL["lessons"] > _TABLE_TTL["facts"]

    def test_get_ttl_helper_exists(self):
        from pulse.brain.plasticity.reconsolidation import _get_ttl
        assert callable(_get_ttl)

    def test_get_ttl_returns_correct_for_known_tables(self):
        from pulse.brain.plasticity.reconsolidation import _get_ttl
        assert _get_ttl("lessons") == 300
        assert _get_ttl("patterns") == 600
        assert _get_ttl("facts") == 120

    def test_get_ttl_default_fallback(self):
        from pulse.brain.plasticity.reconsolidation import _get_ttl, TTL_SECONDS
        assert _get_ttl("unknown_table") == TTL_SECONDS
        assert _get_ttl("") == TTL_SECONDS
        assert _get_ttl("episodes") == TTL_SECONDS

    def test_mark_labile_uses_per_table_ttl(self):
        """mark_labile should use _get_ttl for the expiry calculation."""
        from pulse.brain import reconsolidation
        from datetime import datetime, timezone, timedelta

        with (
            patch.object(reconsolidation, "_load_labile"),
            patch.object(reconsolidation, "_save_labile"),
            patch.object(reconsolidation, "get_conn"),
        ):
            reconsolidation._labile_state = {}
            reconsolidation.mark_labile("test-id-patterns", "patterns")
            entry = reconsolidation._labile_state.get("test-id-patterns")
            assert entry is not None
            expires = datetime.fromisoformat(entry["expires_at"])
            now = datetime.now(timezone.utc)
            delta = (expires - now).total_seconds()
            # patterns TTL=600, allow ±5s for test timing
            assert 590 <= delta <= 610, f"patterns TTL delta={delta}"

    def test_mark_labile_facts_shorter_ttl(self):
        from pulse.brain import reconsolidation
        from datetime import datetime, timezone

        with (
            patch.object(reconsolidation, "_load_labile"),
            patch.object(reconsolidation, "_save_labile"),
            patch.object(reconsolidation, "get_conn"),
        ):
            reconsolidation._labile_state = {}
            reconsolidation.mark_labile("test-id-facts", "facts")
            entry = reconsolidation._labile_state.get("test-id-facts")
            assert entry is not None
            expires = datetime.fromisoformat(entry["expires_at"])
            delta = (expires - datetime.now(timezone.utc)).total_seconds()
            assert 110 <= delta <= 130, f"facts TTL delta={delta}"


# ---------------------------------------------------------------------------
# Item 12: Silent synapse gate (neuroplasticity F10)
# ---------------------------------------------------------------------------
class TestSilentSynapse:
    def test_silent_synapse_constant_exists(self):
        from pulse.brain.plasticity.stdp import _SILENT_SYNAPSE_THRESHOLD
        assert _SILENT_SYNAPSE_THRESHOLD == 3

    def test_new_edge_starts_silent(self):
        """Newly inserted TEMPORAL edges should have confidence=0.0."""
        from pulse.brain import stdp

        fake_conn = MagicMock()
        # Simulate: no existing edge (fetchone returns None)
        fake_conn.execute.return_value.fetchone.return_value = None

        stdp._upsert_temporal_edge(fake_conn, "node_a", "node_b", 0.5)

        # Find the INSERT call
        insert_call = None
        for call in fake_conn.execute.call_args_list:
            sql = call[0][0] if call[0] else ""
            if "INSERT INTO graph_edges" in sql:
                insert_call = call
                break
        assert insert_call is not None, "No INSERT call found"
        args = insert_call[0][1]  # positional SQL params
        # SQL: INSERT INTO graph_edges (from_node, to_node, edge_type='TEMPORAL', confidence, metadata)
        # Tuple is: (from_id, to_id, 0.0, metadata_json) — confidence at index 2
        confidence_param = args[2]
        assert confidence_param == 0.0, f"New edge confidence={confidence_param}, expected 0.0"

    def test_existing_edge_stays_silent_below_threshold(self):
        """Edge with coactivation_count < 3 stays at 0 confidence."""
        from pulse.brain import stdp

        fake_conn = MagicMock()
        existing_meta = json.dumps({"coactivation_count": 1, "natural_confidence": 0.3})
        fake_row = MagicMock()
        fake_row.__getitem__ = lambda s, k: {"id": 1, "confidence": 0.0, "metadata": existing_meta}[k]
        fake_conn.execute.return_value.fetchone.return_value = fake_row

        stdp._upsert_temporal_edge(fake_conn, "node_a", "node_b", 0.08)

        # Should UPDATE but keep confidence at 0.0
        update_calls = [c for c in fake_conn.execute.call_args_list
                        if "UPDATE graph_edges" in (c[0][0] if c[0] else "")]
        assert len(update_calls) > 0
        update_params = update_calls[0][0][1]
        # confidence is first param
        assert float(update_params[0]) == 0.0

    def test_existing_edge_promoted_at_threshold(self):
        """Edge with coactivation_count >= 3 gets real confidence."""
        from pulse.brain import stdp

        fake_conn = MagicMock()
        existing_meta = json.dumps({"coactivation_count": 2, "natural_confidence": 0.3})
        fake_row = MagicMock()
        fake_row.__getitem__ = lambda s, k: {"id": 1, "confidence": 0.0, "metadata": existing_meta}[k]
        fake_conn.execute.return_value.fetchone.return_value = fake_row

        stdp._upsert_temporal_edge(fake_conn, "node_a", "node_b", 0.08)

        update_calls = [c for c in fake_conn.execute.call_args_list
                        if "UPDATE graph_edges" in (c[0][0] if c[0] else "")]
        assert len(update_calls) > 0
        update_params = update_calls[0][0][1]
        # After threshold, confidence should be > 0
        assert float(update_params[0]) > 0.0

    def test_new_edge_metadata_stores_natural_confidence(self):
        """The natural_confidence should be stored in metadata for later promotion."""
        from pulse.brain import stdp

        fake_conn = MagicMock()
        fake_conn.execute.return_value.fetchone.return_value = None

        stdp._upsert_temporal_edge(fake_conn, "node_x", "node_y", 0.42)

        insert_call = None
        for call in fake_conn.execute.call_args_list:
            sql = call[0][0] if call[0] else ""
            if "INSERT INTO graph_edges" in sql:
                insert_call = call
                break
        assert insert_call is not None
        meta_str = insert_call[0][1][3]  # metadata param (index 3: from_id, to_id, confidence, metadata)
        meta = json.loads(meta_str)
        assert "natural_confidence" in meta
        assert meta["natural_confidence"] > 0


# ---------------------------------------------------------------------------
# Item 13: Extinction trace (neuroplasticity F41)
# ---------------------------------------------------------------------------
class TestExtinctionTrace:
    def test_store_extinction_trace_function_exists(self):
        from pulse.admin.audit.truth_verifier import _store_extinction_trace
        assert callable(_store_extinction_trace)

    def test_extinction_trace_inserts_contradicts_edge(self):
        """_store_extinction_trace should INSERT a CONTRADICTS edge."""
        from pulse.admin.audit import truth_verifier

        fake_conn = MagicMock()
        with patch.object(truth_verifier, "_store_extinction_trace",
                          wraps=truth_verifier._store_extinction_trace):
            with patch("pulse.admin.audit.truth_verifier._store_extinction_trace") as mock_fn:
                mock_fn.return_value = None
                truth_verifier._store_extinction_trace("id_a", "id_b", 0.6)

        # Now test directly with mock conn
        with patch("pulse.admin.audit.truth_verifier._store_extinction_trace",
                   truth_verifier._store_extinction_trace):
            with patch("pulse.core.brain_db.get_conn", return_value=fake_conn):
                truth_verifier._store_extinction_trace("id_a", "id_b", 0.6)

        fake_conn.execute.assert_called()
        call_args = fake_conn.execute.call_args[0]
        assert "CONTRADICTS" in call_args[0]
        assert "id_a" in call_args[1]
        assert "id_b" in call_args[1]

    def test_extinction_trace_fail_open(self):
        """If DB raises, _store_extinction_trace must not propagate the exception."""
        from pulse.admin.audit import truth_verifier

        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("DB down")):
            # Must not raise
            truth_verifier._store_extinction_trace("a", "b", 0.5)

    def test_scan_contradictions_calls_extinction_trace(self):
        """scan_contradictions should trigger _store_extinction_trace for new items."""
        from pulse.admin.audit import truth_verifier

        mock_reg = {
            "contradictions": [],
            "stats": {},
            "last_scan": ""
        }
        fake_contradiction = {
            "id": "ctr_abc",
            "type": "prescriptive",
            "item_a": {"id": "item-a-id", "text": "always use X", "confidence": 0.7},
            "item_b": {"id": "item-b-id", "text": "never use X", "confidence": 0.6},
            "similarity": 0.8,
            "detected_at": "2026-01-01T00:00:00",
            "resolution": None,
            "resolved_at": None,
            "resolved_by": None,
        }
        with (
            patch.object(truth_verifier, "_load_registry", return_value=mock_reg),
            patch.object(truth_verifier, "_save_registry"),
            patch.object(truth_verifier, "_scan_factual", return_value=[fake_contradiction]),
            patch.object(truth_verifier, "_scan_prescriptive", return_value=[]),
            patch.object(truth_verifier, "_scan_temporal", return_value=[]),
            patch.object(truth_verifier, "_load_facts", return_value=[]),
            patch.object(truth_verifier, "_load_knowledge", return_value=[]),
            patch.object(truth_verifier, "_store_extinction_trace") as mock_trace,
        ):
            truth_verifier.scan_contradictions()
            mock_trace.assert_called_once()


# ---------------------------------------------------------------------------
# Item 14: Prospective memory triggers (memory F16-17)
# ---------------------------------------------------------------------------
class TestProspectiveMemoryTriggers:
    def test_trigger_regex_constant_exists(self):
        from pulse.brain.save_writers.jsonl_store import _TRIGGER_RE
        assert _TRIGGER_RE is not None

    def test_trigger_regex_matches_when(self):
        from pulse.brain.save_writers.jsonl_store import _TRIGGER_RE
        m = _TRIGGER_RE.search("when the user clicks submit, save the form")
        assert m is not None
        assert "user clicks submit" in m.group(1)

    def test_trigger_regex_matches_if(self):
        from pulse.brain.save_writers.jsonl_store import _TRIGGER_RE
        m = _TRIGGER_RE.search("if the server responds with 500 retry the request")
        assert m is not None

    def test_trigger_regex_matches_before(self):
        from pulse.brain.save_writers.jsonl_store import _TRIGGER_RE
        m = _TRIGGER_RE.search("before deployment check the tests pass")
        assert m is not None

    def test_trigger_regex_matches_after(self):
        from pulse.brain.save_writers.jsonl_store import _TRIGGER_RE
        m = _TRIGGER_RE.search("after the migration completes run validation")
        assert m is not None

    def test_build_jsonl_entry_extracts_trigger_for_intent(self):
        """build_jsonl_entry should add trigger_condition when type=intent."""
        from pulse.brain.save_writers.jsonl_store import build_jsonl_entry
        from unittest.mock import patch

        with (
            patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, t: e),
            patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e),
        ):
            entry = build_jsonl_entry(
                description="when the pipeline completes, notify the user",
                source="test_agent",
                timestamp="2026-01-01T00:00:00Z",
                domain="general",
                confidence=0.7,
                salience=2.0,
                knowledge_type="intent",
            )
        assert "trigger_condition" in entry
        assert len(entry["trigger_condition"]) > 0

    def test_build_jsonl_entry_no_trigger_for_lesson(self):
        """build_jsonl_entry should NOT add trigger_condition when type=lesson."""
        from pulse.brain.save_writers.jsonl_store import build_jsonl_entry

        with (
            patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, t: e),
            patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e),
        ):
            entry = build_jsonl_entry(
                description="when debugging check the logs first",
                source="test_agent",
                timestamp="2026-01-01T00:00:00Z",
                domain="general",
                confidence=0.7,
                salience=2.0,
                knowledge_type="lesson",
            )
        assert "trigger_condition" not in entry

    def test_trigger_extraction_fail_open(self):
        """If trigger regex crashes, entry should still be returned without trigger_condition."""
        from pulse.brain.save_writers import jsonl_store

        original_re = jsonl_store._TRIGGER_RE
        try:
            # Force exception during trigger extraction by monkey-patching
            bad_re = MagicMock()
            bad_re.search.side_effect = Exception("regex crash")
            jsonl_store._TRIGGER_RE = bad_re

            with (
                patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, t: e),
                patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e),
            ):
                entry = jsonl_store.build_jsonl_entry(
                    description="when something happens do the thing",
                    source="test",
                    timestamp="2026-01-01T00:00:00Z",
                    domain="general",
                    confidence=0.5,
                    salience=1.0,
                    knowledge_type="intent",
                )
            assert isinstance(entry, dict)
        finally:
            jsonl_store._TRIGGER_RE = original_re


# ---------------------------------------------------------------------------
# Item 15: Sub-threshold trace store (consciousness F23)
# ---------------------------------------------------------------------------
class TestSubThresholdTrace:
    def _make_fake_conn(self, existing_salience=None, existing_count=None):
        """Build a fake connection that returns given row data."""
        conn = MagicMock()
        conn.__enter__ = lambda s: s
        conn.__exit__ = MagicMock(return_value=False)

        changes_row = MagicMock()
        changes_row.__getitem__ = lambda s, k: 1 if k == 0 else 0

        if existing_salience is not None:
            sub_row = MagicMock()
            sub_row.__getitem__ = lambda s, k: (
                existing_salience if k == "salience" else existing_count
            )
        else:
            sub_row = None

        def execute_side(sql, *args, **kwargs):
            result = MagicMock()
            if "SELECT changes()" in sql:
                result.fetchone.return_value = changes_row
            elif "SELECT salience" in sql:
                result.fetchone.return_value = sub_row
            else:
                result.fetchone.return_value = None
            return result

        conn.execute = MagicMock(side_effect=execute_side)
        return conn

    def test_promotion_at_count_gte_3_low_salience(self):
        """Item with salience=0.2 and count=3 should be promoted to 1.0."""
        from pulse.core import brain_db

        fake_conn = self._make_fake_conn(existing_salience=0.2, existing_count=3)
        with (
            patch.object(brain_db, "get_conn", return_value=fake_conn),
            patch("pulse.core.brain_utils._make_fingerprint", return_value="fp_test"),
        ):
            try:
                brain_db.insert_knowledge("lessons", {
                    "content": "test sub-threshold promotion",
                    "salience": 0.2,
                    "confidence": 0.4,
                    "consolidation_count": 3,
                })
            except Exception:
                pass  # DB quirks ok in test — we just check UPDATE was called

        update_calls = [c for c in fake_conn.execute.call_args_list
                        if "UPDATE" in (c[0][0] if c[0] else "")
                        and "salience = 1.0" in (c[0][0] if c[0] else "")]
        assert len(update_calls) > 0, "Expected salience UPDATE to 1.0"

    def test_no_promotion_high_salience(self):
        """Item with salience > 0.4 should NOT be promoted."""
        from pulse.core import brain_db

        fake_conn = self._make_fake_conn(existing_salience=0.8, existing_count=5)
        with (
            patch.object(brain_db, "get_conn", return_value=fake_conn),
            patch("pulse.core.brain_utils._make_fingerprint", return_value="fp_hi"),
        ):
            try:
                brain_db.insert_knowledge("lessons", {
                    "content": "test high salience no promotion",
                    "salience": 0.8,
                    "confidence": 0.7,
                    "consolidation_count": 5,
                })
            except Exception:
                pass

        update_calls = [c for c in fake_conn.execute.call_args_list
                        if "salience = 1.0" in (c[0][0] if c[0] else "")]
        assert len(update_calls) == 0, "Should not promote high-salience item"

    def test_no_promotion_below_count_threshold(self):
        """Item appearing only 2 times should not be promoted."""
        from pulse.core import brain_db

        fake_conn = self._make_fake_conn(existing_salience=0.2, existing_count=2)
        with (
            patch.object(brain_db, "get_conn", return_value=fake_conn),
            patch("pulse.core.brain_utils._make_fingerprint", return_value="fp_low_count"),
        ):
            try:
                brain_db.insert_knowledge("lessons", {
                    "content": "test low count no promotion",
                    "salience": 0.2,
                    "confidence": 0.4,
                    "consolidation_count": 2,
                })
            except Exception:
                pass

        update_calls = [c for c in fake_conn.execute.call_args_list
                        if "salience = 1.0" in (c[0][0] if c[0] else "")]
        assert len(update_calls) == 0

    def test_sub_threshold_fail_open(self):
        """If sub-threshold SELECT throws, insert_knowledge must not raise."""
        from pulse.core import brain_db

        conn = MagicMock()
        conn.__enter__ = lambda s: s
        conn.__exit__ = MagicMock(return_value=False)

        changes_row = MagicMock()
        changes_row.__getitem__ = lambda s, k: 1

        def exploding_execute(sql, *args, **kwargs):
            result = MagicMock()
            if "SELECT changes()" in sql:
                result.fetchone.return_value = changes_row
            elif "SELECT salience" in sql:
                raise Exception("DB exploded")
            else:
                result.fetchone.return_value = None
            return result

        conn.execute = MagicMock(side_effect=exploding_execute)

        with (
            patch.object(brain_db, "get_conn", return_value=conn),
            patch("pulse.core.brain_utils._make_fingerprint", return_value="fp_err"),
        ):
            # Must not raise
            try:
                brain_db.insert_knowledge("lessons", {
                    "content": "test fail open sub-threshold",
                    "salience": 0.2,
                })
            except Exception as e:
                # Only DB structural errors are ok, not our new code raising
                assert "salience" not in str(e)
