"""Integration tests: verify all 6 production wiring points are connected.

Wire 1: Session continuity — checkpoint saved from pulse_save, autopilot loop, and brain query hook
Wire 2: Somatic veto — anti_patterns confidence migration in init_db()
Wire 3: Grounding — maybe_promote called from finalize_task on task success
Wire 4: Working memory chunking — _chunk_similar called inside _evict
Wire 5: Thompson Sampling — should_rotate accepts model/domain params
Wire 6: Volatility — get_volatility_multiplier returns a float
"""
import inspect
import sqlite3
import sys
import tempfile
from pathlib import Path

import pytest

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


# ---------------------------------------------------------------------------
# Wire 1A: pulse_save.py calls save_checkpoint
# ---------------------------------------------------------------------------

class TestWire1PulseSave:
    def test_save_imports_checkpoint(self):
        """pulse_save.save() source must reference save_checkpoint."""
        from pulse.brain import pulse_save
        src = inspect.getsource(pulse_save.save)
        assert "save_checkpoint" in src, (
            "Wire 1A missing: pulse_save.save() does not call save_checkpoint"
        )

    def test_save_checkpoint_import_path(self):
        """The import path must be pulse.brain.session.session_checkpoint."""
        from pulse.brain import pulse_save
        src = inspect.getsource(pulse_save.save)
        assert "pulse.brain.session.session_checkpoint" in src, (
            "Wire 1A: wrong import path — expected pulse.brain.session.session_checkpoint"
        )


# ---------------------------------------------------------------------------
# Wire 1B: autopilot loop saves checkpoint on task completion
# ---------------------------------------------------------------------------

class TestWire1AutopilotLoop:
    def test_loop_calls_checkpoint_on_success(self):
        """loop._run_autopilot_loop source must reference save_checkpoint after gates_passed."""
        from pulse.cli.autopilot import loop
        src = inspect.getsource(loop._run_autopilot_loop)
        assert "save_checkpoint" in src, (
            "Wire 1B missing: autopilot loop does not call save_checkpoint on task completion"
        )

    def test_loop_checkpoint_in_gates_passed_block(self):
        """save_checkpoint must appear after 'gates_passed' success path."""
        from pulse.cli.autopilot import loop
        src = inspect.getsource(loop._run_autopilot_loop)
        gates_idx = src.find("gates_passed")
        checkpoint_idx = src.find("save_checkpoint")
        assert gates_idx != -1 and checkpoint_idx != -1, (
            "Wire 1B: could not find gates_passed or save_checkpoint in loop source"
        )
        assert checkpoint_idx > gates_idx, (
            "Wire 1B: save_checkpoint must appear after gates_passed block"
        )


# ---------------------------------------------------------------------------
# Wire 1C: brain_query cli saves checkpoint on UserPromptSubmit
# ---------------------------------------------------------------------------

class TestWire1BrainQueryCli:
    def test_cli_main_calls_checkpoint(self):
        """brain_query cli.main source must reference save_checkpoint."""
        from pulse.brain.brain_query import cli
        src = inspect.getsource(cli.main)
        assert "save_checkpoint" in src, (
            "Wire 1C missing: brain_query cli.main does not call save_checkpoint"
        )

    def test_cli_checkpoint_on_user_prompt_submit(self):
        """Checkpoint must be guarded by UserPromptSubmit event check."""
        from pulse.brain.brain_query import cli
        src = inspect.getsource(cli.main)
        assert "UserPromptSubmit" in src, (
            "Wire 1C: save_checkpoint not guarded by UserPromptSubmit"
        )


# ---------------------------------------------------------------------------
# Wire 2: Somatic veto — anti_patterns confidence migration
# ---------------------------------------------------------------------------

class TestWire2SomaticVeto:
    def test_confidence_migration_in_init_db(self):
        """init_db() source must contain the confidence migration UPDATE."""
        from pulse.core import brain_db
        src = inspect.getsource(brain_db.init_db)
        assert "confidence = 0.85" in src or "confidence=0.85" in src, (
            "Wire 2 missing: init_db() has no confidence=0.85 migration for anti_patterns"
        )

    def test_confidence_migration_condition(self):
        """Migration must only upgrade rows with evidence_count >= 5."""
        from pulse.core import brain_db
        src = inspect.getsource(brain_db.init_db)
        assert "evidence_count >= 5" in src, (
            "Wire 2: confidence migration must be conditioned on evidence_count >= 5"
        )

    def test_confidence_column_created_in_db(self):
        """Running init_db on a fresh DB must create confidence column in anti_patterns."""
        import importlib
        # Use an isolated in-memory DB by temporarily patching DB_PATH
        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_db = Path(tmpdir) / "test_brain.db"
            import pulse.core.brain_db as bdb
            original_path = bdb.DB_PATH
            original_initialized = bdb._db_initialized
            # Patch to isolated path
            bdb.DB_PATH = tmp_db
            bdb._db_initialized = False
            # Clear thread-local connection so a new one is created for tmp_db
            if hasattr(bdb._local, "conn"):
                try:
                    bdb._local.conn.close()
                except Exception:
                    pass
                del bdb._local.conn
            try:
                conn = bdb.get_conn()
                cols = [r[1] for r in conn.execute("PRAGMA table_info(anti_patterns)").fetchall()]
                assert "confidence" in cols, (
                    "Wire 2: anti_patterns table missing confidence column after init_db()"
                )
            finally:
                bdb.DB_PATH = original_path
                bdb._db_initialized = False
                if hasattr(bdb._local, "conn"):
                    try:
                        bdb._local.conn.close()
                    except Exception:
                        pass
                    del bdb._local.conn


# ---------------------------------------------------------------------------
# Wire 3: Grounding — task_claiming finalize_task calls maybe_promote
# ---------------------------------------------------------------------------

class TestWire3Grounding:
    def test_finalize_task_calls_maybe_promote(self):
        """finalize_task source must reference maybe_promote."""
        from pulse.tasks import task_claiming
        src = inspect.getsource(task_claiming.finalize_task)
        assert "maybe_promote" in src, (
            "Wire 3 missing: finalize_task() does not call maybe_promote"
        )

    def test_finalize_task_imports_grounding(self):
        """finalize_task must import from pulse.brain.integrity.grounding."""
        from pulse.tasks import task_claiming
        src = inspect.getsource(task_claiming.finalize_task)
        assert "pulse.brain.integrity.grounding" in src, (
            "Wire 3: finalize_task() imports grounding from wrong path"
        )

    def test_finalize_task_uses_task_success_trigger(self):
        """Grounding promotion must use trigger='task_success'."""
        from pulse.tasks import task_claiming
        src = inspect.getsource(task_claiming.finalize_task)
        assert "task_success" in src, (
            "Wire 3: maybe_promote must be called with trigger='task_success'"
        )


# ---------------------------------------------------------------------------
# Wire 4: Working memory — _evict calls _chunk_similar
# ---------------------------------------------------------------------------

class TestWire4WorkingMemoryChunking:
    def test_evict_calls_chunk_similar(self):
        """_evict() source must call _chunk_similar."""
        from pulse.brain import working_memory
        src = inspect.getsource(working_memory._evict)
        assert "_chunk_similar" in src, (
            "Wire 4 missing: _evict() does not call _chunk_similar — chunking is dead code"
        )

    def test_chunk_similar_defined(self):
        """_chunk_similar must be a callable in working_memory."""
        from pulse.brain import working_memory
        assert callable(working_memory._chunk_similar), (
            "Wire 4: _chunk_similar is not callable in working_memory"
        )

    def test_chunk_similar_call_is_guarded(self):
        """_chunk_similar call in _evict must be in a try/except to handle missing deps."""
        from pulse.brain import working_memory
        src = inspect.getsource(working_memory._evict)
        chunk_idx = src.find("_chunk_similar")
        # Find the nearest try: before the call
        try_idx = src.rfind("try:", 0, chunk_idx)
        except_idx = src.find("except", chunk_idx)
        assert try_idx != -1 and except_idx != -1, (
            "Wire 4: _chunk_similar call in _evict must be wrapped in try/except"
        )


# ---------------------------------------------------------------------------
# Wire 5: Thompson Sampling — should_rotate accepts model/domain params
# ---------------------------------------------------------------------------

class TestWire5ThompsonSampling:
    def test_should_rotate_signature(self):
        """should_rotate must accept model and domain keyword arguments."""
        from pulse.tasks.pulse_dispatcher import should_rotate
        sig = inspect.signature(should_rotate)
        params = list(sig.parameters.keys())
        assert "model" in params, (
            "Wire 5: should_rotate() missing 'model' parameter — Thompson Sampling needs model"
        )
        assert "domain" in params, (
            "Wire 5: should_rotate() missing 'domain' parameter — Thompson Sampling needs domain"
        )

    def test_should_rotate_callable_with_args(self):
        """should_rotate(model=..., domain=...) must not raise."""
        from pulse.tasks.pulse_dispatcher import should_rotate
        result = should_rotate(model="claude-sonnet", domain="general")
        assert isinstance(result, bool), (
            "Wire 5: should_rotate() must return bool, got %s" % type(result)
        )

    def test_should_rotate_callable_no_args(self):
        """should_rotate() with no args (defaults) must not raise."""
        from pulse.tasks.pulse_dispatcher import should_rotate
        result = should_rotate()
        assert isinstance(result, bool), (
            "Wire 5: should_rotate() with no args must return bool"
        )

    def test_should_rotate_uses_beta_distribution(self):
        """should_rotate source must reference betavariate (Thompson Sampling)."""
        from pulse.tasks import pulse_dispatcher
        src = inspect.getsource(pulse_dispatcher.should_rotate)
        assert "betavariate" in src, (
            "Wire 5: should_rotate() missing betavariate — Thompson Sampling not implemented"
        )


# ---------------------------------------------------------------------------
# Wire 6: Volatility — get_volatility_multiplier returns a meaningful float
# ---------------------------------------------------------------------------

class TestWire6Volatility:
    def test_get_volatility_multiplier_returns_float(self):
        """get_volatility_multiplier() must return a float."""
        from pulse.core.reward.volatility import get_volatility_multiplier
        result = get_volatility_multiplier()
        assert isinstance(result, float), (
            "Wire 6: get_volatility_multiplier() must return float, got %s" % type(result)
        )

    def test_get_volatility_multiplier_in_range(self):
        """Result must be in [0.5, 2.0] (MIN_MULTIPLIER to MAX_MULTIPLIER)."""
        from pulse.core.reward.volatility import get_volatility_multiplier, MIN_MULTIPLIER, MAX_MULTIPLIER
        result = get_volatility_multiplier()
        assert MIN_MULTIPLIER <= result <= MAX_MULTIPLIER, (
            "Wire 6: get_volatility_multiplier() = %.3f, outside [%.1f, %.1f]"
            % (result, MIN_MULTIPLIER, MAX_MULTIPLIER)
        )

    def test_compute_volatility_returns_float(self):
        """compute_volatility() (the recompute path) must also return float."""
        from pulse.core.reward.volatility import compute_volatility
        result = compute_volatility()
        assert isinstance(result, float), (
            "Wire 6: compute_volatility() must return float"
        )

    def test_volatility_has_three_signals(self):
        """compute_volatility source must reference all 3 signal names."""
        from pulse.core.reward import volatility
        src = inspect.getsource(volatility.compute_volatility)
        assert "rpe" in src.lower(), "Wire 6: RPE variance signal missing from compute_volatility"
        assert "ingestion" in src.lower() or "ingestion_signal" in src, (
            "Wire 6: Ingestion rate signal missing from compute_volatility"
        )
        assert "novelty" in src.lower() or "domain" in src.lower(), (
            "Wire 6: Domain novelty signal missing from compute_volatility"
        )
