"""
Integration tests for Shell storage layer.

Tests use real SQLite databases (tmpdir), no mocking.
Per R8 (Real Wiring): Integration tests use real implementations at boundaries.
"""

from pathlib import Path
import tempfile

import pytest
from returns.result import Failure, Success

from lattice.core.types.enums import Role
from lattice.core.types.log import LogEntry, Session, group_logs_into_sessions
from lattice.shell.embeddings import insert_embedding, search_vec
from lattice.shell.schema import create_store
from lattice.shell.store import (
    count_pending_sessions,
    delete_events_by_message_id,
    get_metadata,
    insert_event,
    insert_log,
    query_logs_by_session,
    query_logs_since,
    search_fts,
    set_metadata,
    generate_session_id,
    generate_external_id,
    get_iso_timestamp,
)


@pytest.fixture
def store_conn():
    """Create a temporary store.db for testing."""
    with tempfile.TemporaryDirectory() as tmpdir:
        result = create_store(Path(tmpdir) / "store.db")
        assert isinstance(result, Success), f"Failed to create store: {result}"
        conn = result.unwrap()
        yield conn
        conn.close()


class TestInsertAndQuery:
    """Test insert_log and query functions."""

    def test_insert_log_basic(self, store_conn):
        """Test basic log insertion."""
        result = insert_log(store_conn, "session-1", Role.USER, "Hello world")
        assert isinstance(result, Success)
        log_id = result.unwrap()
        assert log_id == 1

    def test_insert_log_with_metadata(self, store_conn):
        """Test log insertion with metadata."""
        result = insert_log(
            store_conn,
            "session-1",
            Role.ASSISTANT,
            "Response",
            metadata={"tool": "search", "latency_ms": 150},
        )
        assert isinstance(result, Success)

    def test_insert_multiple_logs(self, store_conn):
        """Test inserting multiple logs in sequence."""
        ids = []
        for i in range(5):
            result = insert_log(
                store_conn,
                "session-1",
                Role.USER if i % 2 == 0 else Role.ASSISTANT,
                f"Message {i}",
            )
            assert isinstance(result, Success)
            ids.append(result.unwrap())
        assert ids == [1, 2, 3, 4, 5]

    def test_query_logs_by_session(self, store_conn):
        """Test querying logs by session_id."""
        insert_log(store_conn, "session-1", Role.USER, "Hello")
        insert_log(store_conn, "session-1", Role.ASSISTANT, "Hi there")
        insert_log(store_conn, "session-2", Role.USER, "Goodbye")

        result = query_logs_by_session(store_conn, "session-1")
        assert isinstance(result, Success)
        logs = result.unwrap()
        assert len(logs) == 2
        assert logs[0].session_id == "session-1"
        assert logs[1].session_id == "session-1"

    def test_query_logs_since(self, store_conn):
        """Test querying logs since a timestamp."""
        insert_log(store_conn, "session-1", Role.USER, "First message")
        insert_log(store_conn, "session-1", Role.ASSISTANT, "Second message")

        # Query from epoch start should return all
        result = query_logs_since(store_conn, "2000-01-01T00:00:00Z")
        assert isinstance(result, Success)
        logs = result.unwrap()
        assert len(logs) == 2

    def test_query_logs_by_nonexistent_session(self, store_conn):
        """Test querying logs for a session that doesn't exist."""
        result = query_logs_by_session(store_conn, "nonexistent")
        assert isinstance(result, Success)
        assert result.unwrap() == []


class TestFTS5Search:
    """Test FTS5 full-text search."""

    def test_fts5_search_basic(self, store_conn):
        """Test basic FTS5 search."""
        insert_log(store_conn, "s1", Role.USER, "Hello world example")
        insert_log(store_conn, "s1", Role.ASSISTANT, "Goodbye universe")

        result = search_fts(store_conn, "hello")
        assert isinstance(result, Success)
        results = result.unwrap()
        assert len(results) == 1
        assert results[0].rowid == 1

    def test_fts5_search_multiple_matches(self, store_conn):
        """Test FTS5 search with multiple matches."""
        insert_log(store_conn, "s1", Role.USER, "Hello world")
        insert_log(store_conn, "s1", Role.ASSISTANT, "Hello universe")
        insert_log(store_conn, "s1", Role.USER, "Goodbye")

        result = search_fts(store_conn, "hello")
        assert isinstance(result, Success)
        results = result.unwrap()
        assert len(results) == 2

    def test_fts5_search_no_match(self, store_conn):
        """Test FTS5 search with no matches."""
        insert_log(store_conn, "s1", Role.USER, "Hello world")

        result = search_fts(store_conn, "nonexistent")
        assert isinstance(result, Success)
        assert result.unwrap() == []

    def test_fts5_search_limit(self, store_conn):
        """Test FTS5 search with limit."""
        for i in range(10):
            insert_log(store_conn, "s1", Role.USER, f"Hello message {i}")

        result = search_fts(store_conn, "hello", limit=3)
        assert isinstance(result, Success)
        assert len(result.unwrap()) == 3

    def test_fts5_trigger_sync_on_insert(self, store_conn):
        """Test FTS5 trigger syncs on insert."""
        # Insert a log
        insert_log(store_conn, "s1", Role.USER, "Python programming tutorial")

        # Search should immediately find it
        result = search_fts(store_conn, "python")
        assert isinstance(result, Success)
        assert len(result.unwrap()) == 1


class TestVectorSearch:
    """Test sqlite-vec vector search."""

    def test_insert_embedding(self, store_conn):
        """Test inserting an embedding."""
        insert_log(store_conn, "s1", Role.USER, "Test message")

        embedding = [0.1] * 768  # Mock embedding
        result = insert_embedding(store_conn, 1, embedding)
        assert isinstance(result, Success)

    def test_search_vec(self, store_conn):
        """Test vector similarity search."""
        insert_log(store_conn, "s1", Role.USER, "Message 1")
        insert_log(store_conn, "s1", Role.USER, "Message 2")

        # Insert embeddings (slightly different)
        insert_embedding(store_conn, 1, [0.1] * 768)
        insert_embedding(store_conn, 2, [0.9] * 768)

        # Search with similar to first
        result = search_vec(store_conn, [0.1] * 768, limit=10)
        assert isinstance(result, Success)
        results = result.unwrap()
        assert len(results) == 2
        # First result should be closest (distance lowest)
        assert results[0].rowid == 1


class TestMetadata:
    """Test metadata key-value store."""

    def test_set_and_get_metadata(self, store_conn):
        """Test setting and getting metadata."""
        set_result = set_metadata(store_conn, "last_evolved_at", "2026-02-17T10:00:00Z")
        assert isinstance(set_result, Success)

        get_result = get_metadata(store_conn, "last_evolved_at")
        assert isinstance(get_result, Success)
        assert get_result.unwrap() == "2026-02-17T10:00:00Z"

    def test_get_nonexistent_metadata(self, store_conn):
        """Test getting metadata that doesn't exist."""
        result = get_metadata(store_conn, "nonexistent")
        assert isinstance(result, Success)
        assert result.unwrap() is None

    def test_update_metadata(self, store_conn):
        """Test updating existing metadata."""
        set_metadata(store_conn, "key", "value1")
        set_metadata(store_conn, "key", "value2")

        result = get_metadata(store_conn, "key")
        assert isinstance(result, Success)
        assert result.unwrap() == "value2"

    def test_multiple_metadata_keys(self, store_conn):
        """Test multiple metadata keys."""
        set_metadata(store_conn, "key1", "value1")
        set_metadata(store_conn, "key2", "value2")
        set_metadata(store_conn, "key3", "value3")

        assert get_metadata(store_conn, "key1").unwrap() == "value1"
        assert get_metadata(store_conn, "key2").unwrap() == "value2"
        assert get_metadata(store_conn, "key3").unwrap() == "value3"


class TestSessionManagement:
    """Test session management functions."""

    def test_generate_session_id(self):
        """Test session ID generation."""
        session_id = generate_session_id()
        assert len(session_id) == 36
        assert session_id.count("-") == 4

    def test_generate_session_id_unique(self):
        """Test session IDs are unique."""
        ids = {generate_session_id() for _ in range(100)}
        assert len(ids) == 100

    def test_generate_external_id(self):
        """Test external ID generation."""
        external_id = generate_external_id()
        assert len(external_id) == 36

    def test_get_iso_timestamp(self):
        """Test ISO timestamp generation."""
        ts = get_iso_timestamp()
        assert ts.endswith("Z")
        assert "T" in ts
        # ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ (20) or with microseconds (27)
        assert len(ts) in (20, 27)

    def test_count_pending_sessions_empty(self, store_conn):
        """Test counting sessions in empty store."""
        result = count_pending_sessions(store_conn, None)
        assert isinstance(result, Success)
        assert result.unwrap() == 0

    def test_count_pending_sessions_with_data(self, store_conn):
        """Test counting sessions with data."""
        insert_log(store_conn, "s1", Role.USER, "Message 1")
        insert_log(store_conn, "s1", Role.ASSISTANT, "Message 2")
        insert_log(store_conn, "s2", Role.USER, "Message 3")

        result = count_pending_sessions(store_conn, None)
        assert isinstance(result, Success)
        assert result.unwrap() == 2


class TestGroupLogsIntoSessions:
    """Test grouping logs into sessions."""

    def test_group_empty_logs(self):
        """Test grouping empty list."""
        sessions = group_logs_into_sessions([])
        assert sessions == []

    def test_group_single_session(self):
        """Test grouping logs from single session."""
        logs = [
            LogEntry("id1", "session-a", "2026-01-01T10:00:00Z", Role.USER, "Hi"),
            LogEntry(
                "id2", "session-a", "2026-01-01T10:01:00Z", Role.ASSISTANT, "Hello"
            ),
            LogEntry("id3", "session-a", "2026-01-01T10:02:00Z", Role.USER, "Bye"),
        ]
        sessions = group_logs_into_sessions(logs)
        assert len(sessions) == 1
        assert sessions[0].session_id == "session-a"
        assert len(sessions[0].logs) == 3
        # Verify sorted by timestamp
        assert sessions[0].logs[0].timestamp == "2026-01-01T10:00:00Z"

    def test_group_multiple_sessions(self):
        """Test grouping logs from multiple sessions."""
        logs = [
            LogEntry("id1", "session-a", "2026-01-01T10:00:00Z", Role.USER, "Hi"),
            LogEntry("id2", "session-b", "2026-01-01T09:00:00Z", Role.USER, "Hello"),
            LogEntry("id3", "session-a", "2026-01-01T11:00:00Z", Role.USER, "Bye"),
        ]
        sessions = group_logs_into_sessions(logs)
        assert len(sessions) == 2
        # Sorted by started_at
        assert sessions[0].session_id == "session-b"
        assert sessions[1].session_id == "session-a"

    def test_session_boundaries_preserved(self):
        """Test that session boundaries are preserved."""
        logs = [
            LogEntry("id1", "session-1", "2026-01-01T10:00:00Z", Role.USER, "A"),
            LogEntry("id2", "session-2", "2026-01-01T10:30:00Z", Role.USER, "B"),
            LogEntry("id3", "session-1", "2026-01-01T11:00:00Z", Role.USER, "C"),
        ]
        sessions = group_logs_into_sessions(logs)
        assert len(sessions) == 2
        # session-1 should have 2 logs, session-2 should have 1
        s1 = next(s for s in sessions if s.session_id == "session-1")
        s2 = next(s for s in sessions if s.session_id == "session-2")
        assert len(s1.logs) == 2
        assert len(s2.logs) == 1


class TestFullWriteFlow:
    """Test full write flow: insert → FTS5 → embedding → vec0."""

    def test_full_flow(self, store_conn):
        """Test inserting log, FTS5 indexing, and vector storage."""
        # Insert log
        insert_result = insert_log(
            store_conn, "session-1", Role.USER, "Python programming tutorial"
        )
        assert isinstance(insert_result, Success)
        log_id = insert_result.unwrap()

        # Verify FTS5 indexed it
        fts_result = search_fts(store_conn, "python")
        assert isinstance(fts_result, Success)
        assert len(fts_result.unwrap()) == 1

        # Insert embedding
        embedding = [0.5] * 768
        embed_result = insert_embedding(store_conn, log_id, embedding)
        assert isinstance(embed_result, Success)

        # Verify vector search finds it
        vec_result = search_vec(store_conn, [0.5] * 768)
        assert isinstance(vec_result, Success)
        assert len(vec_result.unwrap()) == 1

    def test_full_flow_multiple_logs(self, store_conn):
        """Test full flow with multiple logs."""
        # Insert 3 logs
        for i in range(3):
            insert_log(
                store_conn,
                "session-1",
                Role.USER,
                f"Python tutorial part {i}",
            )

        # All should be FTS5 indexed
        fts_result = search_fts(store_conn, "python")
        assert len(fts_result.unwrap()) == 3

        # Add embeddings to each
        for i in range(3):
            embedding = [0.1 * (i + 1)] * 768
            insert_embedding(store_conn, i + 1, embedding)

        # Vector search should find all
        vec_result = search_vec(store_conn, [0.2] * 768, limit=10)
        assert len(vec_result.unwrap()) == 3


class TestDeleteEventsByMessageId:
    """Test delete_events_by_message_id function."""

    def test_delete_events_basic(self, store_conn):
        """Test deleting events by message_id."""
        # Insert events with message_id in metadata
        insert_event(store_conn, "session-1", "user", "Hello")
        insert_event(store_conn, "session-1", "assistant", "Hi there")
        insert_event(store_conn, "session-1", "reasoning", "Thinking...")

        # Manually set message_id for test (simulating message_id field)
        # Note: The events table has a message_id column
        store_conn.execute(
            "UPDATE events SET message_id = ? WHERE session_id = ? AND type = ?",
            ("msg-123", "session-1", "user"),
        )
        store_conn.execute(
            "UPDATE events SET message_id = ? WHERE session_id = ? AND type = ?",
            ("msg-123", "session-1", "assistant"),
        )
        store_conn.execute(
            "UPDATE events SET message_id = ? WHERE session_id = ? AND type = ?",
            ("msg-456", "session-1", "reasoning"),
        )
        store_conn.commit()

        # Delete events with message_id = "msg-123"
        result = delete_events_by_message_id(store_conn, "session-1", "msg-123")
        assert isinstance(result, Success)
        assert result.unwrap() == 2

        # Verify only one event remains
        cursor = store_conn.execute(
            "SELECT COUNT(*) FROM events WHERE session_id = ?", ("session-1",)
        )
        assert cursor.fetchone()[0] == 1

    def test_delete_events_no_match(self, store_conn):
        """Test deleting events when no matches exist."""
        insert_event(store_conn, "session-1", "user", "Hello")
        store_conn.execute(
            "UPDATE events SET message_id = ? WHERE session_id = ?",
            ("msg-123", "session-1"),
        )
        store_conn.commit()

        # Delete with non-existent message_id
        result = delete_events_by_message_id(store_conn, "session-1", "nonexistent")
        assert isinstance(result, Success)
        assert result.unwrap() == 0  # No rows deleted

    def test_delete_events_different_sessions(self, store_conn):
        """Test that delete is scoped to session_id."""
        # Insert same message_id in different sessions
        insert_event(store_conn, "session-1", "user", "Hello from s1")
        insert_event(store_conn, "session-2", "user", "Hello from s2")

        store_conn.execute(
            "UPDATE events SET message_id = ? WHERE session_id = ?",
            ("msg-123", "session-1"),
        )
        store_conn.execute(
            "UPDATE events SET message_id = ? WHERE session_id = ?",
            ("msg-123", "session-2"),
        )
        store_conn.commit()

        # Delete only from session-1
        result = delete_events_by_message_id(store_conn, "session-1", "msg-123")
        assert isinstance(result, Success)
        assert result.unwrap() == 1

        # Session-2 event should still exist
        cursor = store_conn.execute(
            "SELECT COUNT(*) FROM events WHERE session_id = ? AND message_id = ?",
            ("session-2", "msg-123"),
        )
        assert cursor.fetchone()[0] == 1

    def test_delete_events_empty_message_id(self, store_conn):
        """Test that empty message_id fails pre-condition."""
        import deal

        insert_event(store_conn, "session-1", "user", "Hello")

        # Pre-condition should fail and raise PreContractError
        with pytest.raises(deal.PreContractError):
            delete_events_by_message_id(store_conn, "session-1", "")
