"""
Tests: Bridge captures interactions from claude capture log.

Verifies that PulseBridge._tail_file correctly reads signed USER/CLAUDE
interaction lines from pulse_captured_claude.log and populates the interaction
buffer (ready for flush to brain).
"""
from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

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

# Load env so PULSE_BRAIN_KEY is available
try:
    from dotenv import load_dotenv
    load_dotenv(PROJECT_ROOT / ".env")
except ImportError:
    pass

from pulse.core.pulse_crypto import PulseCrypto
from pulse.daemons.bridge.pulse_bridge import PulseBridge, LOG_CLAUDE


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_signed_line(crypto: PulseCrypto, ts: str, role: str, text: str) -> str:
    """Build a log line in the format bridge expects: [TS] ROLE: text | SIG: hex"""
    content = f"[{ts}] {role}: {text}"
    sig = crypto.sign_string(content, "claude_daemon")
    return f"{content} | SIG: {sig}"


def _make_bridge_with_temp_state(tmp_path: Path) -> PulseBridge:
    """Create a PulseBridge that uses a temp state file (no real state mutation)."""
    state_file = tmp_path / "bridge_state.json"
    with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", state_file):
        bridge = PulseBridge(verbose=False)
    # Re-point state file so _save_state goes to tmp
    bridge._state_file = state_file  # reference; _save_state uses module-level STATE_FILE
    return bridge


# ---------------------------------------------------------------------------
# Config: skip if PULSE_BRAIN_KEY missing
# ---------------------------------------------------------------------------

pytestmark = pytest.mark.skipif(
    not os.environ.get("PULSE_BRAIN_KEY"),
    reason="PULSE_BRAIN_KEY not set — crypto-dependent bridge tests skipped",
)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestBridgeLogConfig:
    """Verify bridge is wired to the correct claude capture log path."""

    def test_claude_log_path_uses_suffixed_name(self):
        """Bridge must read pulse_captured_claude.log, NOT pulse_captured.log."""
        assert LOG_CLAUDE.name == "pulse_captured_claude.log", (
            f"Expected 'pulse_captured_claude.log', got '{LOG_CLAUDE.name}'. "
            "Capture log naming anti-pattern: bridge reads only suffixed names."
        )

    def test_claude_log_in_state_dir(self):
        """Claude log must live in the state/ directory."""
        assert LOG_CLAUDE.parent.name == "state"


class TestBridgeTailFileReadsNewLines:
    """Bridge _tail_file advances position and processes new lines."""

    def test_reads_from_position_zero_on_new_file(self, tmp_path):
        crypto = PulseCrypto()
        log = tmp_path / "pulse_captured_claude.log"
        ts = "2026-03-18 10:00:00"

        user_line = _make_signed_line(crypto, ts, "USER", "Help me fix this bug")
        claude_line = _make_signed_line(crypto, ts, "CLAUDE", "Sure, let me look at the code")
        log.write_text(f"{user_line}\n{claude_line}\n", encoding="utf-8")

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", log), \
             patch.object(PulseBridge, "_flush_interaction") as mock_flush:
            bridge = PulseBridge(verbose=False)
            bridge._tail_file(log, "CLAUDE")

        # After reading USER + CLAUDE lines, buffer should have user set
        # (flush not yet called — needs explicit idle or second USER)
        assert bridge.buffers["CLAUDE"]["user"] == "Help me fix this bug"
        assert any("Sure, let me look at the code" in a for a in bridge.buffers["CLAUDE"]["agent"])

    def test_position_advances_after_tail(self, tmp_path):
        """State position must equal file size after _tail_file runs."""
        crypto = PulseCrypto()
        log = tmp_path / "pulse_captured_claude.log"
        ts = "2026-03-18 10:00:00"

        user_line = _make_signed_line(crypto, ts, "USER", "What is PULSE?")
        claude_line = _make_signed_line(crypto, ts, "CLAUDE", "PULSE is a neural OS")
        log.write_text(f"{user_line}\n{claude_line}\n", encoding="utf-8")

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", log), \
             patch.object(PulseBridge, "_flush_interaction"):
            bridge = PulseBridge(verbose=False)
            bridge._tail_file(log, "CLAUDE")

        expected_pos = log.stat().st_size
        actual_pos = bridge.state["positions"][str(log)]
        assert actual_pos == expected_pos

    def test_does_not_reread_already_processed_lines(self, tmp_path):
        """Bridge skips lines before the stored position."""
        crypto = PulseCrypto()
        log = tmp_path / "pulse_captured_claude.log"
        ts = "2026-03-18 10:00:00"

        user_line = _make_signed_line(crypto, ts, "USER", "First question")
        log.write_text(f"{user_line}\n", encoding="utf-8")

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", log), \
             patch.object(PulseBridge, "_flush_interaction"):
            bridge = PulseBridge(verbose=False)
            # First pass — reads the USER line
            bridge._tail_file(log, "CLAUDE")
            assert bridge.buffers["CLAUDE"]["user"] == "First question"

            # Clear buffer manually
            bridge.buffers["CLAUDE"]["user"] = None
            bridge.buffers["CLAUDE"]["agent"] = []

            # Second pass — nothing new, buffer stays empty
            bridge._tail_file(log, "CLAUDE")
            assert bridge.buffers["CLAUDE"]["user"] is None

    def test_handles_missing_log_file_gracefully(self, tmp_path):
        """Bridge does not crash when claude capture log does not exist yet."""
        missing = tmp_path / "pulse_captured_claude.log"

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", missing):
            bridge = PulseBridge(verbose=False)
            bridge._tail_file(missing, "CLAUDE")  # must not raise


class TestBridgeDropsUnsignedLines:
    """Bridge must reject lines without a valid signature."""

    def test_unsigned_line_is_ignored(self, tmp_path):
        """A line without | SIG: token is silently dropped."""
        log = tmp_path / "pulse_captured_claude.log"
        ts = "2026-03-18 10:00:00"
        log.write_text(f"[{ts}] USER: No signature here\n", encoding="utf-8")

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", log), \
             patch.object(PulseBridge, "_flush_interaction"):
            bridge = PulseBridge(verbose=False)
            bridge._tail_file(log, "CLAUDE")

        assert bridge.buffers["CLAUDE"]["user"] is None

    def test_wrong_signature_is_rejected(self, tmp_path):
        """A line with a forged/invalid signature is dropped."""
        log = tmp_path / "pulse_captured_claude.log"
        ts = "2026-03-18 10:00:00"
        content = f"[{ts}] USER: Forged message"
        log.write_text(f"{content} | SIG: deadbeef00000000\n", encoding="utf-8")

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", log), \
             patch.object(PulseBridge, "_flush_interaction"):
            bridge = PulseBridge(verbose=False)
            bridge._tail_file(log, "CLAUDE")

        assert bridge.buffers["CLAUDE"]["user"] is None


class TestBridgeFlushOnSecondInteraction:
    """Verify that a second USER line triggers flush of the first interaction."""

    def test_second_user_line_triggers_flush(self, tmp_path):
        crypto = PulseCrypto()
        log = tmp_path / "pulse_captured_claude.log"
        ts = "2026-03-18 10:00:00"

        lines = [
            _make_signed_line(crypto, ts, "USER", "First question"),
            _make_signed_line(crypto, ts, "CLAUDE", "First answer"),
            _make_signed_line(crypto, ts, "USER", "Second question"),
        ]
        log.write_text("\n".join(lines) + "\n", encoding="utf-8")

        flush_calls = []

        def capture_flush(source):
            flush_calls.append(source)
            # Simulate real flush: clear buffer
            bridge.buffers[source]["user"] = None
            bridge.buffers[source]["agent"] = []

        with patch("pulse.daemons.bridge.pulse_bridge.STATE_FILE", tmp_path / "state.json"), \
             patch("pulse.daemons.bridge.pulse_bridge.LOG_CLAUDE", log):
            bridge = PulseBridge(verbose=False)
            bridge._flush_interaction = capture_flush
            bridge._tail_file(log, "CLAUDE")

        assert flush_calls == ["CLAUDE"], (
            "Expected one flush when second USER line arrived, "
            f"got: {flush_calls}"
        )
        # Second question should now be buffered
        assert bridge.buffers["CLAUDE"]["user"] == "Second question"


class TestLiveClaudeLogState:
    """Verify the production bridge state shows the claude log is fully processed."""

    def test_bridge_has_processed_entire_claude_log(self):
        """Bridge state position must equal current claude log file size."""
        state_file = PROJECT_ROOT / "state" / "pulse_bridge_state.json"
        claude_log = PROJECT_ROOT / "state" / "pulse_captured_claude.log"

        if not state_file.exists() or not claude_log.exists():
            pytest.skip("Bridge state or claude log not present in this environment")

        with open(state_file, "r", encoding="utf-8") as f:
            state = json.load(f)

        positions = state.get("positions", {})
        log_key = str(claude_log)
        assert log_key in positions, (
            f"Claude log '{log_key}' not tracked in bridge state. "
            "Bridge may not be watching the correct path."
        )

        bridge_pos = positions[log_key]
        file_size = claude_log.stat().st_size
        unread = file_size - bridge_pos

        # Allow up to 100KB of unread data — active sessions write new lines
        # continuously and the bridge daemon catches up on each poll cycle.
        # More than 100KB indicates the bridge has genuinely fallen behind.
        MAX_LAG_BYTES = 100 * 1024
        assert unread <= MAX_LAG_BYTES, (
            f"Bridge is {unread} bytes behind on claude capture log "
            f"(pos={bridge_pos}, size={file_size}). "
            "Bridge has fallen behind — possible pipeline gap or stopped daemon."
        )
        assert bridge_pos > 0, "Bridge has never processed the claude capture log"
