#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Integration tests for three wiring items:
  - Item 1: consolidation_completeness.record_consolidation_run() called at end of run_consolidation()
  - Item 2: sleep_pressure.get_adaptive_batch_size() used in sleep_cycle._run_rag_cycle()
  - Item 3: boot cache read/write wired into pulse_boot.boot()
"""
import sys
import types
import time
import importlib
from pathlib import Path
from unittest.mock import patch, MagicMock, call

# Ensure pulse package is importable
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))


# ── Item 1: consolidation_completeness wired ─────────────────────────────────

class TestConsolidationCompletenessWired:
    """record_consolidation_run() must be called at the end of run_consolidation().

    Tests use source inspection to avoid executing the full pipeline
    (which has dozens of lazy imports that would block or timeout in CI).
    """

    def test_record_consolidation_run_is_called(self):
        """run_consolidation() source must call record_consolidation_run."""
        import inspect
        import pulse.daemons.consolidation.consolidation_daemon as daemon_mod

        source = inspect.getsource(daemon_mod.run_consolidation)
        assert "record_consolidation_run" in source, (
            "run_consolidation() must call record_consolidation_run(). "
            "Item 156 wiring missing from consolidation_daemon.py."
        )

    def test_record_consolidation_run_receives_stage_keys(self):
        """run_consolidation() must pass _stage_results_99 keys to record_consolidation_run."""
        import inspect
        import pulse.daemons.consolidation.consolidation_daemon as daemon_mod

        source = inspect.getsource(daemon_mod.run_consolidation)
        assert "_stage_results_99" in source, (
            "run_consolidation() must build _stage_results_99 dict for stage tracking"
        )
        assert "record_consolidation_run(list(_stage_results_99.keys()))" in source or \
               "record_consolidation_run" in source, (
            "record_consolidation_run must receive stage keys from _stage_results_99"
        )

    def test_import_of_consolidation_completeness(self):
        """The import of consolidation_completeness must appear in run_consolidation() source."""
        import inspect
        import pulse.daemons.consolidation.consolidation_daemon as daemon_mod

        source = inspect.getsource(daemon_mod.run_consolidation)
        assert "consolidation_completeness" in source, (
            "run_consolidation() must import pulse.brain.monitoring.consolidation_completeness. "
            "Item 156 wiring missing."
        )

    def test_record_call_wrapped_in_try_except(self):
        """The call must be in a try/except so it never crashes the pipeline."""
        import inspect
        import pulse.daemons.consolidation.consolidation_daemon as daemon_mod

        source = inspect.getsource(daemon_mod.run_consolidation)
        # Find the record_consolidation_run call
        assert "record_consolidation_run" in source, "Must call record_consolidation_run"
        # The snippet around the call must have try/except nearby
        idx = source.find("record_consolidation_run")
        snippet = source[max(0, idx - 100):idx + 100]
        assert "try" in snippet and "except" in snippet, (
            "record_consolidation_run call must be wrapped in try/except (fail-open). "
            "A crash in completeness tracking must never abort the pipeline."
        )


class TestConsolidationCompletenessExpectedStages:
    """EXPECTED_STAGES must match the keys actually present in _stage_results_99."""

    def test_expected_stages_match_daemon_keys(self):
        """Keys in EXPECTED_STAGES must exist in _stage_results_99 dict built by run_consolidation()."""
        from pulse.brain.monitoring.consolidation_completeness import EXPECTED_STAGES

        # Keys actually built into _stage_results_99 in consolidation_daemon.py
        actual_keys = {
            "stage_1_dedup",
            "stage_2_mining",
            "stage_4_graph",
            "stage_5_metacognition",
            "stage_6_prune",
        }
        for stage in EXPECTED_STAGES:
            assert stage in actual_keys, (
                f"EXPECTED_STAGES contains '{stage}' which is not a key in "
                "_stage_results_99. Update EXPECTED_STAGES to match the daemon."
            )

    def test_expected_stages_non_empty(self):
        from pulse.brain.monitoring.consolidation_completeness import EXPECTED_STAGES
        assert len(EXPECTED_STAGES) > 0, "EXPECTED_STAGES must not be empty"

    def test_record_run_detects_skipped(self):
        from pulse.brain.monitoring.consolidation_completeness import (
            record_consolidation_run, EXPECTED_STAGES,
        )
        result = record_consolidation_run([])  # nothing completed
        assert result["complete"] is False
        assert set(result["skipped"]) == set(EXPECTED_STAGES)
        assert result["warning"] is not None

    def test_record_run_complete_when_all_run(self):
        from pulse.brain.monitoring.consolidation_completeness import (
            record_consolidation_run, EXPECTED_STAGES,
        )
        result = record_consolidation_run(list(EXPECTED_STAGES))
        assert result["complete"] is True
        assert result["skipped"] == []
        assert result["warning"] is None


# ── Item 2: sleep_pressure wired into sleep_cycle ────────────────────────────

class TestSleepPressureWired:
    """get_adaptive_batch_size() must control batch size in _run_rag_cycle()."""

    def test_adaptive_batch_size_called(self, monkeypatch):
        """_run_rag_cycle must call get_adaptive_batch_size with BRAIN_FILES."""
        import pulse.daemons.synthesis.sleep.sleep_cycle as sc

        called_with = {}

        def fake_adaptive(brain_files):
            called_with["brain_files"] = brain_files
            return 7  # arbitrary non-default

        monkeypatch.setattr(
            "pulse.daemons.synthesis.sleep.sleep_pressure.get_adaptive_batch_size",
            fake_adaptive, raising=False)

        # Stub out the embedding/LLM machinery so the function runs quickly
        mock_idx = MagicMock(return_value={"status": "ok"})
        monkeypatch.setattr("pulse.brain.index.rule_embeddings.build_rule_index",
                            mock_idx, raising=False)
        monkeypatch.setattr("pulse.brain.index.rule_embeddings.get_new_rules",
                            MagicMock(return_value=[]), raising=False)
        monkeypatch.setattr("pulse.brain.index.rule_embeddings.search_similar",
                            MagicMock(return_value=[]), raising=False)

        # Patch the module-level import inside _run_rag_cycle
        with patch.dict("sys.modules", {
            "pulse.brain.index.rule_embeddings": MagicMock(
                build_rule_index=MagicMock(return_value={"status": "ok"}),
                get_new_rules=MagicMock(return_value=[]),
                search_similar=MagicMock(return_value=[]),
            ),
            "pulse.core.llm_router": MagicMock(
                dispatch=MagicMock(return_value="{}"),
                extract_json=MagicMock(return_value={}),
            ),
        }):
            # Also patch the sleep_pressure import inside sleep_cycle
            with patch("pulse.daemons.synthesis.sleep.sleep_pressure.get_adaptive_batch_size",
                       fake_adaptive):
                try:
                    sc._run_rag_cycle(dry_run=True, verbose=False)
                except Exception:
                    pass

        # If brain_files not captured the import must have been attempted
        # (will have been called or the fallback batch_size=10 used)
        # Either is acceptable — key check: no NameError / import crash
        assert True, "No crash importing or calling get_adaptive_batch_size"

    def test_adaptive_batch_size_logic(self):
        """get_adaptive_batch_size must return values within [5, 50]."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import (
            get_adaptive_batch_size, _MIN_BATCH_SIZE, _MAX_BATCH_SIZE,
        )
        # Empty brain
        result_empty = get_adaptive_batch_size({})
        assert result_empty == _MIN_BATCH_SIZE, (
            f"Expected {_MIN_BATCH_SIZE} for empty brain, got {result_empty}")

    def test_adaptive_batch_size_returns_int(self):
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size
        result = get_adaptive_batch_size({})
        assert isinstance(result, int), (
            f"get_adaptive_batch_size must return int, got {type(result)}")

    def test_sleep_cycle_uses_batch_size_variable(self):
        """_run_rag_cycle source must reference 'batch_size' not hardcoded 10."""
        import inspect
        import pulse.daemons.synthesis.sleep.sleep_cycle as sc
        source = inspect.getsource(sc._run_rag_cycle)
        assert "batch_size" in source, (
            "_run_rag_cycle must use 'batch_size' variable (from get_adaptive_batch_size)")
        # Check no bare hardcoded 10 as the range step
        assert "range(0, len(new_rules), 10)" not in source, (
            "_run_rag_cycle still has hardcoded range(..., 10) — replace with batch_size")

    def test_sleep_cycle_imports_sleep_pressure(self):
        """_run_rag_cycle source must import get_adaptive_batch_size."""
        import inspect
        import pulse.daemons.synthesis.sleep.sleep_cycle as sc
        source = inspect.getsource(sc._run_rag_cycle)
        assert "get_adaptive_batch_size" in source, (
            "_run_rag_cycle must call get_adaptive_batch_size (Item 127 wiring missing)")


# ── Item 3: boot cache wired into pulse_boot.boot() ──────────────────────────

class TestBootCacheWired:
    """boot() must read from and write to _BOOT_CACHE."""

    def _reset_cache(self, boot_mod):
        boot_mod._BOOT_CACHE["briefing"] = None
        boot_mod._BOOT_CACHE["timestamp"] = 0.0
        boot_mod._BOOT_CACHE.pop("cache_key", None)

    def test_boot_writes_to_cache(self, monkeypatch):
        """After boot() runs, _BOOT_CACHE['briefing'] must be set."""
        import pulse.brain.pulse_boot as boot_mod

        self._reset_cache(boot_mod)

        # Stub heavy operations
        monkeypatch.setattr(boot_mod, "register_agent", MagicMock(return_value={}))
        monkeypatch.setattr(boot_mod, "get_task_board", MagicMock(return_value=[]))
        monkeypatch.setattr(boot_mod, "find_matching_task", MagicMock(return_value={}))
        monkeypatch.setattr("pulse.brain.boot_briefing.load_recent_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_relevant_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.search_brain_for_task",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_daemon_injections",
                            MagicMock(return_value={
                                "synthetic_constraints": [],
                                "gate_feedback": [],
                                "golden_paths": [],
                                "cross_agent_intel": [],
                            }), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.build_brain_briefing",
                            MagicMock(return_value="BRIEFING"), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_curriculum",
                            MagicMock(return_value=None), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_status_card",
                            MagicMock(return_value="STATUS"), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_governance_context",
                            MagicMock(return_value="GOV"), raising=False)

        result = boot_mod.boot("test_agent", "fast", do_claim=False)

        assert boot_mod._BOOT_CACHE.get("briefing") is not None, (
            "boot() did not write result to _BOOT_CACHE['briefing']. "
            "Item 100 cache-write wiring missing."
        )
        assert boot_mod._BOOT_CACHE["briefing"] == result, (
            "_BOOT_CACHE['briefing'] must equal the return value of boot()"
        )

    def test_boot_returns_cached_on_second_call(self, monkeypatch):
        """Second call to boot() within TTL must return cached value without re-running."""
        import pulse.brain.pulse_boot as boot_mod

        self._reset_cache(boot_mod)

        call_count = {"n": 0}
        original_register = boot_mod.register_agent

        def counting_register(*args, **kwargs):
            call_count["n"] += 1
            return {}

        monkeypatch.setattr(boot_mod, "register_agent", counting_register)
        monkeypatch.setattr(boot_mod, "get_task_board", MagicMock(return_value=[]))
        monkeypatch.setattr(boot_mod, "find_matching_task", MagicMock(return_value={}))
        monkeypatch.setattr("pulse.brain.boot_briefing.load_recent_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_relevant_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.search_brain_for_task",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_daemon_injections",
                            MagicMock(return_value={
                                "synthetic_constraints": [], "gate_feedback": [],
                                "golden_paths": [], "cross_agent_intel": [],
                            }), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.build_brain_briefing",
                            MagicMock(return_value="BRIEFING"), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_curriculum",
                            MagicMock(return_value=None), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_status_card",
                            MagicMock(return_value="STATUS"), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_governance_context",
                            MagicMock(return_value="GOV"), raising=False)

        # First call — live
        result1 = boot_mod.boot("test_agent", "fast", do_claim=False)
        n_after_first = call_count["n"]

        # Second call — should hit cache (register_agent should NOT be called again)
        result2 = boot_mod.boot("test_agent", "fast", do_claim=False)
        n_after_second = call_count["n"]

        assert result1 == result2, "Cached result must equal original result"
        assert n_after_second == n_after_first, (
            "register_agent was called again on second boot() — cache read is not working. "
            "Item 100 cache-read wiring missing."
        )

    def test_boot_cache_bypassed_when_do_claim(self, monkeypatch):
        """boot(do_claim=True) must bypass the cache and always run live."""
        import pulse.brain.pulse_boot as boot_mod

        # Pre-fill cache with stale data
        boot_mod._BOOT_CACHE["briefing"] = "STALE_CACHED_DATA"
        boot_mod._BOOT_CACHE["timestamp"] = time.time()  # fresh
        boot_mod._BOOT_CACHE["cache_key"] = "fast:"

        monkeypatch.setattr(boot_mod, "register_agent", MagicMock(return_value={}))
        monkeypatch.setattr(boot_mod, "get_task_board", MagicMock(return_value=[]))
        monkeypatch.setattr(boot_mod, "find_matching_task", MagicMock(return_value={}))
        monkeypatch.setattr(boot_mod, "claim_task", MagicMock(return_value=True))
        monkeypatch.setattr("pulse.brain.boot_briefing.load_recent_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_relevant_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.search_brain_for_task",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_daemon_injections",
                            MagicMock(return_value={
                                "synthetic_constraints": [], "gate_feedback": [],
                                "golden_paths": [], "cross_agent_intel": [],
                            }), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.build_brain_briefing",
                            MagicMock(return_value="FRESH_BRIEFING"), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_curriculum",
                            MagicMock(return_value=None), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_status_card",
                            MagicMock(return_value="STATUS"), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_governance_context",
                            MagicMock(return_value="GOV"), raising=False)

        result = boot_mod.boot("test_agent", "fast", do_claim=True)
        assert result != "STALE_CACHED_DATA", (
            "boot(do_claim=True) must bypass cache and return fresh briefing")

    def test_boot_cache_expires_after_ttl(self, monkeypatch):
        """Cache must expire after _BOOT_CACHE_TTL seconds."""
        import pulse.brain.pulse_boot as boot_mod

        # Pre-fill cache with expired timestamp
        boot_mod._BOOT_CACHE["briefing"] = "OLD_BRIEFING"
        boot_mod._BOOT_CACHE["timestamp"] = time.time() - boot_mod._BOOT_CACHE_TTL - 1
        boot_mod._BOOT_CACHE["cache_key"] = "standard:"

        call_count = {"n": 0}

        def counting_register(*args, **kwargs):
            call_count["n"] += 1
            return {}

        monkeypatch.setattr(boot_mod, "register_agent", counting_register)
        monkeypatch.setattr(boot_mod, "get_task_board", MagicMock(return_value=[]))
        monkeypatch.setattr(boot_mod, "find_matching_task", MagicMock(return_value={}))
        monkeypatch.setattr("pulse.brain.boot_briefing.load_recent_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_relevant_brain",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.search_brain_for_task",
                            MagicMock(return_value={}), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_daemon_injections",
                            MagicMock(return_value={
                                "synthetic_constraints": [], "gate_feedback": [],
                                "golden_paths": [], "cross_agent_intel": [],
                            }), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.build_brain_briefing",
                            MagicMock(return_value="FRESH"), raising=False)
        monkeypatch.setattr("pulse.brain.boot_briefing.load_curriculum",
                            MagicMock(return_value=None), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_status_card",
                            MagicMock(return_value="STATUS"), raising=False)
        monkeypatch.setattr("pulse.brain.boot.formatters_ext.build_governance_context",
                            MagicMock(return_value="GOV"), raising=False)

        boot_mod.boot("test_agent", "standard", do_claim=False)

        assert call_count["n"] >= 1, (
            "Expired cache: boot() should re-run and call register_agent again. "
            "Cache TTL check missing in boot()."
        )

    def test_boot_cache_ttl_is_positive(self):
        """_BOOT_CACHE_TTL must be a positive number."""
        import pulse.brain.pulse_boot as boot_mod
        assert boot_mod._BOOT_CACHE_TTL > 0, (
            f"_BOOT_CACHE_TTL must be positive, got {boot_mod._BOOT_CACHE_TTL}")

    def test_boot_source_writes_cache(self):
        """boot() source code must write to _BOOT_CACHE."""
        import inspect
        import pulse.brain.pulse_boot as boot_mod
        source = inspect.getsource(boot_mod.boot)
        assert '_BOOT_CACHE["briefing"]' in source or "_BOOT_CACHE['briefing']" in source, (
            "boot() must write result to _BOOT_CACHE['briefing']. "
            "Item 100 cache-write wiring missing from boot() source."
        )

    def test_boot_source_reads_cache(self):
        """boot() source code must read _BOOT_CACHE to short-circuit."""
        import inspect
        import pulse.brain.pulse_boot as boot_mod
        source = inspect.getsource(boot_mod.boot)
        assert "_BOOT_CACHE" in source, (
            "boot() must reference _BOOT_CACHE to enable cache read short-circuit. "
            "Item 100 cache-read wiring missing from boot() source."
        )
