"""Tests for the opencode-session adapter using temporary SQLite databases."""

from __future__ import annotations

import json
import sqlite3
from datetime import UTC, datetime
from pathlib import Path

import pytest

from opencode_session.config import OpenCodeConfig
from opencode_session.source import OpenCodeSource
from session_store import SessionExport, SessionInfo, SessionMessage
from session_store.errors import SessionLoadError, SessionNotFoundError

SCHEMA = """
CREATE TABLE session (
    id TEXT PRIMARY KEY,
    title TEXT,
    directory TEXT,
    time_created TEXT,
    time_updated TEXT,
    model TEXT,
    slug TEXT,
    cost REAL,
    tokens_input INTEGER,
    tokens_output INTEGER,
    tokens_reasoning INTEGER,
    metadata TEXT
);
CREATE TABLE message (
    id TEXT PRIMARY KEY,
    session_id TEXT,
    data TEXT,
    time TEXT
);
CREATE TABLE part (
    id TEXT PRIMARY KEY,
    data TEXT,
    time TEXT
);
"""


def make_db(tmp_path: Path, name: str = "opencode.db") -> Path:
    """Create a fresh OpenCode-style SQLite database with the schema."""
    db_path = tmp_path / name
    conn = sqlite3.connect(str(db_path))
    conn.executescript(SCHEMA)
    conn.commit()
    conn.close()
    return db_path


def insert_session(
    conn: sqlite3.Connection,
    session_id: str,
    title: str,
    directory: str,
    time_created: str,
    time_updated: str,
    model: str | None = None,
    slug: str | None = None,
    cost: float | None = None,
    tokens_input: int | None = None,
    tokens_output: int | None = None,
    tokens_reasoning: int | None = None,
    metadata: str | None = None,
) -> None:
    conn.execute(
        "INSERT INTO session (id, title, directory, time_created, time_updated, "
        "model, slug, cost, tokens_input, tokens_output, tokens_reasoning, metadata) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        (
            session_id,
            title,
            directory,
            time_created,
            time_updated,
            model,
            slug,
            cost,
            tokens_input,
            tokens_output,
            tokens_reasoning,
            metadata,
        ),
    )
    conn.commit()


def insert_message(
    conn: sqlite3.Connection,
    msg_id: str,
    session_id: str,
    data: dict,
    time: str,
) -> None:
    conn.execute(
        "INSERT INTO message (id, session_id, data, time) VALUES (?, ?, ?, ?)",
        (msg_id, session_id, json.dumps(data), time),
    )
    conn.commit()


def insert_part(
    conn: sqlite3.Connection,
    part_id: str,
    data: dict,
    time: str,
) -> None:
    conn.execute(
        "INSERT INTO part (id, data, time) VALUES (?, ?, ?)",
        (part_id, json.dumps(data), time),
    )
    conn.commit()


def make_source(db_path: Path) -> OpenCodeSource:
    return OpenCodeSource(OpenCodeConfig(db_path=db_path))


# --------------------------------------------------------------------------- #
# Fixtures
# --------------------------------------------------------------------------- #


@pytest.fixture
def db_path(tmp_path: Path) -> Path:
    return make_db(tmp_path)


@pytest.fixture
def populated_db(tmp_path: Path) -> Path:
    """A database with two sessions, multiple messages, and mixed part types."""
    db_path = make_db(tmp_path)
    conn = sqlite3.connect(str(db_path))
    try:
        insert_session(
            conn,
            session_id="sess-1",
            title="Test Session 1",
            directory="/tmp/test",
            time_created="2026-07-14T08:00:00Z",
            time_updated="2026-07-14T10:00:00Z",
            model=json.dumps(
                {"id": "deepseek-v4-flash", "providerID": "deepseek", "variant": "default"}
            ),
            slug="test-session-1",
            cost=0.0023,
            tokens_input=1500,
            tokens_output=800,
            tokens_reasoning=200,
            metadata=json.dumps({"custom": "session-meta"}),
        )
        insert_session(
            conn,
            session_id="sess-2",
            title="Test Session 2",
            directory="/tmp/other",
            time_created="2026-07-14T09:00:00Z",
            time_updated="2026-07-14T11:00:00Z",
        )

        # session 1: one user message with a single text part
        insert_message(
            conn,
            msg_id="msg-1",
            session_id="sess-1",
            data={"role": "user", "parentID": None, "parts": ["part-1"], "agent": "build"},
            time="2026-07-14T08:01:00Z",
        )
        insert_part(
            conn,
            part_id="part-1",
            data={"type": "text", "text": "Hello world"},
            time="2026-07-14T08:01:00Z",
        )

        # session 1: assistant message with multiple text parts
        insert_message(
            conn,
            msg_id="msg-2",
            session_id="sess-1",
            data={
                "role": "assistant",
                "parentID": "msg-1",
                "parts": ["part-2", "part-3"],
                "model": {"id": "deepseek-v4-flash"},
                "cost": 0.001,
            },
            time="2026-07-14T08:02:00Z",
        )
        insert_part(
            conn,
            part_id="part-2",
            data={"type": "text", "text": "First part. "},
            time="2026-07-14T08:02:00Z",
        )
        insert_part(
            conn,
            part_id="part-3",
            data={"type": "text", "text": "Second part."},
            time="2026-07-14T08:02:00Z",
        )

        # session 1: user message with reasoning + tool + text parts
        insert_message(
            conn,
            msg_id="msg-3",
            session_id="sess-1",
            data={"role": "user", "parentID": "msg-2", "parts": ["part-r", "part-t", "part-4"]},
            time="2026-07-14T08:03:00Z",
        )
        insert_part(
            conn,
            part_id="part-r",
            data={"type": "reasoning", "text": "Let me think about this..."},
            time="2026-07-14T08:03:00Z",
        )
        insert_part(
            conn,
            part_id="part-t",
            data={"type": "tool", "tool": "edit", "input": {"file": "a.py"}, "output": "ok"},
            time="2026-07-14T08:03:00Z",
        )
        insert_part(
            conn,
            part_id="part-4",
            data={"type": "text", "text": "Only text matters."},
            time="2026-07-14T08:03:00Z",
        )

        # session 1: message with NO text parts (only reasoning)
        insert_message(
            conn,
            msg_id="msg-4",
            session_id="sess-1",
            data={"role": "assistant", "parentID": "msg-3", "parts": ["part-r2"]},
            time="2026-07-14T08:04:00Z",
        )
        insert_part(
            conn,
            part_id="part-r2",
            data={"type": "reasoning", "text": "Just reasoning here."},
            time="2026-07-14T08:04:00Z",
        )

        # session 2: a single message so it has a message_count of 1
        insert_message(
            conn,
            msg_id="msg-5",
            session_id="sess-2",
            data={"role": "user", "parentID": None, "parts": ["part-5"]},
            time="2026-07-14T09:01:00Z",
        )
        insert_part(
            conn,
            part_id="part-5",
            data={"type": "text", "text": "Session two content"},
            time="2026-07-14T09:01:00Z",
        )
    finally:
        conn.close()
    return db_path


# --------------------------------------------------------------------------- #
# tool_name
# --------------------------------------------------------------------------- #


def test_tool_name_returns_opencode(db_path: Path) -> None:
    source = make_source(db_path)
    assert source.tool_name() == "opencode"


# --------------------------------------------------------------------------- #
# list_sessions
# --------------------------------------------------------------------------- #


def test_list_sessions_returns_ordered_info(populated_db: Path) -> None:
    source = make_source(populated_db)
    sessions = source.list_sessions()

    assert len(sessions) == 2
    # Ordered by time_updated DESC → sess-2 (11:00) first, sess-1 (10:00) second.
    assert sessions[0].session_id == "sess-2"
    assert sessions[1].session_id == "sess-1"

    for info in sessions:
        assert isinstance(info, SessionInfo)
        assert info.tool == "opencode"

    assert sessions[0].title == "Test Session 2"
    assert sessions[0].working_directory == "/tmp/other"
    assert sessions[0].message_count == 1
    assert sessions[0].last_activity == datetime(2026, 7, 14, 11, 0, 0, tzinfo=UTC)

    assert sessions[1].title == "Test Session 1"
    assert sessions[1].working_directory == "/tmp/test"
    assert sessions[1].message_count == 4
    assert sessions[1].last_activity == datetime(2026, 7, 14, 10, 0, 0, tzinfo=UTC)


def test_list_sessions_respects_limit(populated_db: Path) -> None:
    source = make_source(populated_db)
    sessions = source.list_sessions(limit=1)
    assert len(sessions) == 1
    assert sessions[0].session_id == "sess-2"


def test_list_sessions_returns_empty_when_db_missing(tmp_path: Path) -> None:
    missing = tmp_path / "does-not-exist.db"
    source = make_source(missing)
    assert source.list_sessions() == []


# --------------------------------------------------------------------------- #
# load_session
# --------------------------------------------------------------------------- #


def test_load_session_returns_correct_export(populated_db: Path) -> None:
    source = make_source(populated_db)
    export = source.load_session("sess-1")

    assert isinstance(export, SessionExport)
    assert export.tool == "opencode"
    assert export.session_id == "sess-1"
    assert export.title == "Test Session 1"
    assert export.working_directory == "/tmp/test"
    assert len(export.messages) == 4

    # Session-level fields populated from the session row.
    assert export.created_at == datetime(2026, 7, 14, 8, 0, 0, tzinfo=UTC)
    assert export.last_activity_at == datetime(2026, 7, 14, 10, 0, 0, tzinfo=UTC)
    assert export.model == "deepseek-v4-flash"
    assert export.metadata.get("slug") == "test-session-1"
    assert export.metadata.get("cost") == 0.0023
    assert export.metadata.get("tokens_input") == 1500
    assert export.metadata.get("tokens_output") == 800
    assert export.metadata.get("tokens_reasoning") == 200
    assert export.metadata.get("custom") == "session-meta"

    # Messages ordered by time ASC.
    ids = [m.message_id for m in export.messages]
    assert ids == ["msg-1", "msg-2", "msg-3", "msg-4"]

    # msg-1: single text part.
    m1 = export.messages[0]
    assert isinstance(m1, SessionMessage)
    assert m1.role == "user"
    assert m1.parent_node_id is None
    assert m1.content == "Hello world"
    assert m1.created_at == datetime(2026, 7, 14, 8, 1, 0, tzinfo=UTC)
    # agent field preserved in metadata.
    assert m1.metadata.get("agent") == "build"

    # msg-2: assistant message with model and cost in metadata.
    m2 = export.messages[1]
    assert m2.metadata.get("model") == {"id": "deepseek-v4-flash"}
    assert m2.metadata.get("cost") == 0.001


def test_load_session_concatenates_multiple_text_parts(populated_db: Path) -> None:
    source = make_source(populated_db)
    export = source.load_session("sess-1")
    m2 = next(m for m in export.messages if m.message_id == "msg-2")
    assert m2.content == "First part. Second part."


def test_load_session_skips_reasoning_and_tool_parts(populated_db: Path) -> None:
    source = make_source(populated_db)
    export = source.load_session("sess-1")
    m3 = next(m for m in export.messages if m.message_id == "msg-3")
    # Only the text part contributes; reasoning and tool are skipped.
    assert m3.content == "Only text matters."
    # Reasoning and tool parts are preserved in metadata.
    assert m3.metadata.get("reasoning") == ["Let me think about this..."]
    tool_calls = m3.metadata.get("tool_calls")
    assert isinstance(tool_calls, list)
    assert tool_calls[0].get("tool") == "edit"


def test_load_session_message_with_no_text_parts_has_empty_content(populated_db: Path) -> None:
    source = make_source(populated_db)
    export = source.load_session("sess-1")
    m4 = next(m for m in export.messages if m.message_id == "msg-4")
    assert m4.content == ""


def test_load_session_raises_session_not_found(populated_db: Path) -> None:
    source = make_source(populated_db)
    with pytest.raises(SessionNotFoundError):
        source.load_session("nonexistent-session")


def test_load_session_raises_session_load_error_when_db_missing(tmp_path: Path) -> None:
    missing = tmp_path / "no-such.db"
    source = make_source(missing)
    with pytest.raises(SessionLoadError):
        source.load_session("sess-1")


# --------------------------------------------------------------------------- #
# detect_current_session
# --------------------------------------------------------------------------- #


def test_detect_current_session_finds_by_cwd(
    populated_db: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # Use a real directory matching the session's stored directory value.
    cwd = tmp_path / "test"
    cwd.mkdir()
    # Rewrite the session's directory to the real temp path so detection matches.
    conn = sqlite3.connect(str(populated_db))
    conn.execute("UPDATE session SET directory = ? WHERE id = 'sess-1'", (str(cwd),))
    conn.commit()
    conn.close()

    source = make_source(populated_db)
    monkeypatch.chdir(str(cwd))
    assert source.detect_current_session() == "sess-1"


def test_detect_current_session_falls_back_to_most_recent(
    populated_db: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    source = make_source(populated_db)
    # A real cwd that matches no session directory.
    unmatched = tmp_path / "unmatched"
    unmatched.mkdir()
    monkeypatch.chdir(str(unmatched))
    # sess-2 has the latest time_updated.
    assert source.detect_current_session() == "sess-2"


def test_detect_current_session_returns_none_when_db_missing(tmp_path: Path) -> None:
    missing = tmp_path / "missing.db"
    source = make_source(missing)
    assert source.detect_current_session() is None


def test_detect_current_session_returns_none_when_empty_db(db_path: Path) -> None:
    source = make_source(db_path)
    assert source.detect_current_session() is None
