"""Integration tests: Wire S6 — GWT broadcast wiring + calibration SQLite persistence.

Wire S6-1: global_workspace.run_workspace_ignition() calls gwt_broadcast() from
           specialists after building the broadcast dict.

Wire S6-2: metamemory._record_calibration() persists entries to the SQLite
           predictions table (domain='calibration').

Wire S6-3: metamemory._get_calibration_factor() loads history from SQLite on a
           cold start (empty _CALIBRATION_HISTORY) so it can produce a non-1.0
           factor when sufficient data exists.
"""
import inspect
import sqlite3
import sys
import time
import uuid
from pathlib import Path
from unittest.mock import patch

import pytest

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


# ---------------------------------------------------------------------------
# Shared fixture: in-memory SQLite with predictions table
# ---------------------------------------------------------------------------

@pytest.fixture()
def mem_conn():
    """Return an in-memory sqlite3.Connection with the predictions table ready."""
    conn = sqlite3.connect(":memory:", check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("""
        CREATE TABLE predictions (
            id TEXT PRIMARY KEY,
            content TEXT NOT NULL,
            confidence REAL NOT NULL DEFAULT 0.5,
            domain TEXT NOT NULL DEFAULT 'general',
            agent TEXT NOT NULL DEFAULT 'unknown',
            salience REAL NOT NULL DEFAULT 1.0,
            timestamp TEXT NOT NULL DEFAULT '',
            outcome TEXT DEFAULT NULL,
            verified INTEGER DEFAULT 0,
            metadata TEXT DEFAULT '{}'
        )
    """)
    conn.commit()
    return conn


# ---------------------------------------------------------------------------
# Wire S6-1: global_workspace calls gwt_broadcast
# ---------------------------------------------------------------------------

class TestWireS6_1_GwtBroadcastWired:
    def test_global_workspace_source_references_gwt_broadcast(self):
        """run_workspace_ignition source must import and call gwt_broadcast."""
        from pulse.brain.workspace import global_workspace
        src = inspect.getsource(global_workspace.run_workspace_ignition)
        assert "gwt_broadcast" in src, (
            "Wire S6-1 missing: run_workspace_ignition() does not call gwt_broadcast"
        )

    def test_global_workspace_imports_gwt_broadcast_from_specialists(self):
        """The import of gwt_broadcast must reference pulse.brain.specialists."""
        from pulse.brain.workspace import global_workspace
        src = inspect.getsource(global_workspace.run_workspace_ignition)
        assert "from pulse.brain.workspace.specialists import gwt_broadcast" in src or (
            "specialists" in src and "gwt_broadcast" in src
        ), "Wire S6-1: gwt_broadcast not imported from pulse.brain.specialists"

    def test_gwt_broadcast_called_after_broadcast_dict_built(self):
        """gwt_broadcast call must appear after the broadcast dict is created."""
        from pulse.brain.workspace import global_workspace
        src = inspect.getsource(global_workspace.run_workspace_ignition)
        broadcast_pos = src.find('"winning_sources"')
        gwt_pos = src.find("gwt_broadcast")
        assert gwt_pos > broadcast_pos, (
            "Wire S6-1: gwt_broadcast must be called after broadcast dict is assembled"
        )

    def test_gwt_broadcast_wrapped_in_try_except(self):
        """The gwt_broadcast call must be in a try/except to be non-fatal."""
        from pulse.brain.workspace import global_workspace
        src = inspect.getsource(global_workspace.run_workspace_ignition)
        # Find try block containing gwt_broadcast
        try_pos = src.rfind("try:", 0, src.find("gwt_broadcast"))
        assert try_pos != -1, (
            "Wire S6-1: gwt_broadcast must be wrapped in try/except (fail-safe)"
        )

    def test_gwt_broadcast_returns_integer_notified_count(self):
        """gwt_broadcast() should return an int (notified specialist count)."""
        from pulse.brain.workspace.specialists import gwt_broadcast
        result = gwt_broadcast({"items": [], "query": "test", "agent": "test"})
        assert isinstance(result, int)

    def test_run_workspace_ignition_still_returns_broadcast(self, monkeypatch):
        """Even if gwt_broadcast raises, run_workspace_ignition returns a dict."""
        from pulse.brain.workspace import specialists, global_workspace

        # Patch gwt_broadcast to raise so we confirm the except swallows it
        monkeypatch.setattr(specialists, "gwt_broadcast", lambda *a, **kw: (_ for _ in ()).throw(RuntimeError("boom")))

        result = global_workspace.run_workspace_ignition(
            agent="test_agent",
            query="test query about brain",
            search_results={},
            task_type="general",
        )
        assert isinstance(result, dict)
        assert "items" in result


# ---------------------------------------------------------------------------
# Wire S6-2: _record_calibration persists to SQLite
# ---------------------------------------------------------------------------

class TestWireS6_2_CalibrationSQLitePersist:
    def test_record_calibration_source_has_sqlite_write(self):
        """_record_calibration source must include SQLite INSERT."""
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory._record_calibration)
        assert "INSERT INTO predictions" in src, (
            "Wire S6-2: _record_calibration must INSERT into predictions table"
        )

    def test_record_calibration_source_uses_calibration_domain(self):
        """The SQLite row must use domain='calibration'."""
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory._record_calibration)
        assert "'calibration'" in src or '"calibration"' in src, (
            "Wire S6-2: INSERT must set domain='calibration'"
        )

    def test_record_calibration_persists_row_to_db(self, mem_conn):
        """Calling _record_calibration writes a row queryable from brain_db."""
        from pulse.brain import metamemory
        import pulse.core.brain_db as bdb

        predicted = round(0.60 + (time.time() % 0.3), 4)  # unique-ish value

        with patch.object(bdb, "get_conn", return_value=mem_conn):
            metamemory._record_calibration(predicted, True)

        rows = mem_conn.execute(
            "SELECT content, confidence, domain FROM predictions "
            "WHERE domain='calibration' ORDER BY timestamp DESC LIMIT 10"
        ).fetchall()
        contents = [r["content"] for r in rows]
        # At least one row should encode our predicted confidence
        matched = any(f"{predicted:.3f}" in c for c in contents)
        assert matched, (
            f"Wire S6-2: expected calibration row with confidence={predicted:.3f} in DB. "
            f"Got contents: {contents[:3]}"
        )

    def test_record_calibration_encodes_actual_outcome(self, mem_conn):
        """Row content must encode actual outcome (1 for success, 0 for failure)."""
        from pulse.brain import metamemory
        import pulse.core.brain_db as bdb

        with patch.object(bdb, "get_conn", return_value=mem_conn):
            metamemory._record_calibration(0.55, False)  # failure case
            metamemory._record_calibration(0.75, True)   # success case

        rows = mem_conn.execute(
            "SELECT content FROM predictions WHERE domain='calibration' "
            "ORDER BY timestamp DESC LIMIT 10"
        ).fetchall()
        contents = [r["content"] for r in rows]
        has_failure = any(":0" in c for c in contents)
        has_success = any(":1" in c for c in contents)
        assert has_failure, "Wire S6-2: failure outcome (actual=0) not found in DB"
        assert has_success, "Wire S6-2: success outcome (actual=1) not found in DB"

    def test_record_calibration_wrapped_in_try_except(self):
        """SQLite write in _record_calibration must be guarded by try/except."""
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory._record_calibration)
        # The try for the SQLite block should appear after the in-memory append
        append_pos = src.find("_CALIBRATION_HISTORY.append")
        try_pos = src.find("try:", append_pos)
        assert try_pos != -1, (
            "Wire S6-2: SQLite write must be wrapped in try/except (fail-safe)"
        )


# ---------------------------------------------------------------------------
# Wire S6-3: _get_calibration_factor loads from SQLite on cold start
# ---------------------------------------------------------------------------

class TestWireS6_3_CalibrationColdStartLoad:
    def test_get_calibration_factor_source_reads_from_sqlite(self):
        """_get_calibration_factor must contain a SELECT from predictions."""
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory._get_calibration_factor)
        assert "SELECT" in src and "predictions" in src, (
            "Wire S6-3: _get_calibration_factor must SELECT from predictions table"
        )

    def test_get_calibration_factor_source_filters_calibration_domain(self):
        """SELECT must filter WHERE domain='calibration'."""
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory._get_calibration_factor)
        assert "domain='calibration'" in src or 'domain="calibration"' in src, (
            "Wire S6-3: SELECT must filter by domain='calibration'"
        )

    def test_get_calibration_factor_cold_start_loads_history(self, mem_conn):
        """After seeding DB with 10+ calibration rows, factor should differ from 1.0 on cold start."""
        import pulse.brain.metacognition.metamemory as mm
        import pulse.core.brain_db as bdb

        # Seed the in-memory DB with known overconfident data: predict 0.9, actual always 0 (fail)
        ts_base = "2026-01-01T00:00:00+00:00"
        for i in range(15):
            mem_conn.execute(
                "INSERT OR REPLACE INTO predictions (id, content, confidence, domain, timestamp) "
                "VALUES (?, ?, ?, ?, ?)",
                (
                    f"cal-coldstart-test-{i}-{uuid.uuid4().hex[:8]}",
                    "calibration:0.900:0",
                    0.9,
                    "calibration",
                    ts_base,
                ),
            )
        mem_conn.commit()

        # Cold start — history is empty, should load from DB
        saved = list(mm._CALIBRATION_HISTORY)
        mm._CALIBRATION_HISTORY.clear()
        try:
            with patch.object(bdb, "get_conn", return_value=mem_conn):
                factor = mm._get_calibration_factor()
        finally:
            mm._CALIBRATION_HISTORY.clear()
            mm._CALIBRATION_HISTORY.extend(saved)

        # With predicted=0.9 and actual=0.0, factor = 0.0/0.9 → clamped to 0.5
        assert factor < 1.0, (
            f"Wire S6-3: expected factor < 1.0 (overconfident case), got {factor}"
        )
        assert factor >= 0.5, f"Wire S6-3: factor should be clamped >= 0.5, got {factor}"

    def test_get_calibration_factor_returns_1_when_no_data(self):
        """Returns exactly 1.0 when DB has no calibration rows and history is empty."""
        import pulse.brain.metacognition.metamemory as mm

        # Save real history, clear it
        saved = list(mm._CALIBRATION_HISTORY)
        mm._CALIBRATION_HISTORY.clear()

        # Patch get_conn to return empty rows (simulate fresh DB)
        import pulse.core.brain_db as bdb
        orig_get_conn = bdb.get_conn

        class _FakeConn:
            def execute(self, *a, **kw):
                return self
            def fetchall(self):
                return []

        bdb.get_conn = lambda: _FakeConn()
        try:
            factor = mm._get_calibration_factor()
        finally:
            bdb.get_conn = orig_get_conn
            mm._CALIBRATION_HISTORY.extend(saved)

        assert factor == 1.0, (
            f"Wire S6-3: expected 1.0 with no data, got {factor}"
        )

    def test_get_calibration_factor_cold_start_guards_malformed_rows(self, mem_conn):
        """Malformed content rows in DB must not crash _get_calibration_factor."""
        import pulse.brain.metacognition.metamemory as mm
        import pulse.core.brain_db as bdb

        # Insert a malformed row (4 parts instead of 3)
        mem_conn.execute(
            "INSERT OR REPLACE INTO predictions (id, content, confidence, domain, timestamp) "
            "VALUES (?, ?, ?, ?, ?)",
            ("cal-malformed-test", "not:valid:content:extra:parts", 0.5, "calibration", "2026-01-01T00:00:00+00:00"),
        )
        mem_conn.commit()

        saved = list(mm._CALIBRATION_HISTORY)
        mm._CALIBRATION_HISTORY.clear()
        try:
            with patch.object(bdb, "get_conn", return_value=mem_conn):
                # Should not raise
                factor = mm._get_calibration_factor()
        finally:
            mm._CALIBRATION_HISTORY.clear()
            mm._CALIBRATION_HISTORY.extend(saved)

        assert isinstance(factor, float)
