from pathlib import Path
"""P44 critical security tests — SQL injection whitelist on brain_db.insert_knowledge."""
import sys
import os
import pytest

# Ensure project root is on path for direct execution
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from pulse.core.brain_db import _VALID_TABLES, _validate_table


# ── Unit tests for _VALID_TABLES and _validate_table ──────────────────────────

class TestValidTables:
    def test_whitelist_contains_expected_names(self):
        expected = {
            "lessons", "failures", "patterns", "private", "intents",
            "decisions", "facts", "evidence", "predictions", "knowledge_fts",
        }
        assert expected.issubset(_VALID_TABLES), (
            f"Missing from _VALID_TABLES: {expected - _VALID_TABLES}"
        )

    def test_validate_table_accepts_valid_name(self):
        result = _validate_table("lessons")
        assert result == "lessons"

    def test_validate_table_raises_on_injection(self):
        with pytest.raises(ValueError, match="Invalid table name"):
            _validate_table("lessons; DROP TABLE patterns; --")

    def test_validate_table_raises_on_nonexistent(self):
        with pytest.raises(ValueError, match="Invalid table name"):
            _validate_table("nonexistent_table")

    def test_validate_table_raises_on_empty(self):
        with pytest.raises(ValueError):
            _validate_table("")

    def test_validate_table_raises_on_semicolon_variant(self):
        with pytest.raises(ValueError):
            _validate_table("facts; SELECT * FROM secrets --")


# ── Integration smoke test for insert_knowledge ────────────────────────────────

class TestInsertKnowledgeTableValidation:
    """These tests call insert_knowledge and rely on the validation at its entry point."""

    def test_insert_valid_table_does_not_raise(self, tmp_path, monkeypatch):
        import pulse.core.brain_db as bdb
        # Point DB at a temp file so we don't pollute the real brain
        monkeypatch.setattr(bdb, "DB_PATH", tmp_path / "test_brain.db")
        # Reset init flag so the temp DB gets initialised
        monkeypatch.setattr(bdb, "_db_initialized", False)
        if hasattr(bdb._local, "conn"):
            del bdb._local.conn

        result = bdb.insert_knowledge("lessons", {
            "content": "test lesson for sql injection check",
            "domain": "test",
            "agent": "test_agent",
        })
        assert result is not None

    def test_insert_injection_string_raises_before_db(self, tmp_path, monkeypatch):
        import pulse.core.brain_db as bdb
        monkeypatch.setattr(bdb, "DB_PATH", tmp_path / "test_brain2.db")
        monkeypatch.setattr(bdb, "_db_initialized", False)
        if hasattr(bdb._local, "conn"):
            del bdb._local.conn

        with pytest.raises(ValueError, match="Invalid table name"):
            bdb.insert_knowledge("lessons; DROP TABLE patterns; --", {
                "content": "malicious",
            })

    def test_insert_unknown_table_raises(self, tmp_path, monkeypatch):
        import pulse.core.brain_db as bdb
        monkeypatch.setattr(bdb, "DB_PATH", tmp_path / "test_brain3.db")
        monkeypatch.setattr(bdb, "_db_initialized", False)
        if hasattr(bdb._local, "conn"):
            del bdb._local.conn

        with pytest.raises(ValueError, match="Invalid table name"):
            bdb.insert_knowledge("nonexistent_table", {
                "content": "should not reach db",
            })


# ── API Auth middleware tests ────────────────────────────────────────────────
# Verifies that PULSE_SESSION_TOKEN=="" no longer bypasses auth on /v1/ routes.
#
# NOTE: FastAPI's TestClient sends "testclient" as the ASGI client host, which
# is not a valid IP — ipaddress.ip_address("testclient") raises ValueError, so
# is_local=False and requests hit the remote-access branch instead of localhost.
# We use httpx.ASGITransport with an explicit loopback client tuple.

import asyncio
import importlib
from unittest.mock import patch
import httpx


def _get_api_app(session_token: str):
    """Load (or reload) pulse_api with PULSE_SESSION_TOKEN patched."""
    env_patch = {"PULSE_SESSION_TOKEN": session_token, "PULSE_API_TOKEN": ""}
    with patch.dict(os.environ, env_patch, clear=False):
        if "pulse.api.pulse_api" in sys.modules:
            api_module = importlib.reload(sys.modules["pulse.api.pulse_api"])
        else:
            import pulse.api.pulse_api as api_module
    return api_module.app


def _sync_get(app, path: str, headers: dict = None) -> httpx.Response:
    """Run a GET against *app* with loopback client IP via ASGITransport."""
    async def _run():
        transport = httpx.ASGITransport(app=app, client=("127.0.0.1", 12345))
        async with httpx.AsyncClient(
            transport=transport, base_url="http://testserver"
        ) as c:
            return await c.get(path, headers=headers or {})
    return asyncio.get_event_loop().run_until_complete(_run())


class TestAPIAuthNoToken:
    """/v1/ endpoints must be blocked when PULSE_SESSION_TOKEN is not set."""

    @pytest.fixture(scope="class")
    def app(self):
        return _get_api_app(session_token="")

    def test_healthz_always_200(self, app):
        """/healthz is outside /v1/ — must always be reachable without auth."""
        resp = _sync_get(app, "/healthz")
        assert resp.status_code == 200, (
            f"Expected 200 from /healthz, got {resp.status_code}: {resp.text}"
        )

    def test_brain_stats_403_when_no_token_configured(self, app):
        """/v1/brain/stats must return 403 when PULSE_SESSION_TOKEN is empty."""
        resp = _sync_get(app, "/v1/brain/stats")
        assert resp.status_code == 403, (
            f"Expected 403 from /v1/brain/stats (no token), "
            f"got {resp.status_code}: {resp.text}"
        )
        detail = resp.json().get("detail", "")
        assert "PULSE_API_TOKEN not configured" in detail, (
            f"Expected config-error message, got: {detail!r}"
        )


class TestAPIAuthWithToken:
    """/v1/ endpoints must accept requests with the correct session token."""

    TOKEN = "test-secret-token-abc123"

    @pytest.fixture(scope="class")
    def app(self):
        return _get_api_app(session_token=self.TOKEN)

    def test_healthz_still_200_with_token(self, app):
        """/healthz remains reachable regardless of token configuration."""
        resp = _sync_get(app, "/healthz")
        assert resp.status_code == 200, (
            f"Expected 200 from /healthz, got {resp.status_code}"
        )

    def test_brain_stats_200_with_correct_token(self, app):
        """/v1/brain/stats must return 200 when the correct X-Pulse-Session is sent."""
        resp = _sync_get(app, "/v1/brain/stats",
                         headers={"X-Pulse-Session": self.TOKEN})
        assert resp.status_code == 200, (
            f"Expected 200 from /v1/brain/stats with correct token, "
            f"got {resp.status_code}: {resp.text}"
        )

    def test_brain_stats_401_with_wrong_token(self, app):
        """/v1/brain/stats must return 401 when an incorrect token is sent."""
        resp = _sync_get(app, "/v1/brain/stats",
                         headers={"X-Pulse-Session": "wrong-token"})
        assert resp.status_code == 401, (
            f"Expected 401 from /v1/brain/stats with wrong token, "
            f"got {resp.status_code}: {resp.text}"
        )


# ── PulseCrypto: verify_integrity / verify_transit split (P44 fix) ────────────

import time as _time
from pulse.core.pulse_crypto import PulseCrypto as _PulseCrypto

_CRYPTO_KEY = "p44-test-secret-key"
_TEN_MIN_AGO = _time.time() - 600


def _make_crypto() -> _PulseCrypto:
    return _PulseCrypto(secret_key=_CRYPTO_KEY)


class TestVerifyIntegrityNoReplayCheck:
    """verify_integrity() must pass for any correctly-signed item regardless of age."""

    def test_fresh_item_passes(self):
        crypto = _make_crypto()
        item = {"content": "fresh brain item", "domain": "crypto"}
        sealed = crypto.seal(item, "agent-test")
        assert crypto.verify_integrity(sealed)

    def test_old_item_still_passes(self):
        """An item backdated 10 minutes must still pass verify_integrity()."""
        crypto = _make_crypto()
        item = {"content": "old brain item", "domain": "crypto"}
        sealed = crypto.seal(item, "agent-test")
        # Backdate the timestamp and re-sign so the HMAC covers the new timestamp
        sealed["_signed_at"] = _TEN_MIN_AGO
        sealed["_signature"] = crypto.sign(sealed)
        assert crypto.verify_integrity(sealed)

    def test_verify_alias_matches_verify_integrity(self):
        """verify() must produce the same result as verify_integrity()."""
        crypto = _make_crypto()
        item = {"content": "alias check"}
        sealed = crypto.seal(item, "agent-test")
        sealed["_signed_at"] = _TEN_MIN_AGO
        sealed["_signature"] = crypto.sign(sealed)
        assert crypto.verify(sealed) == crypto.verify_integrity(sealed)
        assert crypto.verify(sealed) is True


class TestVerifyTransitReplayCheck:
    """verify_transit() must reject items older than REPLAY_WINDOW."""

    def test_fresh_item_passes_transit(self):
        crypto = _make_crypto()
        item = {"content": "message", "domain": "transit"}
        sealed = crypto.seal(item, "agent-test")
        assert crypto.verify_transit(sealed)

    def test_old_item_rejected_by_transit(self):
        """An item backdated 10 minutes must fail verify_transit()."""
        crypto = _make_crypto()
        item = {"content": "stale message", "domain": "transit"}
        sealed = crypto.seal(item, "agent-test")
        sealed["_signed_at"] = _TEN_MIN_AGO
        sealed["_signature"] = crypto.sign(sealed)
        assert not crypto.verify_transit(sealed)

    def test_tampered_transit_fails_hmac(self):
        crypto = _make_crypto()
        item = {"content": "real message"}
        sealed = crypto.seal(item, "agent-test")
        sealed["content"] = "tampered"
        assert not crypto.verify_transit(sealed)


class TestSignedAtInHmac:
    """_signed_at is now part of the HMAC payload — tampering with it must
    break verify_integrity()."""

    def test_tampered_signed_at_breaks_verify_integrity(self):
        """After sealing, changing _signed_at without re-signing must fail."""
        crypto = _make_crypto()
        item = {"content": "integrity test"}
        sealed = crypto.seal(item, "agent-test")
        # Tamper with the timestamp only; leave _signature unchanged
        sealed["_signed_at"] = sealed["_signed_at"] - 3600
        assert not crypto.verify_integrity(sealed)

    def test_different_signed_at_produces_different_signature(self):
        """sign() must produce different outputs when _signed_at differs."""
        crypto = _make_crypto()
        base = {"content": "data", "_signed_at": 1000.0}
        modified = {"content": "data", "_signed_at": 2000.0}
        assert crypto.sign(base) != crypto.sign(modified)

    def test_signed_at_absent_vs_present_differ(self):
        """Omitting _signed_at entirely must differ from having it set."""
        crypto = _make_crypto()
        with_ts = crypto.sign({"content": "data", "_signed_at": 1000.0})
        without_ts = crypto.sign({"content": "data"})
        assert with_ts != without_ts

    def test_re_signing_after_backdate_passes_integrity(self):
        """If _signed_at is backdated AND the signature is updated, integrity holds."""
        crypto = _make_crypto()
        item = {"content": "backdated"}
        sealed = crypto.seal(item, "agent-test")
        sealed["_signed_at"] = _TEN_MIN_AGO
        sealed["_signature"] = crypto.sign(sealed)
        assert crypto.verify_integrity(sealed)


class TestCryptoBackwardCompatibility:
    """Existing callers of verify() must see no regression for correctly-signed items."""

    def test_sealed_item_verifies_via_legacy_method(self):
        crypto = _make_crypto()
        item = {"text": "legacy caller", "confidence": 0.9}
        sealed = crypto.seal(item, "legacy-agent")
        assert crypto.verify(sealed)

    def test_tampered_item_fails_via_legacy_method(self):
        crypto = _make_crypto()
        item = {"text": "original"}
        sealed = crypto.seal(item.copy(), "agent")
        sealed["text"] = "tampered"
        assert not crypto.verify(sealed)

    def test_wrong_key_fails_via_legacy_method(self, monkeypatch):
        monkeypatch.delenv("PULSE_KEY_VERSION", raising=False)
        c1 = _PulseCrypto(secret_key="key-A")
        c2 = _PulseCrypto(secret_key="key-B")
        sealed = c1.seal({"text": "data"}, "agent")
        assert not c2.verify(sealed)


# ---------------------------------------------------------------------------
# STDP anti-causal LTD fix — Bi & Poo 1998 compliance
# ---------------------------------------------------------------------------
import json
import sqlite3
import tempfile
from datetime import datetime, timezone, timedelta
from unittest.mock import patch


def _make_stdp_conn():
    """In-memory SQLite DB with graph_edges schema for stdp._upsert_temporal_edge."""
    conn = sqlite3.connect(":memory:")
    conn.row_factory = sqlite3.Row
    conn.execute(
        """CREATE TABLE graph_edges (
            id        INTEGER PRIMARY KEY AUTOINCREMENT,
            from_node TEXT NOT NULL,
            to_node   TEXT NOT NULL,
            edge_type TEXT NOT NULL DEFAULT 'TEMPORAL',
            confidence REAL NOT NULL DEFAULT 0.0,
            metadata  TEXT NOT NULL DEFAULT '{}'
        )"""
    )
    conn.commit()
    return conn


def _run_stdp_pair(success: bool):
    """
    Run apply_stdp for a two-item session A->B (A retrieved before B, gap=5s).
    Seeds both A->B and B->A edges with conf=0.5, coactivation_count=5 (past silent
    synapse threshold) so _upsert_temporal_edge actually applies the delta.
    Returns (ab_conf, ba_conf); None means edge was deleted (conf<0.01).
    """
    import pulse.brain.plasticity.stdp as stdp_mod

    conn = _make_stdp_conn()
    meta = json.dumps({"coactivation_count": 5})
    conn.execute(
        "INSERT INTO graph_edges (from_node,to_node,edge_type,confidence,metadata) "
        "VALUES ('A','B','TEMPORAL',0.5,?)", (meta,)
    )
    conn.execute(
        "INSERT INTO graph_edges (from_node,to_node,edge_type,confidence,metadata) "
        "VALUES ('B','A','TEMPORAL',0.5,?)", (meta,)
    )
    conn.commit()

    session_id = "stdp-test-pair"
    now = datetime.now(timezone.utc)
    seq = [
        {"item_id": "A", "ts": now.isoformat()},
        {"item_id": "B", "ts": (now + timedelta(seconds=5)).isoformat()},
    ]

    with tempfile.TemporaryDirectory() as tmpdir:
        sf = Path(tmpdir) / "session_retrievals.json"
        sf.write_text(json.dumps({session_id: seq}), encoding="utf-8")
        with (
            patch.object(stdp_mod, "SESSIONS_FILE", sf),
            patch("pulse.core.brain_db.get_conn", return_value=conn),
        ):
            stdp_mod.apply_stdp(session_id, success=success)

    ab_row = conn.execute(
        "SELECT confidence FROM graph_edges "
        "WHERE from_node='A' AND to_node='B' AND edge_type='TEMPORAL'"
    ).fetchone()
    ba_row = conn.execute(
        "SELECT confidence FROM graph_edges "
        "WHERE from_node='B' AND to_node='A' AND edge_type='TEMPORAL'"
    ).fetchone()
    return (ab_row["confidence"] if ab_row else None,
            ba_row["confidence"] if ba_row else None)


class TestStdpAntiCausalLTD:
    """Verify Bi & Poo 1998: anti-causal stimulation (B fires before A reads) -> LTD on success."""

    def test_success_causal_ltp(self):
        """Success: causal edge A->B must strengthen above seed 0.5."""
        ab_conf, _ = _run_stdp_pair(success=True)
        assert ab_conf is not None, "A->B edge should exist after LTP"
        assert ab_conf > 0.5, f"Causal A->B should be > 0.5 on success, got {ab_conf}"

    def test_success_anti_causal_ltd(self):
        """Success: anti-causal edge B->A must weaken below seed 0.5 (LTD per Bi & Poo 1998)."""
        _, ba_conf = _run_stdp_pair(success=True)
        # Deleted edge (ba_conf is None) also satisfies LTD
        if ba_conf is not None:
            assert ba_conf < 0.5, (
                f"Anti-causal B->A should be < 0.5 on success (LTD), got {ba_conf}. "
                "Post-fires-before-pre must produce LTD per Bi & Poo 1998."
            )

    def test_failure_causal_ltd(self):
        """Failure: causal edge A->B must weaken below seed 0.5."""
        ab_conf, _ = _run_stdp_pair(success=False)
        assert ab_conf is not None, "A->B should not be deleted on failure LTD"
        assert ab_conf < 0.5, f"Causal A->B should be < 0.5 on failure, got {ab_conf}"

    def test_failure_anti_causal_mild_ltp(self):
        """Failure: anti-causal edge B->A should get mild LTP (>= seed 0.5)."""
        _, ba_conf = _run_stdp_pair(success=False)
        if ba_conf is not None:
            assert ba_conf >= 0.5, (
                f"Anti-causal B->A should get mild LTP on failure (>= 0.5), got {ba_conf}"
            )

    def test_anti_causal_sign_comment_in_source(self):
        """Regression guard: anti-causal LTD fix comment must exist in stdp.py."""
        import inspect
        import pulse.brain.plasticity.stdp as stdp_mod
        src = inspect.getsource(stdp_mod)
        assert "anti-causal" in src.lower(), "anti-causal comment missing from stdp.py"
        assert "LTD" in src, "LTD keyword missing from stdp.py"

    def test_burst_buffer_time_based_not_count_based(self):
        """Regression guard: _check_burst must use time window, not a hard count cap."""
        import inspect
        import pulse.brain.plasticity.stdp as stdp_mod
        src = inspect.getsource(stdp_mod._check_burst)
        assert "len(_BURST_TIMESTAMPS) > 10" not in src, (
            "Old count-based cap must be removed from _check_burst"
        )
        assert "_BURST_WINDOW" in src, "_check_burst must use _BURST_WINDOW for time-based cleanup"


# ---------------------------------------------------------------------------
# C7: worker_tools.py -- dispatch_tasks -> dispatch fix
# ---------------------------------------------------------------------------
from unittest.mock import MagicMock, patch


def test_c7_tool_submit_task_calls_dispatch_not_dispatch_tasks():
    """tool_submit_task must call dispatcher.dispatch(), not dispatcher.dispatch_tasks()."""
    import pulse.workers.worker_tools as wt

    mock_dispatcher = MagicMock()
    mock_dispatcher._hmac_key = "fake-hmac-key"

    with (
        patch("pulse.tasks.pulse_dispatcher.TaskDispatcher", return_value=mock_dispatcher),
        patch("pulse.tasks.pulse_relay.parse_manual_tasks", return_value=[{"title": "test"}]),
    ):
        result = wt.tool_submit_task("test task", domain="general", complexity="medium")

    mock_dispatcher.dispatch.assert_called_once()
    mock_dispatcher.dispatch_tasks.assert_not_called()
    assert "OK" in result


def test_c7_dispatch_method_name_in_source():
    """Regression guard: source must not contain the broken dispatch_tasks call."""
    import inspect
    import pulse.workers.worker_tools as wt
    src = inspect.getsource(wt.tool_submit_task)
    assert "dispatch_tasks" not in src, (
        "dispatch_tasks() call must be removed from tool_submit_task -- use dispatch()"
    )
    assert "dispatcher.dispatch(" in src, "dispatcher.dispatch() call must be present"


# ---------------------------------------------------------------------------
# C8: reward_propagation.py -- relevance key in evidence dict
# ---------------------------------------------------------------------------

def test_c8_evidence_dict_contains_relevance_key():
    """maybe_promote must receive an evidence dict with a relevance key != 1.0."""
    captured_calls = []

    import pulse.core.reward.reward_propagation as rp

    # Build a conn mock that returns different rows based on call count
    call_count = [0]

    class FakeRow(dict):
        """Dict subclass that also supports integer indexing for sqlite3 Row compat."""
        def __getitem__(self, key):
            return super().__getitem__(key)

    def fake_execute(sql, params=()):
        result = MagicMock()
        # First call: FTS lookup for table_name
        if "knowledge_fts" in sql:
            result.fetchone.return_value = FakeRow({"table_name": "lessons"})
        # salience SELECT
        elif "SELECT salience" in sql:
            result.fetchone.return_value = [3.0]  # list-style, row[0]
        # confidence SELECT
        elif "SELECT confidence" in sql:
            result.fetchone.return_value = FakeRow({"confidence": 0.8})
        # domain SELECT
        elif "SELECT domain" in sql:
            result.fetchone.return_value = FakeRow({"domain": "test"})
        else:
            result.fetchone.return_value = None
        return result

    fake_conn = MagicMock()
    fake_conn.execute.side_effect = fake_execute
    fake_conn.__enter__ = MagicMock(return_value=fake_conn)
    fake_conn.__exit__ = MagicMock(return_value=False)

    def mock_maybe_promote(item_id, table, trigger, evidence=None):
        captured_calls.append(evidence or {})
        return False

    with (
        patch("pulse.core.brain_db.get_conn", return_value=fake_conn),
        patch("pulse.brain.reconsolidation.reinforce"),
        patch("pulse.brain.reconsolidation.contradict"),
        patch("pulse.brain.reconsolidation.mark_labile"),
        patch("pulse.brain.integrity.grounding.maybe_promote", mock_maybe_promote),
    ):
        rp.propagate_reward({"item-001": 0.8})

    assert len(captured_calls) >= 1, "maybe_promote must have been called at least once"
    ev = captured_calls[0]
    assert "relevance" in ev, f"evidence dict must contain relevance key; got: {ev}"
    assert ev["relevance"] != 1.0, (
        "relevance must not default to 1.0 (gate always-pass)"
    )


def test_c8_relevance_default_is_neutral():
    """When no relevance data is available, relevance must be 0.5 (neutral)."""
    import inspect
    import pulse.core.reward.reward_propagation as rp
    src = inspect.getsource(rp.propagate_reward)
    assert '"relevance": 0.5' in src or "'relevance': 0.5" in src, (
        "Default relevance must be 0.5 (neutral), not 1.0 (always-pass)"
    )


# ---------------------------------------------------------------------------
# C9: consolidation_late_stages.py -- abstraction_result NameError fix
# ---------------------------------------------------------------------------

def test_c9_no_name_error_with_abstraction_result():
    """run_late_stages must not raise NameError when abstraction_result is in early dict."""
    from unittest.mock import patch as _patch, MagicMock as _MM

    early = {
        "abstraction_result": {"test": True, "abstractions": 3},
        "start": 0.0,
    }

    _noop = _MM(return_value={"status": "ok"})
    modules_to_patch = [
        "pulse.graph.causal_model.detect_confounders",
        "pulse.daemons.synthesis.planning.goal_engine.formulate_goals",
        "pulse.daemons.synthesis.planning.prospective_memory.check_intentions",
        "pulse.admin.audit.sentinels.run_sentinels",
        "pulse.core.autonomy_state.check_promotion",
        "pulse.brain.pattern_evolution.evolve_patterns",
        "pulse.daemons.synthesis.learning.associative_replay.run_replay",
        "pulse.daemons.consolidation.consolidation_stages.stage_deep_consolidation",
        "pulse.daemons.consolidation.session_replay.run_session_replay",
        "pulse.daemons.synthesis.core.conflict_monitor.scan_for_conflicts",
        "pulse.daemons.consolidation.retrieval_scheduler.schedule_reviews",
        "pulse.core.brain_db.get_conn",
    ]

    import pulse.daemons.consolidation.consolidation_late_stages as cls_mod

    patches = [_patch(m, _noop) for m in modules_to_patch]
    patches.append(_patch.object(cls_mod, "save_json", _MM()))
    patches.append(_patch.object(cls_mod, "CONSOLIDATION_LOG", _MM()))

    for p in patches:
        p.start()
    try:
        result = cls_mod.run_late_stages(verbose=False, early=early)
    finally:
        for p in patches:
            p.stop()

    assert isinstance(result, dict), "run_late_stages must return a dict"
    assert "stage_4c_abstraction" in result, "log dict must include stage_4c_abstraction key"
    assert result["stage_4c_abstraction"] == {"test": True, "abstractions": 3}


def test_c9_abstraction_result_unpacked_in_source():
    """Regression guard: abstraction_result must be unpacked from early in source."""
    import inspect
    import pulse.daemons.consolidation.consolidation_late_stages as cls_mod
    src = inspect.getsource(cls_mod.run_late_stages)
    assert "abstraction_result = early.get(" in src, (
        "abstraction_result must be unpacked from early dict in run_late_stages"
    )



# ---------------------------------------------------------------------------
# P44 capture_to_bridge_log signer fix
# ---------------------------------------------------------------------------
import importlib as _il_p44

_WT_P44 = _il_p44.import_module("pulse.workers." + "worker" + "_tools")
_WEXT_P44 = _il_p44.import_module("pulse.workers." + "worker" + "_tools_ext")


def test_p44_signer_is_callable():
    """_worker_sign must be a callable function in the sibling module."""
    signer = getattr(_WT_P44, "_worker_sign", None)
    assert signer is not None, "_worker_sign missing"
    assert callable(signer), "_worker_sign not callable"


def test_p44_capture_importable():
    """capture_to_bridge_log must be importable without NameError."""
    fn = getattr(_WEXT_P44, "capture_to_bridge_log", None)
    assert fn is not None and callable(fn)


def test_p44_capture_no_nameerror(tmp_path, monkeypatch):
    """capture_to_bridge_log must not raise NameError or any exception."""
    monkeypatch.setattr(_WEXT_P44, "STATE_DIR", tmp_path)
    events = [
        {"type": "reasoning", "content": "thinking...", "timestamp": "2026-01-01T00:00:00"},
        {"type": "complete", "content": "done", "timestamp": "2026-01-01T00:00:01"},
    ]
    _WEXT_P44.capture_to_bridge_log("test_worker", events, "task-001", "Test Task")
    log_file = tmp_path / "pulse_captured_test_worker.log"
    assert log_file.exists(), "Capture log was not created"
    content = log_file.read_text(encoding="utf-8")
    assert "[TASK task-001]" in content
    assert "thinking..." in content
    assert "[DONE]" in content


# ---------------------------------------------------------------------------
# C10: wrapper_orchestrator.py -- wrong path fix
# ---------------------------------------------------------------------------

def test_c10_orchestrator_path_exists():
    """The PULSE_SCRIPT path referenced in wrapper_orchestrator must exist on disk."""
    import pulse.agents.wrapper_orchestrator as wo
    assert wo.PULSE_SCRIPT.exists(), (
        f"PULSE_SCRIPT does not exist: {wo.PULSE_SCRIPT}. "
        "Should point to pulse/daemons/orchestrator/core.py"
    )


def test_c10_orchestrator_path_is_orchestrator_not_pulse():
    """PULSE_SCRIPT must point to orchestrator/core.py, not the non-existent pulse.py."""
    import pulse.agents.wrapper_orchestrator as wo
    assert wo.PULSE_SCRIPT.name == "core.py", (
        f"Expected core.py, got: {wo.PULSE_SCRIPT.name}"
    )
    assert "orchestrator" in str(wo.PULSE_SCRIPT), (
        "PULSE_SCRIPT must reference the orchestrator subpackage"
    )
    assert "pulse.py" not in str(wo.PULSE_SCRIPT), (
        "PULSE_SCRIPT must not reference the non-existent pulse.py"
    )
