#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""P2 Cognition Audit Tests — verifies all 5 P2 architectural fixes are in place."""
import json
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))


# ---------------------------------------------------------------------------
# P2-1: Somatic veto — high-conf anti-pattern emits !! SOMATIC_VETO prefix
# ---------------------------------------------------------------------------

class TestSomaticVeto:
    """P2-1: briefing.py must prepend !! SOMATIC_VETO for anti-pattern hits
    with confidence > 0.8."""

    def _make_results(self, anti_patterns):
        return {
            "total_results": len(anti_patterns),
            "elapsed_ms": 1,
            "results": {"anti_patterns": anti_patterns},
        }

    def test_somatic_veto_emitted_for_high_conf(self):
        from pulse.brain.brain_query.briefing import format_briefing
        results = self._make_results([{
            "title": "Never use sys.exit() in library code",
            "confidence": 0.92,
            "source": "anti_patterns",
            "type": "anti_pattern",
        }])
        output = format_briefing("sys exit", results)
        assert "!! SOMATIC_VETO:" in output, (
            "High-confidence anti-pattern (conf=0.92) must produce !! SOMATIC_VETO line"
        )
        # Veto must be the FIRST line
        first_line = output.splitlines()[0]
        assert first_line.startswith("!! SOMATIC_VETO:"), (
            f"SOMATIC_VETO must be the FIRST line of output, got: {first_line!r}"
        )

    def test_no_veto_for_low_conf(self):
        from pulse.brain.brain_query.briefing import format_briefing
        results = self._make_results([{
            "title": "Some weak pattern",
            "confidence": 0.5,
            "source": "anti_patterns",
            "type": "anti_pattern",
        }])
        output = format_briefing("something", results)
        assert "!! SOMATIC_VETO:" not in output, (
            "Low-confidence anti-pattern (conf=0.5) must NOT produce SOMATIC_VETO"
        )

    def test_veto_threshold_boundary(self):
        """Exactly 0.8 is NOT a veto; 0.81 is."""
        from pulse.brain.brain_query.briefing import format_briefing
        for conf, should_veto in [(0.80, False), (0.81, True), (0.95, True)]:
            results = self._make_results([{
                "title": "Boundary rule",
                "confidence": conf,
                "source": "anti_patterns",
                "type": "anti_pattern",
            }])
            output = format_briefing("boundary", results)
            has_veto = "!! SOMATIC_VETO:" in output
            assert has_veto == should_veto, (
                f"conf={conf}: expected veto={should_veto}, got {has_veto}"
            )

    def test_check_somatic_veto_function_exists(self):
        """The internal helper _check_somatic_veto must exist."""
        from pulse.brain.brain_query.briefing import _check_somatic_veto
        hits = {"anti_patterns": [{"title": "Bad pattern", "confidence": 0.9,
                                    "source": "anti_patterns", "type": "anti_pattern"}]}
        lines = _check_somatic_veto(hits)
        assert isinstance(lines, list)
        assert any("SOMATIC_VETO" in l for l in lines)


# ---------------------------------------------------------------------------
# P2-2: Prediction daemon — fallback to brain activity when board is empty
# ---------------------------------------------------------------------------

class TestPredictionFallback:
    """P2-2: prediction_daemon.py must generate predictions from brain failures
    and query log when no completed tasks exist on the task board."""

    def test_get_recent_failures_function_exists(self):
        from pulse.daemons.neural.prediction_daemon import _get_recent_failures_from_brain
        # Must be callable and return a list (even if DB is empty/unavailable)
        result = _get_recent_failures_from_brain(limit=5)
        assert isinstance(result, list)

    def test_get_recent_query_log_function_exists(self):
        from pulse.daemons.neural.prediction_daemon import _get_recent_query_log
        result = _get_recent_query_log(limit=5)
        assert isinstance(result, list)

    def test_query_log_reads_file(self, tmp_path):
        from pulse.daemons.neural import prediction_daemon as pd
        log_file = tmp_path / "brain_query_log.json"
        entries = [
            {"query": "fix import error", "results": 3, "timestamp": "2026-03-19T10:00:00Z"},
            {"query": "prediction daemon", "results": 0, "timestamp": "2026-03-19T11:00:00Z"},
        ]
        log_file.write_text(json.dumps(entries), encoding="utf-8")

        original = pd.BRAIN_QUERY_LOG
        pd.BRAIN_QUERY_LOG = log_file
        try:
            result = pd._get_recent_query_log(limit=10)
            assert len(result) == 2
            # Most recent first
            assert result[0]["query"] == "prediction daemon"
        finally:
            pd.BRAIN_QUERY_LOG = original

    def test_generate_predictions_skips_when_no_fallback(self, tmp_path):
        """When board is empty AND query log is empty AND no brain failures,
        generate_predictions returns [] without calling LLM."""
        from pulse.daemons.neural import prediction_daemon as pd

        original_log = pd.BRAIN_QUERY_LOG
        pd.BRAIN_QUERY_LOG = tmp_path / "nonexistent_log.json"
        try:
            with patch.object(pd, "_get_completed_tasks", return_value=[]), \
                 patch.object(pd, "_get_recent_failures_from_brain", return_value=[]), \
                 patch.object(pd, "_get_recent_query_log", return_value=[]):
                result = pd.generate_predictions(verbose=False)
                assert result == [], "Must return [] when all sources are empty"
        finally:
            pd.BRAIN_QUERY_LOG = original_log

    def test_generate_predictions_uses_fallback_when_board_empty(self):
        """When board empty but brain failures exist, oracle proceeds to LLM call."""
        from pulse.daemons.neural import prediction_daemon as pd

        fake_failures = [{"title": "Import crash in brain_db", "content": "...",
                          "confidence": 0.8, "timestamp": "2026-03-19T09:00:00"}]
        mock_llm_result = json.dumps({"predictions": []})

        with patch.object(pd, "_get_completed_tasks", return_value=[]), \
             patch.object(pd, "_get_recent_failures_from_brain", return_value=fake_failures), \
             patch.object(pd, "_get_recent_query_log", return_value=[]), \
             patch.object(pd, "_load_brain_context", return_value="ctx"), \
             patch.object(pd, "_format_accuracy_context", return_value="no history"), \
             patch("pulse.core.llm_router.dispatch", return_value=mock_llm_result), \
             patch("pulse.core.llm_router.extract_json", return_value={"predictions": []}):
            result = pd.generate_predictions(verbose=False)
            # LLM returned no predictions — that's fine, test is that it ran (not skipped)
            assert isinstance(result, list)


# ---------------------------------------------------------------------------
# P2-3: Ignorance signal — empty / low-confidence results
# ---------------------------------------------------------------------------

class TestIgnoranceSignal:
    """P2-3: format_briefing must append !! IGNORANCE when total_results == 0
    or average confidence < 0.4."""

    def test_ignorance_on_zero_results(self):
        from pulse.brain.brain_query.briefing import format_briefing
        results = {"total_results": 0, "elapsed_ms": 1, "results": {}}
        output = format_briefing("totally unknown topic xyz", results)
        assert "!! IGNORANCE:" in output, (
            "Zero results must produce !! IGNORANCE signal"
        )

    def test_ignorance_on_low_avg_confidence(self):
        from pulse.brain.brain_query.briefing import format_briefing
        results = {
            "total_results": 2,
            "elapsed_ms": 5,
            "results": {
                "wins": [
                    {"text": "some result", "confidence": 0.2},
                    {"text": "another result", "confidence": 0.3},
                ],
            },
        }
        output = format_briefing("obscure topic", results)
        assert "!! IGNORANCE:" in output, (
            "Average confidence 0.25 < 0.4 must produce !! IGNORANCE signal"
        )

    def test_no_ignorance_on_high_confidence(self):
        from pulse.brain.brain_query.briefing import format_briefing
        results = {
            "total_results": 2,
            "elapsed_ms": 5,
            "results": {
                "wins": [
                    {"text": "solid result", "confidence": 0.85},
                    {"text": "another solid result", "confidence": 0.9},
                ],
            },
        }
        output = format_briefing("well-known topic", results)
        assert "!! IGNORANCE:" not in output, (
            "High average confidence must NOT produce ignorance signal"
        )

    def test_runner_seeds_explore_intent_on_empty(self):
        """runner.py must call insert_knowledge with EXPLORE intent when total==0."""
        from pulse.brain.brain_query import runner

        mock_results = {"total_results": 0, "elapsed_ms": 1, "results": {}}
        inserted = []

        def fake_insert(table, data):
            inserted.append((table, data))

        with patch.object(runner, "brain_search", return_value=mock_results), \
             patch("pulse.core.brain_db.insert_knowledge", side_effect=fake_insert):
            # Re-import to pick up patched insert_knowledge via runner's lazy import
            import importlib
            importlib.reload(runner)
            with patch.object(runner, "brain_search", return_value=mock_results):
                try:
                    with patch("pulse.core.brain_db.insert_knowledge", side_effect=fake_insert):
                        runner.run_query("completely unknown xyz topic 12345")
                except Exception:
                    pass

        # Check runner source contains the EXPLORE seeding logic
        src = (ROOT / "pulse/brain/brain_query/runner.py").read_text(encoding="utf-8")
        assert "EXPLORE:" in src, "runner.py must seed EXPLORE intent for empty results"
        assert "insert_knowledge" in src, "runner.py must call insert_knowledge"


# ---------------------------------------------------------------------------
# P2-4: Session checkpoint — save/load round-trip
# ---------------------------------------------------------------------------

class TestSessionCheckpoint:
    """P2-4: save_checkpoint / load_latest_checkpoint must round-trip correctly."""

    def test_save_creates_file(self, tmp_path):
        from pulse.brain import session_checkpoint as sc
        original_dir = sc._BUFFER_DIR
        sc._BUFFER_DIR = tmp_path / "session_buffer"
        try:
            path = sc.save_checkpoint(
                agent="claude",
                task="Implement P2 cognition fixes",
                progress="4/5 done",
                findings=["Somatic veto works", "Ignorance signal added"],
                files_touched=["pulse/brain/brain_query/briefing.py"],
            )
            assert path.exists(), "Checkpoint file must be created"
            data = json.loads(path.read_text(encoding="utf-8"))
            assert data["agent"] == "claude"
            assert data["task"] == "Implement P2 cognition fixes"
            assert data["progress"] == "4/5 done"
            assert "Somatic veto works" in data["findings"]
        finally:
            sc._BUFFER_DIR = original_dir

    def test_load_returns_none_when_no_checkpoint(self, tmp_path):
        from pulse.brain import session_checkpoint as sc
        original_dir = sc._BUFFER_DIR
        sc._BUFFER_DIR = tmp_path / "empty_buffer"
        try:
            result = sc.load_latest_checkpoint("claude")
            assert result is None
        finally:
            sc._BUFFER_DIR = original_dir

    def test_save_load_round_trip(self, tmp_path):
        from pulse.brain import session_checkpoint as sc
        original_dir = sc._BUFFER_DIR
        sc._BUFFER_DIR = tmp_path / "session_buffer"
        try:
            sc.save_checkpoint(
                agent="gemini",
                task="Run brain audit",
                progress="complete",
                findings=["Graph has 362 nodes", "FTS5 works"],
                files_touched=["pulse/brain/brain_search.py", "pulse/core/brain_db.py"],
            )
            loaded = sc.load_latest_checkpoint("gemini")
            assert loaded is not None, "load_latest_checkpoint must return the saved data"
            assert loaded["agent"] == "gemini"
            assert loaded["task"] == "Run brain audit"
            assert loaded["progress"] == "complete"
            assert "Graph has 362 nodes" in loaded["findings"]
            assert "pulse/brain/brain_search.py" in loaded["files_touched"]
        finally:
            sc._BUFFER_DIR = original_dir

    def test_load_session_continuity_in_loaders(self):
        """loaders.py must expose load_session_continuity()."""
        from pulse.brain.boot.loaders import load_session_continuity
        # Call with no real checkpoint — must return a string (possibly empty)
        result = load_session_continuity("claude")
        assert isinstance(result, str)

    def test_load_session_continuity_formats_section(self, tmp_path):
        """When a checkpoint exists, load_session_continuity returns ## PREVIOUS SESSION STATE."""
        from pulse.brain.boot import loaders
        from pulse.brain import session_checkpoint as sc

        original_dir = sc._BUFFER_DIR
        sc._BUFFER_DIR = tmp_path / "session_buffer"
        try:
            sc.save_checkpoint(
                agent="claude",
                task="Test continuity section",
                progress="in progress",
                findings=["Finding A"],
                files_touched=["foo.py"],
            )
            result = loaders.load_session_continuity("claude")
            assert "## PREVIOUS SESSION STATE" in result
            assert "Test continuity section" in result
            assert "in progress" in result
        finally:
            sc._BUFFER_DIR = original_dir


# ---------------------------------------------------------------------------
# P2-5: Governance context has PREDICTION_AWARENESS and SOURCE_GROUNDING rules
# ---------------------------------------------------------------------------

class TestGovernanceRules:
    """P2-5: build_governance_context() must include RULE:PREDICTION_AWARENESS
    and RULE:SOURCE_GROUNDING for all agents."""

    def test_prediction_awareness_rule_present(self):
        from pulse.brain.boot.formatters_ext import build_governance_context
        ctx = build_governance_context()
        assert "[RULE:PREDICTION_AWARENESS]" in ctx, (
            "Governance context must contain [RULE:PREDICTION_AWARENESS]"
        )

    def test_source_grounding_rule_present(self):
        from pulse.brain.boot.formatters_ext import build_governance_context
        ctx = build_governance_context()
        assert "[RULE:SOURCE_GROUNDING]" in ctx, (
            "Governance context must contain [RULE:SOURCE_GROUNDING]"
        )

    def test_prediction_awareness_content(self):
        from pulse.brain.boot.formatters_ext import build_governance_context
        ctx = build_governance_context()
        assert "irreversible" in ctx.lower(), (
            "PREDICTION_AWARENESS must mention irreversible changes"
        )
        assert "prediction" in ctx.lower(), (
            "PREDICTION_AWARENESS must mention checking prediction alerts"
        )

    def test_source_grounding_content(self):
        from pulse.brain.boot.formatters_ext import build_governance_context
        ctx = build_governance_context()
        assert "CONFIRMED" in ctx, (
            "SOURCE_GROUNDING must reference [CONFIRMED] knowledge"
        )
        assert "CONVERSATIONAL" in ctx, (
            "SOURCE_GROUNDING must reference [CONVERSATIONAL] knowledge"
        )

    def test_rules_appear_after_brain_first_section(self):
        """Both rules must come after BRAIN-FIRST ENFORCEMENT (ordering check)."""
        from pulse.brain.boot.formatters_ext import build_governance_context
        ctx = build_governance_context()
        brain_first_pos = ctx.find("BRAIN-FIRST ENFORCEMENT")
        pred_pos = ctx.find("[RULE:PREDICTION_AWARENESS]")
        grounding_pos = ctx.find("[RULE:SOURCE_GROUNDING]")
        assert brain_first_pos != -1, "BRAIN-FIRST ENFORCEMENT section missing"
        assert pred_pos > brain_first_pos, "PREDICTION_AWARENESS must come after BRAIN-FIRST"
        assert grounding_pos > brain_first_pos, "SOURCE_GROUNDING must come after BRAIN-FIRST"
