"""
Integration tests for P41 neuroscience items 176-180.
Run: python -m pytest tests/test_neuro_p41.py -v
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest

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


# ---------------------------------------------------------------------------
# Item 176: Visuospatial sketchpad READ — sketch boost in post-scoring
# ---------------------------------------------------------------------------
class TestItem176VisuospatialSketchBoost:

    def test_sketch_boost_applied_to_matching_id(self):
        """Result whose id is in the sketch gets +0.05 confidence and _sketch_boost flag."""
        from pulse.brain.brain_search import search_post_scoring as sps
        import pulse.brain.memory.working_memory as wm

        # Populate the sketch with a known id
        original_sketch = list(wm._VISUOSPATIAL_SKETCH)
        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.append("node-abc")

        results = {
            "lessons": [
                {"id": "node-abc", "text": "test item", "confidence": 0.6},
                {"id": "node-xyz", "text": "other item", "confidence": 0.5},
            ]
        }
        # Apply only the sketch boost logic (call apply_post_scoring_impl with no-op fns)
        sps.apply_post_scoring_impl(
            results, "test query", "general",
            lambda r, q: None, lambda r: None, lambda r, q: None, None,
        )

        assert results["lessons"][0]["_sketch_boost"] is True
        assert results["lessons"][0]["confidence"] == pytest.approx(0.65, abs=0.01)
        # Non-matching item unchanged
        assert "node-xyz" not in [r for r in [results["lessons"][1].get("id")] if r in wm._VISUOSPATIAL_SKETCH]
        assert results["lessons"][1].get("_sketch_boost") is not True

        # Restore sketch
        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.extend(original_sketch)

    def test_sketch_boost_caps_confidence_at_1(self):
        """Confidence should never exceed 1.0 after sketch boost."""
        from pulse.brain.brain_search import search_post_scoring as sps
        import pulse.brain.memory.working_memory as wm

        original_sketch = list(wm._VISUOSPATIAL_SKETCH)
        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.append("node-cap")

        results = {
            "lessons": [{"id": "node-cap", "text": "high conf", "confidence": 0.98}]
        }
        sps.apply_post_scoring_impl(
            results, "q", "general",
            lambda r, q: None, lambda r: None, lambda r, q: None, None,
        )
        assert results["lessons"][0]["confidence"] <= 1.0

        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.extend(original_sketch)

    def test_empty_sketch_no_boost(self):
        """Empty sketch causes no _sketch_boost annotations."""
        from pulse.brain.brain_search import search_post_scoring as sps
        import pulse.brain.memory.working_memory as wm

        original_sketch = list(wm._VISUOSPATIAL_SKETCH)
        wm._VISUOSPATIAL_SKETCH.clear()

        results = {
            "lessons": [{"id": "node-any", "text": "item", "confidence": 0.5}]
        }
        sps.apply_post_scoring_impl(
            results, "q", "general",
            lambda r, q: None, lambda r: None, lambda r, q: None, None,
        )
        assert results["lessons"][0].get("_sketch_boost") is not True

        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.extend(original_sketch)

    def test_no_id_result_not_boosted(self):
        """Results without an 'id' field are safely skipped."""
        from pulse.brain.brain_search import search_post_scoring as sps
        import pulse.brain.memory.working_memory as wm

        original_sketch = list(wm._VISUOSPATIAL_SKETCH)
        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.append("node-exists")

        results = {
            "lessons": [{"text": "no id here", "confidence": 0.5}]
        }
        sps.apply_post_scoring_impl(
            results, "q", "general",
            lambda r, q: None, lambda r: None, lambda r, q: None, None,
        )
        assert results["lessons"][0].get("_sketch_boost") is not True

        wm._VISUOSPATIAL_SKETCH.clear()
        wm._VISUOSPATIAL_SKETCH.extend(original_sketch)

    def test_fails_open_on_import_error(self):
        """If working_memory import fails, post-scoring continues without crashing."""
        from pulse.brain.brain_search import search_post_scoring as sps
        results = {"lessons": [{"id": "x", "confidence": 0.5}]}
        with patch.dict("sys.modules", {"pulse.brain.memory.working_memory": None}):
            # Should not raise even if import is broken
            try:
                sps.apply_post_scoring_impl(
                    results, "q", "general",
                    lambda r, q: None, lambda r: None, lambda r, q: None, None,
                )
            except Exception:
                pytest.fail("apply_post_scoring_impl raised on import failure")


# ---------------------------------------------------------------------------
# Item 177: Phonological buffer READ — query priming
# ---------------------------------------------------------------------------
class TestItem177PhonologicalPriming:

    def setup_method(self):
        import pulse.brain.memory.working_memory as wm
        wm._PHONOLOGICAL_BUFFER.clear()

    def teardown_method(self):
        import pulse.brain.memory.working_memory as wm
        wm._PHONOLOGICAL_BUFFER.clear()

    def test_related_query_surfaces_context_line(self):
        """A sufficiently similar buffered query produces a CONTEXT line."""
        import pulse.brain.memory.working_memory as wm
        from pulse.brain.brain_query.briefing_wm import check_phonological_priming

        # Manually add a similar query to the buffer
        wm._PHONOLOGICAL_BUFFER["fp1"] = {"text": "brain search optimization techniques"}

        with patch("pulse.core.brain_analysis.text_similarity", return_value=0.7):
            lines = check_phonological_priming("brain search performance improvements")

        assert any("CONTEXT" in l for l in lines)

    def test_exact_match_skipped(self):
        """Exact matches do not fire — habituation is handled elsewhere."""
        import pulse.brain.memory.working_memory as wm
        from pulse.brain.brain_query.briefing_wm import check_phonological_priming

        wm._PHONOLOGICAL_BUFFER["fp2"] = {"text": "brain search optimization"}

        with patch("pulse.core.brain_analysis.text_similarity", return_value=0.99):
            lines = check_phonological_priming("brain search optimization")

        # Exact match (buf_text == q_short) should be skipped
        assert lines == []

    def test_dissimilar_query_no_context(self):
        """Low-similarity buffered query produces no CONTEXT line."""
        import pulse.brain.memory.working_memory as wm
        from pulse.brain.brain_query.briefing_wm import check_phonological_priming

        wm._PHONOLOGICAL_BUFFER["fp3"] = {"text": "deployment pipeline configuration"}

        with patch("pulse.core.brain_analysis.text_similarity", return_value=0.1):
            lines = check_phonological_priming("security audit vulnerability scan")

        assert lines == []

    def test_empty_buffer_returns_empty(self):
        """Empty phonological buffer produces no output."""
        from pulse.brain.brain_query.briefing_wm import check_phonological_priming
        lines = check_phonological_priming("anything")
        assert lines == []

    def test_fails_open_on_exception(self):
        """Any exception inside check_phonological_priming returns empty list."""
        from pulse.brain.brain_query.briefing_wm import check_phonological_priming
        with patch("pulse.brain.memory.working_memory._PHONOLOGICAL_BUFFER",
                   new_callable=lambda: type("Bad", (), {"items": lambda s: (_ for _ in ()).throw(RuntimeError("boom"))})()  ):
            try:
                lines = check_phonological_priming("test")
                assert isinstance(lines, list)
            except Exception:
                pass  # accept either empty list or silent failure


# ---------------------------------------------------------------------------
# Item 178: Working memory refresh rate READ — volatility alert
# ---------------------------------------------------------------------------
class TestItem178WMVolatilityAlert:

    def setup_method(self):
        from pulse.brain import working_memory_p36 as wm36
        wm36.reset_refresh_count()

    def teardown_method(self):
        from pulse.brain import working_memory_p36 as wm36
        wm36.reset_refresh_count()

    def test_low_refresh_no_warning(self):
        """Below threshold: no warning line in briefing output."""
        from pulse.brain.brain_query.briefing_wm import check_wm_volatility
        lines = check_wm_volatility()
        assert lines == []

    def test_high_refresh_warning_emitted(self):
        """Above threshold: HIGH REFRESH RATE warning appears."""
        from pulse.brain import working_memory_p36 as wm36
        from pulse.brain.brain_query.briefing_wm import check_wm_volatility

        wm36.increment_refresh_count(wm36._HIGH_REFRESH_THRESHOLD + 1)
        lines = check_wm_volatility()
        assert len(lines) == 1
        assert "HIGH REFRESH RATE" in lines[0]

    def test_warning_contains_count(self):
        """Warning message includes the eviction count."""
        from pulse.brain import working_memory_p36 as wm36
        from pulse.brain.brain_query.briefing_wm import check_wm_volatility

        wm36.increment_refresh_count(wm36._HIGH_REFRESH_THRESHOLD + 5)
        lines = check_wm_volatility()
        assert any(str(wm36.get_refresh_count()) in l for l in lines)

    def test_fails_open_on_exception(self):
        """Import error returns empty list, not an exception."""
        from pulse.brain.brain_query.briefing_wm import check_wm_volatility
        with patch("pulse.brain.memory.working_memory_p36.get_refresh_rate_status",
                   side_effect=Exception("boom")):
            lines = check_wm_volatility()
        assert isinstance(lines, list)

    def test_returns_list(self):
        """check_wm_volatility always returns a list."""
        from pulse.brain.brain_query.briefing_wm import check_wm_volatility
        result = check_wm_volatility()
        assert isinstance(result, list)


# ---------------------------------------------------------------------------
# Item 179: post_shared READ — shared context in briefing
# ---------------------------------------------------------------------------
class TestItem179SharedContext:

    def test_high_priority_item_surfaced(self):
        """High-priority shared item appears as !! SHARED line."""
        from pulse.brain.brain_query.briefing_wm import check_shared_context

        mock_data = {
            "shared_items": [
                {
                    "text": "Critical: memory pipeline stalled",
                    "posted_by": "gemini_1",
                    "priority": "high",
                    "posted_at": "2099-01-01T00:00:00",
                    "ttl_minutes": 99999,
                    "acknowledged_by": [],
                }
            ]
        }
        with patch("pulse.brain.memory.working_memory._load_shared", return_value=mock_data), \
             patch("pulse.brain.memory.working_memory._evict_ttl", return_value=None):
            lines = check_shared_context()

        assert any("SHARED" in l for l in lines)
        assert any("gemini_1" in l for l in lines)

    def test_normal_priority_uses_tilde(self):
        """Normal priority items use ~ prefix, not !!."""
        from pulse.brain.brain_query.briefing_wm import check_shared_context

        mock_data = {
            "shared_items": [
                {
                    "text": "FYI: new search index built",
                    "posted_by": "claude_1",
                    "priority": "normal",
                    "posted_at": "2099-01-01T00:00:00",
                    "ttl_minutes": 99999,
                    "acknowledged_by": [],
                }
            ]
        }
        with patch("pulse.brain.memory.working_memory._load_shared", return_value=mock_data), \
             patch("pulse.brain.memory.working_memory._evict_ttl", return_value=None):
            lines = check_shared_context()

        assert any(l.strip().startswith("~ SHARED") for l in lines)

    def test_empty_shared_returns_empty(self):
        """No shared items returns empty list."""
        from pulse.brain.brain_query.briefing_wm import check_shared_context

        with patch("pulse.brain.memory.working_memory._load_shared",
                   return_value={"shared_items": []}), \
             patch("pulse.brain.memory.working_memory._evict_ttl", return_value=None):
            lines = check_shared_context()
        assert lines == []

    def test_max_two_shared_lines(self):
        """At most 2 shared lines are returned regardless of input size."""
        from pulse.brain.brain_query.briefing_wm import check_shared_context

        mock_data = {
            "shared_items": [
                {"text": f"item {i}", "posted_by": "agent_1",
                 "priority": "normal", "posted_at": "2099-01-01T00:00:00",
                 "ttl_minutes": 99999, "acknowledged_by": []}
                for i in range(10)
            ]
        }
        with patch("pulse.brain.memory.working_memory._load_shared", return_value=mock_data), \
             patch("pulse.brain.memory.working_memory._evict_ttl", return_value=None):
            lines = check_shared_context()
        assert len(lines) <= 2

    def test_fails_open_on_exception(self):
        """Exception from _load_shared returns empty list."""
        from pulse.brain.brain_query.briefing_wm import check_shared_context
        with patch("pulse.brain.memory.working_memory._load_shared",
                   side_effect=Exception("disk error")):
            lines = check_shared_context()
        assert isinstance(lines, list)
        assert lines == []


# ---------------------------------------------------------------------------
# Item 180: rotate_session integration on task completion
# ---------------------------------------------------------------------------
class TestItem180RotateSessionOnCompletion:

    def _make_board(self, task_id: str, agent_id: str) -> dict:
        return {
            "dispatches": [{
                "tasks": [{
                    "task_id": task_id,
                    "status": "active",
                    "claimed_by": agent_id,
                    "domain": "brain",
                    "title": "Test task",
                    "recommended_tier": "standard",
                    "retry_count": 0,
                }]
            }]
        }

    def test_rotate_session_called_on_done(self):
        """rotate_session is called with agent_id when a task is finalized as done."""
        from pulse.tasks import task_claiming
        from unittest.mock import patch, MagicMock, call
        import tempfile, json
        from pathlib import Path

        board = self._make_board("task-001", "claude_1")
        with tempfile.TemporaryDirectory() as tmpdir:
            board_path = Path(tmpdir) / "task_board.json"
            board_path.write_text(json.dumps(board), encoding="utf-8")

            mock_wm = MagicMock()

            with patch("pulse.tasks.task_ops._get_atomic_update") as mock_atomic, \
                 patch("pulse.tasks.task_ops._get_crypto", return_value=None), \
                 patch("pulse.brain.memory.working_memory.rotate_session") as mock_rotate:

                # Use real atomic update by directly mutating the board JSON
                def real_atomic(path, fn, default=None):
                    try:
                        data = json.loads(path.read_text(encoding="utf-8"))
                    except Exception:
                        data = default or {}
                    result = fn(data)
                    if result is not None:
                        path.write_text(json.dumps(result), encoding="utf-8")

                mock_atomic.return_value = real_atomic

                task_claiming.finalize_task(
                    board_path, "task-001", "claude_1",
                    outcome="done", status="done"
                )

            mock_rotate.assert_called_once_with("claude_1")

    def test_rotate_session_failure_does_not_block_completion(self):
        """If rotate_session raises, finalize_task still completes the task."""
        from pulse.tasks import task_claiming
        import tempfile, json
        from pathlib import Path

        board = self._make_board("task-002", "claude_2")
        with tempfile.TemporaryDirectory() as tmpdir:
            board_path = Path(tmpdir) / "task_board.json"
            board_path.write_text(json.dumps(board), encoding="utf-8")

            with patch("pulse.tasks.task_ops._get_atomic_update") as mock_atomic, \
                 patch("pulse.tasks.task_ops._get_crypto", return_value=None), \
                 patch("pulse.brain.memory.working_memory.rotate_session",
                       side_effect=RuntimeError("wm crashed")):

                def real_atomic(path, fn, default=None):
                    try:
                        data = json.loads(path.read_text(encoding="utf-8"))
                    except Exception:
                        data = default or {}
                    result = fn(data)
                    if result is not None:
                        path.write_text(json.dumps(result), encoding="utf-8")

                mock_atomic.return_value = real_atomic

                # Should not raise even if rotate_session blows up
                try:
                    task_claiming.finalize_task(
                        board_path, "task-002", "claude_2",
                        outcome="done", status="done"
                    )
                except Exception as exc:
                    pytest.fail(f"finalize_task raised: {exc}")

            # Task should still be marked done
            final_board = json.loads(board_path.read_text(encoding="utf-8"))
            task = final_board["dispatches"][0]["tasks"][0]
            assert task["status"] == "done"

    def test_rotate_session_not_called_on_escalation(self):
        """rotate_session should NOT be called when task is escalated, only on done."""
        from pulse.tasks import task_claiming
        import tempfile, json
        from pathlib import Path

        board = self._make_board("task-003", "claude_3")
        with tempfile.TemporaryDirectory() as tmpdir:
            board_path = Path(tmpdir) / "task_board.json"
            board_path.write_text(json.dumps(board), encoding="utf-8")

            with patch("pulse.tasks.task_ops._get_atomic_update") as mock_atomic, \
                 patch("pulse.tasks.task_ops._get_crypto", return_value=None), \
                 patch("pulse.brain.memory.working_memory.rotate_session") as mock_rotate:

                def real_atomic(path, fn, default=None):
                    try:
                        data = json.loads(path.read_text(encoding="utf-8"))
                    except Exception:
                        data = default or {}
                    result = fn(data)
                    if result is not None:
                        path.write_text(json.dumps(result), encoding="utf-8")

                mock_atomic.return_value = real_atomic

                task_claiming.finalize_task(
                    board_path, "task-003", "claude_3",
                    outcome="failed", status="escalated"
                )

            mock_rotate.assert_not_called()

    def test_rotate_session_import_in_task_claiming(self):
        """Verify rotate_session is importable from working_memory."""
        from pulse.brain.memory.working_memory import rotate_session
        assert callable(rotate_session)

    def test_rotate_session_returns_summary(self):
        """rotate_session() returns a dict with session summary keys."""
        from pulse.brain.memory.working_memory import rotate_session
        result = rotate_session("test_agent_99")
        assert isinstance(result, dict)
        # Must have at minimum agent field or error field
        assert "agent" in result or "error" in result
