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

from __future__ import annotations

import json
import sqlite3
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import patch

import pytest

from devin_session import DevinConfig, DevinSource
from devin_session.source import parse_datetime
from session_store import SessionExport, SessionInfo
from session_store.errors import SessionLoadError, SessionNotFoundError

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

SCHEMA = """
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    title TEXT,
    working_directory TEXT,
    last_activity_at TEXT,
    created_at TEXT,
    model TEXT,
    backend_type TEXT,
    agent_mode TEXT,
    metadata TEXT
);
CREATE TABLE message_nodes (
    row_id INTEGER,
    session_id TEXT,
    parent_node_id INTEGER,
    chat_message TEXT,
    created_at INTEGER
);
CREATE TABLE refinery_schema_history (
    version INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    applied_on TEXT NOT NULL
);
"""


def make_db(tmp_path: Path, sessions: list[dict], nodes: list[dict]) -> Path:
    """Create a temp SQLite DB with the Devin schema and insert rows."""
    db_path = tmp_path / "sessions.db"
    conn = sqlite3.connect(db_path)
    try:
        conn.executescript(SCHEMA)
        for s in sessions:
            conn.execute(
                "INSERT INTO sessions (id, title, working_directory, last_activity_at, "
                "created_at, model, backend_type, agent_mode, metadata) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
                (
                    s["id"],
                    s.get("title", ""),
                    s.get("working_directory", ""),
                    s.get("last_activity_at", ""),
                    s.get("created_at", ""),
                    s.get("model"),
                    s.get("backend_type"),
                    s.get("agent_mode"),
                    s.get("metadata"),
                ),
            )
        for n in nodes:
            conn.execute(
                "INSERT INTO message_nodes (row_id, session_id, parent_node_id, "
                "chat_message, created_at) "
                "VALUES (?, ?, ?, ?, ?)",
                (
                    n.get("row_id", 0),
                    n["session_id"],
                    n.get("parent_node_id"),
                    n.get("chat_message"),
                    n.get("created_at", 0),
                ),
            )
        conn.commit()
    finally:
        conn.close()
    return db_path


def make_source(db_path: Path) -> DevinSource:
    """Build a DevinSource pointed at the given temp DB, forcing SQLite mode."""
    config = DevinConfig(db_path=db_path)
    source = DevinSource(config=config)
    # Force SQLite fallback by making shutil.which always return None.
    source._which_patch = patch("devin_session.source.shutil.which", return_value=None)
    source._which_patch.start()
    return source


@pytest.fixture()
def source_and_db(tmp_path):
    sessions = [
        {
            "id": "sess-1",
            "title": "First session",
            "working_directory": "/tmp/project",
            "last_activity_at": "2026-07-14T12:00:00Z",
            "created_at": "2026-07-14T10:00:00Z",
            "model": "glm-5-2",
            "backend_type": "cloud",
            "agent_mode": "interactive",
            "metadata": json.dumps({"custom_key": "custom_value"}),
        },
        {
            "id": "sess-2",
            "title": "Second session",
            "working_directory": "/tmp/other",
            "last_activity_at": "2026-07-15T09:00:00Z",
            "created_at": "2026-07-15T08:00:00Z",
        },
    ]
    nodes = [
        {
            "row_id": 1,
            "session_id": "sess-1",
            "parent_node_id": None,
            "chat_message": json.dumps(
                {
                    "role": "user",
                    "content": "Hello",
                    "message_id": "msg-1",
                    "created_at": "2026-07-14T10:05:00Z",
                }
            ),
            "created_at": 1720940700,
        },
        {
            "row_id": 2,
            "session_id": "sess-1",
            "parent_node_id": 1,
            "chat_message": json.dumps(
                {
                    "role": "assistant",
                    "content": "Here is the implementation of your feature.",
                    "message_id": "msg-2",
                    "created_at": "2026-07-14T10:10:00Z",
                    "tool_calls": [{"id": "call-1", "name": "edit"}],
                    "tool_call_id": "call-1",
                    "metadata": {"extra": "data"},
                }
            ),
            "created_at": 1720941000,
        },
        {
            "row_id": 3,
            "session_id": "sess-2",
            "parent_node_id": None,
            "chat_message": json.dumps(
                {
                    "role": "user",
                    "content": "Build it",
                    "message_id": "msg-3",
                    "created_at": "2026-07-15T08:30:00Z",
                }
            ),
            "created_at": 1721027400,
        },
    ]
    db_path = make_db(tmp_path, sessions, nodes)
    source = make_source(db_path)
    yield source, db_path
    source._which_patch.stop()


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


def test_tool_name(source_and_db):
    source, _ = source_and_db
    assert source.tool_name() == "devin"


def test_list_sessions_orders_by_last_activity_desc(source_and_db):
    source, _ = source_and_db
    sessions = source.list_sessions()
    assert len(sessions) == 2
    assert all(isinstance(s, SessionInfo) for s in sessions)
    # sess-2 has later last_activity_at (2026-07-15) than sess-1 (2026-07-14)
    assert sessions[0].session_id == "sess-2"
    assert sessions[1].session_id == "sess-1"
    assert sessions[0].message_count == 1
    assert sessions[1].message_count == 2
    assert sessions[0].tool == "devin"
    assert sessions[0].title == "Second session"
    assert sessions[0].working_directory == "/tmp/other"
    assert sessions[1].working_directory == "/tmp/project"


def test_list_sessions_returns_empty_when_db_missing(tmp_path):
    missing = tmp_path / "does-not-exist.db"
    source = make_source(missing)
    try:
        assert source.list_sessions() == []
    finally:
        source._which_patch.stop()


def test_load_session_returns_export_with_parsed_messages(source_and_db):
    source, _ = source_and_db
    export = source.load_session("sess-1")
    assert isinstance(export, SessionExport)
    assert export.tool == "devin"
    assert export.session_id == "sess-1"
    assert export.title == "First session"
    assert export.working_directory == "/tmp/project"
    assert len(export.messages) == 2

    # Session-level fields populated from the session row.
    assert export.created_at == datetime(2026, 7, 14, 10, 0, tzinfo=UTC)
    assert export.last_activity_at == datetime(2026, 7, 14, 12, 0, tzinfo=UTC)
    assert export.model == "glm-5-2"
    assert export.metadata.get("backend_type") == "cloud"
    assert export.metadata.get("agent_mode") == "interactive"
    assert export.metadata.get("custom_key") == "custom_value"

    msg0 = export.messages[0]
    assert msg0.node_id == "1"
    assert msg0.parent_node_id is None
    assert msg0.message_id == "msg-1"
    assert msg0.role == "user"
    assert msg0.content == "Hello"
    assert msg0.created_at == datetime(2026, 7, 14, 10, 5, tzinfo=UTC)
    assert msg0.metadata == {}

    msg1 = export.messages[1]
    assert msg1.node_id == "2"
    assert msg1.parent_node_id == "1"
    assert msg1.role == "assistant"
    assert msg1.content == "Here is the implementation of your feature."
    # Tool-specific fields preserved in metadata.
    assert msg1.metadata.get("tool_calls") == [{"id": "call-1", "name": "edit"}]
    assert msg1.metadata.get("tool_call_id") == "call-1"
    assert msg1.metadata.get("extra") == "data"


def test_load_session_raises_not_found_for_unknown_id(source_and_db):
    source, _ = source_and_db
    with pytest.raises(SessionNotFoundError):
        source.load_session("does-not-exist")


def test_load_session_raises_load_error_when_db_missing(tmp_path):
    missing = tmp_path / "nope.db"
    source = make_source(missing)
    try:
        with pytest.raises(SessionLoadError):
            source.load_session("sess-1")
    finally:
        source._which_patch.stop()


def test_detect_current_session_by_cwd(source_and_db, tmp_path):
    source, db_path = source_and_db
    # Insert a session whose working_directory matches the current cwd.
    cwd = str(tmp_path)
    conn = sqlite3.connect(db_path)
    conn.execute(
        "INSERT INTO sessions (id, title, working_directory, last_activity_at, created_at) "
        "VALUES (?, ?, ?, ?, ?)",
        ("sess-cwd", "cwd session", cwd, "2026-07-16T00:00:00Z", "2026-07-16T00:00:00Z"),
    )
    conn.commit()
    conn.close()

    with patch("devin_session.source.Path.cwd", return_value=Path(cwd)):
        sid = source.detect_current_session()
    assert sid == "sess-cwd"


def test_detect_current_session_falls_back_to_most_recent(source_and_db):
    source, _ = source_and_db
    # cwd won't match either session, so should return most recent = sess-2
    with patch("devin_session.source.Path.cwd", return_value=Path("/nonexistent/cwd")):
        sid = source.detect_current_session()
    assert sid == "sess-2"


def test_detect_current_session_returns_none_when_db_missing(tmp_path):
    missing = tmp_path / "nope.db"
    source = make_source(missing)
    try:
        assert source.detect_current_session() is None
    finally:
        source._which_patch.stop()


def test_chat_message_parse_failure_logs_warning_and_empty_content(tmp_path):
    sessions = [
        {
            "id": "sess-bad",
            "title": "Bad session",
            "working_directory": "/tmp",
            "last_activity_at": "2026-07-14T12:00:00Z",
            "created_at": "2026-07-14T10:00:00Z",
        }
    ]
    nodes = [
        {
            "row_id": 1,
            "session_id": "sess-bad",
            "parent_node_id": None,
            "chat_message": "{not valid json",
            "created_at": 1720940700,
        }
    ]
    db_path = make_db(tmp_path, sessions, nodes)
    source = make_source(db_path)
    try:
        export = source.load_session("sess-bad")
        assert len(export.messages) == 1
        msg = export.messages[0]
        assert msg.content == ""
        assert msg.role == ""
        assert msg.node_id == "1"
    finally:
        source._which_patch.stop()


def test_parse_datetime_handles_z_suffix():
    dt = parse_datetime("2026-07-14T10:30:00Z")
    assert dt == datetime(2026, 7, 14, 10, 30, tzinfo=UTC)


def test_parse_datetime_handles_offset():
    dt = parse_datetime("2026-07-14T10:30:00+02:00")
    assert dt.tzinfo is not None


def test_parse_datetime_fallback_for_none():
    dt = parse_datetime(None)
    assert dt.tzinfo is not None


def test_parse_datetime_fallback_for_invalid():
    dt = parse_datetime("not-a-date")
    assert dt.tzinfo is not None


def test_detect_schema_version_returns_max_version(tmp_path):
    """detect_schema_version reads refinery_schema_history and returns MAX(version)."""
    sessions = [{"id": "s1", "working_directory": "/tmp", "last_activity_at": 1720936800}]
    nodes: list[dict] = []
    db_path = make_db(tmp_path, sessions, nodes)
    # Insert migration history.
    conn = sqlite3.connect(db_path)
    conn.executemany(
        "INSERT INTO refinery_schema_history (version, name, applied_on) VALUES (?, ?, ?)",
        [
            (1, "initial_schema", "2026-05-02T15:44:57Z"),
            (5, "message_forest", "2026-05-02T15:44:57Z"),
            (16, "add_session_json_metadata", "2026-06-17T17:45:50Z"),
        ],
    )
    conn.commit()
    conn.close()

    source = make_source(db_path)
    try:
        assert source.detect_schema_version() == 16
    finally:
        source._which_patch.stop()


def test_detect_schema_version_returns_none_when_db_missing(tmp_path):
    """detect_schema_version returns None when the DB file doesn't exist."""
    source = DevinSource(config=DevinConfig(db_path=tmp_path / "nonexistent.db"))
    assert source.detect_schema_version() is None


def test_load_session_includes_schema_version_in_metadata(source_and_db):
    """load_session should include schema_version in the export metadata."""
    source, _ = source_and_db
    # Insert migration history into the fixture DB.
    db_path = source.config.db_path
    conn = sqlite3.connect(db_path)
    conn.executemany(
        "INSERT INTO refinery_schema_history (version, name, applied_on) VALUES (?, ?, ?)",
        [
            (1, "initial_schema", "2026-05-02T15:44:57Z"),
            (5, "message_forest", "2026-05-02T15:44:57Z"),
            (16, "add_session_json_metadata", "2026-06-17T17:45:50Z"),
        ],
    )
    conn.commit()
    conn.close()

    export = source.load_session("sess-1")
    assert export.metadata.get("schema_version") == 16


def test_load_session_with_minimal_schema_missing_optional_columns(tmp_path):
    """load_session should work even when optional columns (metadata, model, etc.) are missing.

    This tests ADR 001 Layer 2: defensive column access.
    """
    db_path = tmp_path / "minimal.db"
    conn = sqlite3.connect(db_path)
    conn.executescript("""
        CREATE TABLE sessions (
            id TEXT PRIMARY KEY,
            title TEXT,
            working_directory TEXT,
            last_activity_at INTEGER,
            created_at INTEGER
        );
        CREATE TABLE message_nodes (
            row_id INTEGER,
            session_id TEXT,
            parent_node_id INTEGER,
            chat_message TEXT,
            created_at INTEGER
        );
        CREATE TABLE refinery_schema_history (
            version INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            applied_on TEXT NOT NULL
        );
    """)
    conn.execute(
        "INSERT INTO sessions (id, title, working_directory, last_activity_at, created_at) "
        "VALUES (?, ?, ?, ?, ?)",
        ("minimal-1", "Minimal", "/tmp", 1720936800, 1720936800),
    )
    conn.execute(
        "INSERT INTO message_nodes (row_id, session_id, parent_node_id, chat_message, created_at) "
        "VALUES (?, ?, ?, ?, ?)",
        (1, "minimal-1", None, json.dumps({"role": "user", "content": "Hi"}), 1720936860),
    )
    conn.execute(
        "INSERT INTO refinery_schema_history (version, name, applied_on) VALUES (?, ?, ?)",
        (5, "message_forest", "2026-05-02T15:44:57Z"),
    )
    conn.commit()
    conn.close()

    source = make_source(db_path)
    try:
        export = source.load_session("minimal-1")
        assert export.title == "Minimal"
        assert export.model is None
        assert export.metadata.get("backend_type") is None
        assert export.metadata.get("agent_mode") is None
        assert export.metadata.get("schema_version") == 5
        assert len(export.messages) == 1
        assert export.messages[0].content == "Hi"
    finally:
        source._which_patch.stop()
