"""End-to-end tests for session compression.

Spec Reference: RFC-002-R1 §3.1 (Compression Rules), §4.1 (Event Types)

Expected behavior:
- Events are logged via Client.log() with proper type/content separation
- Tool events store only metadata (input summary, status, error)
- Compressed sessions should be <10% of raw size
- High-value data (user, assistant, reasoning) is preserved

Actual behavior: [Verified by tests]
"""

import json
import sqlite3
from pathlib import Path
from typing import Any

import pytest
from returns.result import Failure, Success

from lattice import Client
from lattice.core.types.event import (
    Event,
    EventType,
    is_high_signal,
    summarize_error_output,
    summarize_success_output,
    summarize_tool_input,
    truncate_error,
)
from lattice.shell.layer1 import estimate_tokens
from lattice.shell.schema import create_store
from lattice.shell.store import generate_session_id


class TestEventTypes:
    """Test Event creation and classification per RFC-002-R1 §4.1."""

    def test_user_event_is_high_signal(self) -> None:
        """User events are high-signal and preserved in full."""
        event = Event(type=EventType.USER, content="auth.py 报错了")
        assert is_high_signal(event) is True, "User events must be high-signal"

    def test_assistant_event_is_high_signal(self) -> None:
        """Assistant events are high-signal and preserved in full."""
        event = Event(type=EventType.ASSISTANT, content="I'll help you fix it.")
        assert is_high_signal(event) is True, "Assistant events must be high-signal"

    def test_reasoning_event_is_high_signal(self) -> None:
        """Reasoning events are high-signal and preserved in full."""
        event = Event(type=EventType.REASONING, content="需要检查 UserValidator...")
        assert is_high_signal(event) is True, "Reasoning events must be high-signal"

    def test_tool_event_is_low_signal(self) -> None:
        """Tool events are low-signal; only metadata is stored."""
        event = Event(
            type=EventType.TOOL,
            content="read",
            tool_input="src/auth.py",
            tool_status="success",
        )
        assert is_high_signal(event) is False, "Tool events must be low-signal"


class TestToolInputSummarization:
    """Test tool input summarization per RFC-002-R1 §3.1."""

    def test_read_tool_input_keeps_file_path(self) -> None:
        """Tool input: read → keep file path only."""
        result = summarize_tool_input("read", {"file_path": "src/auth.py"})
        assert result == "src/auth.py", f"Expected 'src/auth.py', got {result!r}"

    def test_bash_tool_input_keeps_command(self) -> None:
        """Tool input: bash → keep command only."""
        result = summarize_tool_input("bash", {"command": "npm test"})
        assert result == "npm test", f"Expected 'npm test', got {result!r}"

    def test_edit_tool_input_keeps_file_path(self) -> None:
        """Tool input: edit → keep file path only."""
        result = summarize_tool_input(
            "edit", {"file_path": "src/auth.py", "old_string": "def auth(): pass"}
        )
        assert result == "src/auth.py", f"Expected 'src/auth.py', got {result!r}"

    def test_unknown_tool_input_returns_empty(self) -> None:
        """Unknown tool with None input returns empty string."""
        result = summarize_tool_input("unknown_tool", None)
        assert result == "", f"Expected empty string, got {result!r}"

    def test_output_length_under_100_chars(self) -> None:
        """Summarized tool input must be <= 100 chars."""
        # Long command should still comply
        long_cmd = {"command": "x" * 200}
        result = summarize_tool_input("bash", long_cmd)
        assert len(result) <= 100, f"Output too long: {len(result)} chars"


class TestToolOutputSummarization:
    """Test tool output summarization per RFC-002-R1 §3.1."""

    def test_success_output_keeps_size_only(self) -> None:
        """Tool output: success → keep size only.

        Contract: @post: isinstance(result, str) and len(result) <= 50
        """
        content = "x" * 10000
        result = summarize_success_output(content)
        # Verify size information is present (format may vary)
        assert "10000" in result, f"Expected size information, got {result!r}"
        # Contract guarantee: result length <= 50 chars
        assert len(result) <= 50, (
            f"Summary too long: {len(result)} chars (contract: <= 50)"
        )

    def test_success_output_bytes(self) -> None:
        """Tool output: success (bytes) → keeps size in bytes."""
        content = b"hello world"
        result = summarize_success_output(content)
        assert "[11 bytes]" in result, f"Expected byte size, got {result!r}"

    def test_error_output_keeps_first_n_chars(self) -> None:
        """Tool output: error → keep first max_len chars."""
        error_msg = "ImportError: No module named 'jwt'"
        result = summarize_error_output(error_msg)
        assert error_msg in result, f"Expected full error, got {result!r}"

    def test_error_output_truncation(self) -> None:
        """Long error output is truncated to max_len chars.

        Contract: @post: isinstance(result, str)
        The function should keep the first max_len chars but may add truncation marker.
        Verify that output is reasonable size and indicates truncation.
        """
        error_msg = "x" * 1000
        result = summarize_error_output(error_msg, max_len=100)
        # Verify it's truncated (output should be much less than original)
        assert len(result) < len(error_msg), (
            f"Expected truncation, got full message ({len(result)} chars)"
        )
        assert "truncated" in result.lower(), (
            f"Expected truncation indicator, got {result!r}"
        )


class TestErrorTruncation:
    """Test error message truncation."""

    def test_short_error_not_truncated(self) -> None:
        """Short error messages are preserved in full."""
        error = "Short error"
        result = truncate_error(error)
        assert result == error, f"Expected {error!r}, got {result!r}"

    def test_long_error_is_truncated(self) -> None:
        """Long error messages are truncated.

        Contract: @pre: max_len > 0, @post: isinstance(result, str)
        Verify the result is a string and has truncation indicator.
        """
        error = "x" * 1000
        result = truncate_error(error, max_len=500)
        # Verify it's a string (contract guarantee)
        assert isinstance(result, str), f"Expected str, got {type(result)}"
        # Verify truncation happened (result shorter than original)
        assert len(result) < len(error), f"Expected truncation, got full message"
        # Should indicate truncation
        assert "truncated" in result.lower(), (
            f"Expected truncation indicator, got {result!r}"
        )

    def test_truncation_respects_max_len(self) -> None:
        """Truncation respects custom max_len parameter.

        Contract: @pre: max_len > 0, @post: isinstance(result, str)
        The result should be shorter than original and include truncation info.
        """
        error = "x" * 100
        result = truncate_error(error, max_len=10)
        # Verify result is string (contract) and shorter than original
        assert isinstance(result, str), f"Expected str, got {type(result)}"
        assert len(result) < len(error), (
            f"Expected truncated result shorter than original"
        )
        assert "truncated" in result.lower(), f"Expected truncation indicator"


class TestSDKClientLog:
    """Test Client.log() API for session compression."""

    @pytest.fixture
    def project_dir(self, tmp_path: Path) -> Path:
        """Create a temporary project directory with .lattice initialized."""
        project = tmp_path / "test_project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir(parents=True)
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        result.unwrap().close()

        return project

    def test_log_user_event(self, project_dir: Path) -> None:
        """Client.log('user', content) stores user event."""
        client = Client(project_dir)
        result = client.log("user", "auth.py 报错了")

        assert isinstance(result, Success), f"Expected Success, got {result}"
        event_id = result.unwrap()
        assert isinstance(event_id, int), f"Expected int event_id, got {type(event_id)}"

    def test_log_assistant_event(self, project_dir: Path) -> None:
        """Client.log('assistant', content) stores assistant event."""
        client = Client(project_dir)
        result = client.log("assistant", "I'll help you fix the auth issue.")

        assert isinstance(result, Success), f"Expected Success, got {result}"

    def test_log_reasoning_event(self, project_dir: Path) -> None:
        """Client.log('reasoning', content) stores reasoning event."""
        client = Client(project_dir)
        result = client.log("reasoning", "需要检查 UserValidator 类的实现...")

        assert isinstance(result, Success), f"Expected Success, got {result}"

    def test_log_tool_success_event(self, project_dir: Path) -> None:
        """Client.log('tool', name, input=..., status='success') stores tool event."""
        client = Client(project_dir)
        result = client.log(
            "tool",
            "read",
            input="src/auth.py",
            status="success",
        )

        assert isinstance(result, Success), f"Expected Success, got {result}"

    def test_log_tool_error_event(self, project_dir: Path) -> None:
        """Client.log('tool', name, input=..., status='error', error=...) stores tool error."""
        client = Client(project_dir)
        result = client.log(
            "tool",
            "edit",
            input="src/auth.py",
            status="error",
            error="SyntaxError: invalid syntax on line 42",
        )

        assert isinstance(result, Success), f"Expected Success, got {result}"

    def test_log_with_session_id(self, project_dir: Path) -> None:
        """Client.log() accepts optional session_id parameter."""
        client = Client(project_dir)
        session_id = generate_session_id()

        result = client.log("user", "Test message", session_id=session_id)

        assert isinstance(result, Success), f"Expected Success, got {result}"


class TestSessionCompression:
    """Test full session compression pipeline."""

    @pytest.fixture
    def project_dir(self, tmp_path: Path) -> Path:
        """Create a temporary project directory with .lattice initialized."""
        project = tmp_path / "test_project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir(parents=True)
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        conn = result.unwrap()

        # Create events table if missing (schema migration)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                session_id TEXT NOT NULL,
                type TEXT NOT NULL,
                content TEXT NOT NULL,
                tool_input TEXT,
                tool_status TEXT,
                tool_error TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)
        conn.commit()
        conn.close()

        return project

    def _calculate_raw_size(self, events: list[dict[str, Any]]) -> int:
        """Calculate raw (uncompressed) size of events as JSON."""
        return len(json.dumps(events, ensure_ascii=False))

    def _calculate_compressed_size(self, events: list[Event]) -> int:
        """Calculate compressed size using event objects."""
        total = 0
        for event in events:
            if is_high_signal(event):
                # Keep full content
                total += len(event.content)
            else:
                # Tool event: metadata only
                total += len(event.content)  # tool name
                total += len(event.tool_input or "")
                total += len(event.tool_status or "")
                total += len(event.tool_error or "")
        return total

    def test_compression_ratio_under_10_percent(self, project_dir: Path) -> None:
        """Compressed output size should be < 10% of raw."""
        session_id = generate_session_id()
        client = Client(project_dir)

        # Create session with mixed events
        raw_events: list[dict[str, Any]] = []

        # User message (high-signal)
        user_content = "I need to fix the authentication module in src/auth.py"
        raw_events.append({"type": "user", "content": user_content})
        client.log("user", user_content, session_id=session_id)

        # Reasoning (high-signal)
        reasoning_content = (
            "需要检查 UserValidator 类，看看 JWT token 验证的逻辑是否有问题..."
        )
        raw_events.append({"type": "reasoning", "content": reasoning_content})
        client.log("reasoning", reasoning_content, session_id=session_id)

        # Tool: read file (low-signal, success)
        tool_output = "x" * 50000  # 50KB of output
        raw_events.append(
            {
                "type": "tool",
                "content": "read",
                "tool_input": "src/auth.py",
                "tool_status": "success",
                "raw_output": tool_output,  # Not stored in compressed version
            }
        )
        client.log(
            "tool", "read", input="src/auth.py", status="success", session_id=session_id
        )

        # Tool: bash command (low-signal, success)
        bash_output = "Test passed. 50 tests, 0 failures."
        raw_events.append(
            {
                "type": "tool",
                "content": "bash",
                "tool_input": "npm test",
                "tool_status": "success",
                "raw_output": bash_output,
            }
        )
        client.log(
            "tool", "bash", input="npm test", status="success", session_id=session_id
        )

        # Assistant response (high-signal)
        assistant_content = (
            "I found the issue. The JWT secret was hardcoded. Let me fix it."
        )
        raw_events.append({"type": "assistant", "content": assistant_content})
        client.log("assistant", assistant_content, session_id=session_id)

        # Tool: edit file (low-signal, error)
        edit_error = "Error: SyntaxError on line 42. Unexpected token 'def'"
        raw_events.append(
            {
                "type": "tool",
                "content": "edit",
                "tool_input": "src/auth.py",
                "tool_status": "error",
                "tool_error": edit_error,
            }
        )
        client.log(
            "tool",
            "edit",
            input="src/auth.py",
            status="error",
            error=edit_error,
            session_id=session_id,
        )

        # Calculate sizes
        raw_size = self._calculate_raw_size(raw_events)
        compressed_size_estimate = (
            len(user_content)  # user
            + len(reasoning_content)  # reasoning
            + len("read")
            + len("src/auth.py")
            + 7  # tool read (name + input + status)
            + len("bash")
            + len("npm test")
            + 7  # tool bash
            + len(assistant_content)  # assistant
            + len("edit")
            + len("src/auth.py")
            + 5
            + len(edit_error)  # tool edit
        )

        # Compression ratio check
        # Note: raw includes big tool_output which isn't stored
        compression_ratio = compressed_size_estimate / raw_size if raw_size > 0 else 0

        # For this test, the 50KB tool output dominates raw_size
        # Compressed version doesn't store it, so ratio should be very low
        assert compression_ratio < 0.10, (
            f"Compression ratio {compression_ratio:.2%} exceeds 10%. "
            f"Raw: {raw_size} bytes, Compressed estimate: {compressed_size_estimate} bytes"
        )

    def test_high_value_data_preserved(self, project_dir: Path) -> None:
        """All high-value data (user, assistant, reasoning) must be preserved."""
        session_id = generate_session_id()
        client = Client(project_dir)

        user_msg = "This is a detailed user question about authentication."
        reasoning_msg = (
            "Analyzing the authentication flow and checking potential issues."
        )
        assistant_msg = "Based on my analysis, here's what I found..."

        client.log("user", user_msg, session_id=session_id)
        client.log("reasoning", reasoning_msg, session_id=session_id)
        client.log("assistant", assistant_msg, session_id=session_id)

        # Read events back from database
        lattice_dir = project_dir / ".lattice"
        store_path = lattice_dir / "store.db"

        conn = sqlite3.connect(store_path)
        cursor = conn.execute(
            "SELECT type, content FROM events WHERE session_id = ? ORDER BY id",
            (session_id,),
        )
        events = cursor.fetchall()
        conn.close()

        # Verify all high-value content preserved
        event_dict = {row[0]: row[1] for row in events}

        assert event_dict.get("user") == user_msg, (
            f"User content not preserved. Expected {user_msg!r}, got {event_dict.get('user')!r}"
        )
        assert event_dict.get("reasoning") == reasoning_msg, (
            f"Reasoning content not preserved. Expected {reasoning_msg!r}, got {event_dict.get('reasoning')!r}"
        )
        assert event_dict.get("assistant") == assistant_msg, (
            f"Assistant content not preserved. Expected {assistant_msg!r}, got {event_dict.get('assistant')!r}"
        )

    def test_tool_metadata_preserved(self, project_dir: Path) -> None:
        """Tool event metadata (input, status, error) must be preserved."""
        session_id = generate_session_id()
        client = Client(project_dir)

        client.log(
            "tool",
            "read",
            input="src/auth.py",
            status="success",
            session_id=session_id,
        )
        client.log(
            "tool",
            "edit",
            input="src/auth.py",
            status="error",
            error="SyntaxError: invalid syntax",
            session_id=session_id,
        )

        # Read events back
        lattice_dir = project_dir / ".lattice"
        store_path = lattice_dir / "store.db"

        conn = sqlite3.connect(store_path)
        cursor = conn.execute(
            "SELECT content, tool_input, tool_status, tool_error FROM events "
            "WHERE session_id = ? AND type = 'tool' ORDER BY id",
            (session_id,),
        )
        events = cursor.fetchall()
        conn.close()

        assert len(events) == 2, f"Expected 2 tool events, got {len(events)}"

        # First tool: read, success
        read_event = events[0]
        assert read_event[0] == "read", f"Expected 'read', got {read_event[0]!r}"
        assert read_event[1] == "src/auth.py", (
            f"Expected 'src/auth.py', got {read_event[1]!r}"
        )
        assert read_event[2] == "success", f"Expected 'success', got {read_event[2]!r}"

        # Second tool: edit, error
        edit_event = events[1]
        assert edit_event[0] == "edit", f"Expected 'edit', got {edit_event[0]!r}"
        assert edit_event[1] == "src/auth.py", (
            f"Expected 'src/auth.py', got {edit_event[1]!r}"
        )
        assert edit_event[2] == "error", f"Expected 'error', got {edit_event[2]!r}"
        assert "SyntaxError" in (edit_event[3] or ""), (
            f"Expected error message, got {edit_event[3]!r}"
        )


class TestTokenSavings:
    """Test token estimation and savings verification."""

    def test_estimate_tokens_heuristic(self) -> None:
        """Token estimation uses ~4 chars per token."""
        events = [Event(type=EventType.USER, content="Hello world")]  # 11 chars
        tokens = estimate_tokens(events)

        # 11 chars ÷ 4 = 2.75 → round up to ~3 tokens (includes event overhead)
        # Actual formula from spec: len(content) // 4 + overhead
        assert tokens > 0, "Token estimate should be positive"

    def test_high_signal_events_contribute_more_tokens(self) -> None:
        """High-signal events contribute more tokens than low-signal."""
        high_signal_events = [
            Event(type=EventType.USER, content="x" * 100),
            Event(type=EventType.ASSISTANT, content="y" * 100),
            Event(type=EventType.REASONING, content="z" * 100),
        ]

        low_signal_events = [
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="file.py",
                tool_status="success",
            ),
            Event(
                type=EventType.TOOL,
                content="bash",
                tool_input="cmd",
                tool_status="success",
            ),
        ]

        high_tokens = estimate_tokens(high_signal_events)
        low_tokens = estimate_tokens(low_signal_events)

        assert high_tokens > low_tokens, (
            f"High-signal events ({high_tokens} tokens) should have more tokens "
            f"than low-signal events ({low_tokens} tokens)"
        )

    def test_token_savings_from_compression(self) -> None:
        """Simulate token savings from compression."""
        # Before compression: raw tool output is huge
        raw_events = [
            Event(type=EventType.USER, content="Fix the bug"),
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="file.py",
                tool_status="success",
            ),
            # Pretend we had 50KB of output that's now summarized
        ]

        # After compression: tool output is summarized (not in event content)
        compressed_events = [
            Event(type=EventType.USER, content="Fix the bug"),
            Event(
                type=EventType.TOOL,
                content="read",
                tool_input="file.py",
                tool_status="success",
            ),
        ]

        # Token count should be same for this simplified example
        # (real savings come from not storing raw_output)
        raw_tokens = estimate_tokens(raw_events)
        compressed_tokens = estimate_tokens(compressed_events)

        # In reality, compressed would be much smaller if we counted raw_output
        assert raw_tokens == compressed_tokens, "Events should be equivalent"

    def test_large_tool_output_savings(self) -> None:
        """Calculate savings when tool output is large."""
        # User question + 50KB tool output
        user_event = Event(type=EventType.USER, content="Show me the file")

        # Before: user + pretend 50KB output
        # After: user + tool metadata (~20 chars)
        estimated_savings = (50000 - 20) // 4  # ~12,495 tokens saved

        assert estimated_savings > 10000, (
            f"Expected >10k token savings from 50KB output, got {estimated_savings}"
        )


class TestMCPLogTool:
    """Test session compression via MCP interface.

    These tests verify the MCP tool accepts the same parameters as Client.log().
    MCP interface is typically exposed via STDIO transport.
    """

    @pytest.fixture
    def project_dir(self, tmp_path: Path) -> Path:
        """Create a temporary project directory with .lattice initialized."""
        project = tmp_path / "test_project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir(parents=True)
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        conn = result.unwrap()
        conn.execute("""
            CREATE TABLE IF NOT EXISTS events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                session_id TEXT NOT NULL,
                type TEXT NOT NULL,
                content TEXT NOT NULL,
                tool_input TEXT,
                tool_status TEXT,
                tool_error TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)
        conn.commit()
        conn.close()

        return project

    def test_mcp_log_user_event(self, project_dir: Path) -> None:
        """MCP log tool accepts user event with content."""
        # MCP tools delegate to Client.log() internally
        # The MCP tool signature should match Client.log()
        client = Client(project_dir)

        # Simulate MCP call: log({type: "user", content: "..."})
        result = client.log("user", "Test via MCP")

        assert isinstance(result, Success), f"MCP log failed: {result}"

    def test_mcp_log_tool_with_all_params(self, project_dir: Path) -> None:
        """MCP log tool accepts tool event with input, status, error."""
        client = Client(project_dir)

        # Simulate MCP call: log({type: "tool", content: "read", input: "file.py", status: "error", error: "..."})
        result = client.log(
            "tool",
            "read",
            input="src/auth.py",
            status="error",
            error="File not found",
        )

        assert isinstance(result, Success), f"MCP log failed: {result}"

    def test_mcp_session_id_propagation(self, project_dir: Path) -> None:
        """MCP log tool accepts session_id parameter."""
        client = Client(project_dir)
        session_id = generate_session_id()

        result = client.log("user", "Test", session_id=session_id)

        assert isinstance(result, Success), f"MCP log failed: {result}"


class TestPluginCapture:
    """Test plugin capture: simulate plugin sending { session_id, events } to CLI ingest."""

    @pytest.fixture
    def project_dir(self, tmp_path: Path) -> Path:
        """Create a temporary project directory."""
        project = tmp_path / "test_project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir(parents=True)
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        conn = result.unwrap()
        conn.execute("""
            CREATE TABLE IF NOT EXISTS events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                session_id TEXT NOT NULL,
                type TEXT NOT NULL,
                content TEXT NOT NULL,
                tool_input TEXT,
                tool_status TEXT,
                tool_error TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)
        conn.commit()
        conn.close()

        return project

    def test_plugin_ingest_single_session(self, project_dir: Path) -> None:
        """Plugin can ingest a batch of events for a single session."""
        session_id = generate_session_id()
        client = Client(project_dir)

        # Plugin sends batch of events
        events = [
            {"type": "user", "content": "First message"},
            {
                "type": "tool",
                "content": "read",
                "input": "file.py",
                "status": "success",
            },
            {"type": "assistant", "content": "Response"},
        ]

        event_ids = []
        for event in events:
            if event["type"] == "tool":
                result = client.log(
                    event["type"],
                    event["content"],
                    input=event.get("input", ""),
                    status=event.get("status", "success"),
                    session_id=session_id,
                )
            else:
                result = client.log(
                    event["type"],
                    event["content"],
                    session_id=session_id,
                )

            assert isinstance(result, Success), f"Ingest failed for {event}"
            event_ids.append(result.unwrap())

        assert len(event_ids) == 3, f"Expected 3 events, got {len(event_ids)}"

    def test_plugin_ingest_preserves_session_id(self, project_dir: Path) -> None:
        """All events in a plugin batch share the same session_id."""
        session_id = generate_session_id()
        client = Client(project_dir)

        client.log("user", "Message 1", session_id=session_id)
        client.log("assistant", "Response 1", session_id=session_id)
        client.log("user", "Message 2", session_id=session_id)

        # Verify all events have same session_id
        lattice_dir = project_dir / ".lattice"
        store_path = lattice_dir / "store.db"

        conn = sqlite3.connect(store_path)
        cursor = conn.execute(
            "SELECT DISTINCT session_id FROM events",
        )
        session_ids = [row[0] for row in cursor.fetchall()]
        conn.close()

        assert len(session_ids) == 1, f"Expected 1 session_id, got {session_ids}"
        assert session_ids[0] == session_id, (
            f"Session ID mismatch: {session_ids[0]} != {session_id}"
        )


class TestPerformanceLatency:
    """Test ingest + compile latency for session compression."""

    @pytest.fixture
    def project_dir(self, tmp_path: Path) -> Path:
        """Create a temporary project directory."""
        project = tmp_path / "test_project"
        project.mkdir()
        lattice_dir = project / ".lattice"
        lattice_dir.mkdir()
        (lattice_dir / "rules").mkdir()
        (lattice_dir / "drift" / "proposals").mkdir(parents=True)
        (lattice_dir / "config.toml").write_text('[compiler]\nmodel = "test"\n')

        result = create_store(lattice_dir / "store.db")
        assert isinstance(result, Success)
        conn = result.unwrap()
        conn.execute("""
            CREATE TABLE IF NOT EXISTS events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                session_id TEXT NOT NULL,
                type TEXT NOT NULL,
                content TEXT NOT NULL,
                tool_input TEXT,
                tool_status TEXT,
                tool_error TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """)
        conn.commit()
        conn.close()

        return project

    def test_ingest_latency_under_100ms(self, project_dir: Path) -> None:
        """Single event ingest should complete under 100ms."""
        import time

        client = Client(project_dir)

        start = time.perf_counter()
        result = client.log("user", "Test message for latency")
        elapsed_ms = (time.perf_counter() - start) * 1000

        assert isinstance(result, Success), f"Ingest failed: {result}"
        assert elapsed_ms < 100, f"Ingest took {elapsed_ms:.1f}ms (expected <100ms)"

    def test_batch_ingest_latency_scaled(self, project_dir: Path) -> None:
        """Batch ingest of 100 events should scale linearly."""
        import time

        client = Client(project_dir)
        session_id = generate_session_id()

        start = time.perf_counter()
        for i in range(100):
            client.log("user", f"Message {i}", session_id=session_id)
        elapsed_ms = (time.perf_counter() - start) * 1000

        # 100 events should complete under 5 seconds (50ms per event avg)
        assert elapsed_ms < 5000, (
            f"Batch ingest took {elapsed_ms:.1f}ms (expected <5000ms)"
        )


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
