#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Integration tests for P28 neuroscience refinements (Items 116-120)."""
import sqlite3
import sys
import unittest
from datetime import datetime, timezone, timedelta
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 116: Memory consolidation priority queue
# ---------------------------------------------------------------------------
class TestConsolidationPriorityQueue(unittest.TestCase):
    """Item 116 — build_consolidation_priority_queue sorts by salience>confidence>timestamp."""

    def _make_items(self):
        return [
            {"id": "A", "salience": 1.0, "confidence": 0.6, "timestamp": "2026-01-01T00:00:00Z"},
            {"id": "B", "salience": 3.0, "confidence": 0.9, "timestamp": "2026-01-02T00:00:00Z"},
            {"id": "C", "salience": 3.0, "confidence": 0.5, "timestamp": "2026-01-03T00:00:00Z"},
            {"id": "D", "salience": 2.0, "confidence": 0.8, "timestamp": "2026-01-04T00:00:00Z"},
        ]

    def test_highest_salience_first(self):
        from pulse.daemons.consolidation.stages.stage_priority_queue import (
            build_consolidation_priority_queue,
        )
        items = self._make_items()
        ordered = build_consolidation_priority_queue(items)
        # B and C have salience=3.0, should come before D (2.0) and A (1.0)
        top_two_ids = {ordered[0]["id"], ordered[1]["id"]}
        self.assertIn("B", top_two_ids)
        self.assertIn("C", top_two_ids)

    def test_confidence_tiebreak_within_same_salience(self):
        from pulse.daemons.consolidation.stages.stage_priority_queue import (
            build_consolidation_priority_queue,
        )
        items = self._make_items()
        ordered = build_consolidation_priority_queue(items)
        # Among B (conf=0.9) and C (conf=0.5), B should come first
        first_id = ordered[0]["id"]
        self.assertEqual(first_id, "B")

    def test_empty_list_returns_empty(self):
        from pulse.daemons.consolidation.stages.stage_priority_queue import (
            build_consolidation_priority_queue,
        )
        self.assertEqual(build_consolidation_priority_queue([]), [])

    def test_stage_wrapper_with_explicit_items(self):
        from pulse.daemons.consolidation.stages.stage_priority_queue import (
            stage_consolidation_priority_queue,
        )
        items = self._make_items()
        result = stage_consolidation_priority_queue(items=items, verbose=False)
        self.assertEqual(result["status"], "ok")
        self.assertEqual(result["total"], 4)
        self.assertEqual(len(result["ordered_items"]), 4)

    def test_stage_wrapper_db_fallback_on_error(self):
        from pulse.daemons.consolidation.stages.stage_priority_queue import (
            stage_consolidation_priority_queue,
        )
        # get_conn is imported lazily inside the function — patch at the brain_db level
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("DB unavailable")):
            result = stage_consolidation_priority_queue(items=None, verbose=False)
            # Should fail gracefully
            self.assertIn(result["status"], ("error", "ok"))

    def test_re_exported_from_stage_maintenance(self):
        """Integration: Item 116 is re-exported through stage_maintenance."""
        from pulse.daemons.consolidation.stages.stage_maintenance import (
            stage_consolidation_priority_queue,
            build_consolidation_priority_queue,
        )
        items = [{"salience": 2.0, "confidence": 0.7, "timestamp": "2026-01-01T00:00:00Z"}]
        ordered = build_consolidation_priority_queue(items)
        self.assertEqual(len(ordered), 1)


# ---------------------------------------------------------------------------
# Item 117: Prediction market for agent selection
# ---------------------------------------------------------------------------
class TestPredictionMarketAgentSelection(unittest.TestCase):
    """Item 117 — dispatcher biases agent selection by domain track record."""

    def _make_task(self, domain="python"):
        return {"title": "Test task", "domain": domain, "task_type": "feature"}

    def test_domain_recommended_agent_set_when_specialization_available(self):
        """If agent specialization data exists, _domain_recommended_agent should be set."""
        spec = {
            "claude": {"python": {"tasks": 10, "successes": 9, "success_rate": 0.9}},
            "gemini": {"python": {"tasks": 10, "successes": 5, "success_rate": 0.5}},
        }
        with patch("pulse.brain.metacognition.metamemory.get_agent_specialization", return_value=spec):
            # Simulate the dispatcher code inline
            domain = "python"
            candidates = ["claude", "gemini"]
            domain_scores = {}
            for agent in candidates:
                agent_spec = spec.get(agent, {})
                domain_entry = agent_spec.get(domain, {})
                domain_scores[agent] = domain_entry.get("success_rate", 0.5)
            best = max(domain_scores, key=domain_scores.get)
            self.assertEqual(best, "claude")

    def test_fallback_when_no_specialization(self):
        """If specialization is empty, code should fail-open without error."""
        with patch("pulse.brain.metacognition.metamemory.get_agent_specialization", return_value={}):
            task = self._make_task()
            # Simulate dispatcher logic
            domain = task["domain"]
            spec = {}
            domain_scores = {}
            for agent in ["claude", "gemini"]:
                agent_spec = spec.get(agent, {})
                domain_entry = agent_spec.get(domain, {})
                domain_scores[agent] = domain_entry.get("success_rate", 0.5)
            # With no specialization data, all agents score 0.5
            self.assertTrue(all(v == 0.5 for v in domain_scores.values()))

    def test_record_agent_domain_updates_specialization(self):
        """Integration: _record_agent_domain feeds _AGENT_SPECIALIZATION (may be in metamemory_tracking)."""
        # The linter may have extracted these to metamemory_tracking — try both locations
        try:
            from pulse.brain.metacognition.metamemory_tracking import _record_agent_domain, _AGENT_SPECIALIZATION
        except ImportError:
            from pulse.brain.metacognition.metamemory import _record_agent_domain, _AGENT_SPECIALIZATION
        _AGENT_SPECIALIZATION.clear()
        _record_agent_domain("test_agent", "python", success=True)
        _record_agent_domain("test_agent", "python", success=False)
        spec = _AGENT_SPECIALIZATION.get("test_agent", {}).get("python", {})
        self.assertEqual(spec["tasks"], 2)
        self.assertEqual(spec["successes"], 1)

    def test_get_agent_specialization_returns_success_rates(self):
        try:
            from pulse.brain.metacognition.metamemory_tracking import (
                _record_agent_domain, _AGENT_SPECIALIZATION, get_agent_specialization,
            )
        except ImportError:
            from pulse.brain.metacognition.metamemory import (
                _record_agent_domain, _AGENT_SPECIALIZATION, get_agent_specialization,
            )
        _AGENT_SPECIALIZATION.clear()
        _record_agent_domain("agent_x", "security", success=True)
        _record_agent_domain("agent_x", "security", success=True)
        _record_agent_domain("agent_x", "security", success=False)
        result = get_agent_specialization()
        rate = result["agent_x"]["security"]["success_rate"]
        self.assertAlmostEqual(rate, 2 / 3, places=2)

    def test_dispatcher_dispatch_does_not_crash(self):
        """Integration: TaskDispatcher.dispatch runs without crashing on Item 117 code."""
        import json
        import tempfile
        import os
        from pulse.tasks.pulse_dispatcher import TaskDispatcher

        with tempfile.TemporaryDirectory() as tmpdir:
            board_path = Path(tmpdir) / "task_board.json"
            board_path.write_text('{"dispatches": []}', encoding="utf-8")
            with patch("pulse.tasks.pulse_dispatcher.TASK_BOARD_FILE", board_path), \
                 patch("pulse.tasks.pulse_dispatcher.atomic_update_json") as mock_au:
                mock_au.return_value = None
                td = TaskDispatcher(verbose=False)
                dispatch_id = td.dispatch("test prompt", [{"title": "t", "domain": "python"}])
                self.assertTrue(dispatch_id.startswith("d_"))


# ---------------------------------------------------------------------------
# Item 118: Contextual inhibition
# ---------------------------------------------------------------------------
class TestContextualInhibition(unittest.TestCase):
    """Item 118 — unrelated-domain results penalized by 20% when query domain is clear."""

    def test_unrelated_domain_penalized(self):
        from pulse.brain.brain_search.search_scoring_ext import apply_contextual_inhibition
        results = {
            "wins": [
                {"domain": "security", "confidence": 0.8, "text": "auth token"},
                {"domain": "testing", "confidence": 0.8, "text": "unit test"},
            ]
        }
        # Query clearly about deployment
        apply_contextual_inhibition(results, "deploy docker kubernetes production release")
        sec_item = results["wins"][0]
        test_item = results["wins"][1]
        # Both should be penalized if their domain != deployment
        if sec_item.get("_contextual_inhibition"):
            self.assertLess(sec_item["confidence"], 0.8)

    def test_same_domain_not_penalized(self):
        from pulse.brain.brain_search.search_scoring_ext import apply_contextual_inhibition
        results = {
            "wins": [{"domain": "security", "confidence": 0.8, "text": "auth token vuln"}],
        }
        # Query clearly about security
        apply_contextual_inhibition(results, "security auth exploit vulnerability token")
        item = results["wins"][0]
        self.assertFalse(item.get("_contextual_inhibition"))

    def test_ambiguous_query_no_inhibition(self):
        from pulse.brain.brain_search.search_scoring_ext import (
            apply_contextual_inhibition, infer_query_domain,
        )
        # Ambiguous query — no clear domain
        domain = infer_query_domain("help me with something")
        self.assertIsNone(domain)
        results = {
            "wins": [{"domain": "security", "confidence": 0.8, "text": "info"}],
        }
        apply_contextual_inhibition(results, "help me with something")
        self.assertFalse(results["wins"][0].get("_contextual_inhibition"))

    def test_infer_query_domain_security(self):
        from pulse.brain.brain_search.search_scoring_ext import infer_query_domain
        self.assertEqual(infer_query_domain("security auth vulnerability exploit"), "security")

    def test_inhibition_called_in_apply_post_scoring(self):
        """Integration: apply_contextual_inhibition is wired into _apply_post_scoring."""
        with patch("pulse.brain.brain_search.search_scoring.apply_contextual_inhibition") as mock_ci:
            from pulse.brain.brain_search.search_scoring import _apply_post_scoring
            results = {"wins": [{"text": "t", "confidence": 0.6}]}
            _apply_post_scoring(results, "security auth exploit", "general")
            mock_ci.assert_called_once_with(results, "security auth exploit")


# ---------------------------------------------------------------------------
# Item 119: Memory strength vs accessibility distinction
# ---------------------------------------------------------------------------
class TestMemoryStrengthVsAccessibility(unittest.TestCase):
    """Item 119 — high evidence_count + low access_count = latent knowledge flag."""

    def test_latent_knowledge_flagged(self):
        from pulse.brain.brain_search.search_scoring_ext import apply_latent_knowledge_flag
        item = {"evidence_count": 10, "access_count": 1, "confidence": 0.6, "text": "deep knowledge"}
        result = apply_latent_knowledge_flag(item)
        self.assertTrue(result.get("_latent_knowledge"))
        self.assertGreater(result["confidence"], 0.6)

    def test_high_access_not_flagged(self):
        from pulse.brain.brain_search.search_scoring_ext import apply_latent_knowledge_flag
        item = {"evidence_count": 10, "access_count": 20, "confidence": 0.7, "text": "popular item"}
        result = apply_latent_knowledge_flag(item)
        self.assertFalse(result.get("_latent_knowledge"))

    def test_low_evidence_not_flagged(self):
        from pulse.brain.brain_search.search_scoring_ext import apply_latent_knowledge_flag
        item = {"evidence_count": 2, "access_count": 1, "confidence": 0.5, "text": "weak item"}
        result = apply_latent_knowledge_flag(item)
        self.assertFalse(result.get("_latent_knowledge"))

    def test_confidence_boosted_for_latent(self):
        from pulse.brain.brain_search.search_scoring_ext import apply_latent_knowledge_flag
        item = {"evidence_count": 8, "access_count": 0, "confidence": 0.5}
        result = apply_latent_knowledge_flag(item)
        self.assertGreaterEqual(result["confidence"], 0.55)

    def test_latent_flag_called_in_score_item(self):
        """Integration: apply_latent_knowledge_flag is wired into _score_item."""
        with patch("pulse.brain.brain_search.search_scoring.apply_latent_knowledge_flag") as mock_lf:
            mock_lf.side_effect = lambda item: item
            from pulse.brain.brain_search.search_scoring import _score_item
            item = {
                "confidence": 0.6, "text": "test", "validity": "valid",
                "evidence_count": 10, "access_count": 0,
            }
            with patch("pulse.brain.brain_search.search_scoring.get_provenance", return_value=None), \
                 patch("pulse.brain.brain_search.search_scoring.text_similarity", return_value=0.0):
                from pulse.core.temporal import temporal_confidence, temporal_salience
                from pulse.core.uncertainty import wilson_interval
                _score_item(item, "wins", "query", "feature", set(), None, {}, {},
                            temporal_confidence, temporal_salience, wilson_interval)
            mock_lf.assert_called_once()


# ---------------------------------------------------------------------------
# Item 120: Prospective memory reminder
# ---------------------------------------------------------------------------
class TestProspectiveMemoryReminder(unittest.TestCase):
    """Item 120 — intents with matching trigger_condition surface as REMINDER."""

    def _make_conn_with_intents(self, intents):
        conn = sqlite3.connect(":memory:")
        conn.row_factory = sqlite3.Row
        conn.execute(
            "CREATE TABLE intents (id TEXT, title TEXT, content TEXT, trigger_condition TEXT)"
        )
        for intent in intents:
            conn.execute(
                "INSERT INTO intents VALUES (?, ?, ?, ?)",
                (intent.get("id", "i1"), intent.get("title", ""),
                 intent.get("content", ""), intent.get("trigger_condition", ""))
            )
        conn.commit()
        return conn

    def test_matching_trigger_surfaces_reminder(self):
        from pulse.brain.brain_query.briefing_ext import check_prospective_memory
        conn = self._make_conn_with_intents([
            {"id": "i1", "title": "Run security audit", "content": "audit the module",
             "trigger_condition": "security audit review"}
        ])
        # get_conn is imported lazily inside the function — patch at brain_db level
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            reminders = check_prospective_memory("security audit review requested")
        self.assertTrue(any("REMINDER" in r for r in reminders))

    def test_non_matching_trigger_no_reminder(self):
        from pulse.brain.brain_query.briefing_ext import check_prospective_memory
        conn = self._make_conn_with_intents([
            {"id": "i1", "title": "Deploy to prod", "content": "deploy stuff",
             "trigger_condition": "deploy production"}
        ])
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            reminders = check_prospective_memory("unit test pytest coverage")
        self.assertEqual(len(reminders), 0)

    def test_no_intents_returns_empty(self):
        from pulse.brain.brain_query.briefing_ext import check_prospective_memory
        conn = self._make_conn_with_intents([])
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            reminders = check_prospective_memory("security audit token validation")
        self.assertEqual(reminders, [])

    def test_db_error_returns_empty(self):
        """Integration: check_prospective_memory fails open on DB error."""
        from pulse.brain.brain_query.briefing_ext import check_prospective_memory
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("no DB")):
            reminders = check_prospective_memory("anything")
        self.assertEqual(reminders, [])

    def test_prospective_memory_called_in_format_briefing(self):
        """Integration: check_prospective_memory is wired into format_briefing."""
        with patch("pulse.brain.brain_query.briefing.check_prospective_memory") as mock_pm:
            mock_pm.return_value = []
            from pulse.brain.brain_query.briefing import format_briefing
            results = {
                "total_results": 1,
                "elapsed_ms": 5,
                "results": {"wins": [{"text": "works", "confidence": 0.7}]},
            }
            format_briefing("security auth deploy", results)
            mock_pm.assert_called_once_with("security auth deploy")

    def test_reminder_prepended_in_output(self):
        """Integration: REMINDER lines appear before [BRAIN] header in output."""
        with patch("pulse.brain.brain_query.briefing.check_prospective_memory") as mock_pm:
            mock_pm.return_value = ["!! REMINDER: Check auth module before deploy"]
            from pulse.brain.brain_query.briefing import format_briefing
            results = {
                "total_results": 1,
                "elapsed_ms": 5,
                "results": {"wins": [{"text": "deploy works", "confidence": 0.7}]},
            }
            output = format_briefing("deploy security", results)
            lines = output.split("\n")
            brain_idx = next((i for i, l in enumerate(lines) if "[BRAIN]" in l), len(lines))
            reminder_idx = next(
                (i for i, l in enumerate(lines) if "REMINDER" in l), len(lines)
            )
            self.assertLess(reminder_idx, brain_idx)


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