"""Real database tests: run adapters against actual session DB fixture files.

These tests use real SQLite databases harvested by the harvest-session-dbs tool.
They are skipped when the fixture files are not present.

Fixture directory is configured via the SESSION_FIXTURES_DIR environment
variable (default: ~/fixtures/).

Fixture filename pattern: <tool>.<host>.<version>.<date>.db
"""

from __future__ import annotations

import os
import sqlite3
from pathlib import Path

import pytest

from devin_session import DevinConfig, DevinSource
from opencode_session import OpenCodeConfig, OpenCodeSource
from session_store import SessionStore, SourceRegistry

# ─── Fixture path resolution ───────────────────────────────────────────────

FIXTURES_DIR = Path(os.environ.get("SESSION_FIXTURES_DIR", Path.home() / "fixtures"))


def _find_fixtures(tool: str) -> list[Path]:
    """Find all fixture DB files for a given tool, sorted by name (newest first)."""
    if not FIXTURES_DIR.exists():
        return []
    return sorted(
        FIXTURES_DIR.glob(f"{tool}.*.db"),
        reverse=True,
    )


def _has_devin_fixtures() -> bool:
    return len(_find_fixtures("devin")) > 0


def _has_opencode_fixtures() -> bool:
    return len(_find_fixtures("opencode")) > 0


# ─── Skip conditions ───────────────────────────────────────────────────────

pytestmark = pytest.mark.skipif(
    not _has_devin_fixtures() and not _has_opencode_fixtures(),
    reason=f"No fixture DBs found in {FIXTURES_DIR}. Run harvest-session-dbs to collect them.",
)

devin_skip = pytest.mark.skipif(
    not _has_devin_fixtures(),
    reason="No Devin fixture DBs found",
)

opencode_skip = pytest.mark.skipif(
    not _has_opencode_fixtures(),
    reason="No OpenCode fixture DBs found",
)


# ─── Fixtures ──────────────────────────────────────────────────────────────


@pytest.fixture
def devin_fixture_path() -> Path:
    """Path to the most recent Devin fixture DB."""
    fixtures = _find_fixtures("devin")
    if not fixtures:
        pytest.skip("No Devin fixture DBs found")
    return fixtures[0]


@pytest.fixture
def opencode_fixture_path() -> Path:
    """Path to the most recent OpenCode fixture DB."""
    fixtures = _find_fixtures("opencode")
    if not fixtures:
        pytest.skip("No OpenCode fixture DBs found")
    return fixtures[0]


@pytest.fixture
def devin_source(devin_fixture_path: Path) -> DevinSource:
    """DevinSource configured to use the real fixture DB."""
    return DevinSource(DevinConfig(db_path=devin_fixture_path))


@pytest.fixture
def opencode_source(opencode_fixture_path: Path) -> OpenCodeSource:
    """OpenCodeSource configured to use the real fixture DB."""
    return OpenCodeSource(OpenCodeConfig(db_path=opencode_fixture_path))


@pytest.fixture
def real_store(devin_source: DevinSource, opencode_source: OpenCodeSource) -> SessionStore:
    """SessionStore with real adapters pointing at fixture DBs."""
    registry = SourceRegistry()
    registry.register(devin_source)
    registry.register(opencode_source)
    return SessionStore(registry=registry)


# ─── Schema compatibility tests ────────────────────────────────────────────


@devin_skip
class TestDevinSchema:
    """Verify our queries work against the real Devin DB schema."""

    def test_sessions_table_has_expected_columns(self, devin_fixture_path: Path) -> None:
        """The sessions table must have the columns our queries reference."""
        conn = sqlite3.connect(str(devin_fixture_path))
        cols = {row[1] for row in conn.execute("PRAGMA table_info(sessions)").fetchall()}
        conn.close()
        expected = {
            "id",
            "title",
            "working_directory",
            "last_activity_at",
            "created_at",
            "model",
            "backend_type",
            "agent_mode",
            "metadata",
        }
        missing = expected - cols
        assert not missing, f"Missing columns in sessions table: {missing}"

    def test_message_nodes_table_has_expected_columns(self, devin_fixture_path: Path) -> None:
        """The message_nodes table must have the columns our queries reference."""
        conn = sqlite3.connect(str(devin_fixture_path))
        cols = {row[1] for row in conn.execute("PRAGMA table_info(message_nodes)").fetchall()}
        conn.close()
        # Our queries reference: id, parent_id, chat_message, created_at, session_id
        expected = {"chat_message", "created_at", "session_id"}
        missing = expected - cols
        assert not missing, f"Missing columns in message_nodes table: {missing}"

    def test_sessions_table_has_data(self, devin_fixture_path: Path) -> None:
        """The fixture DB should contain at least one session."""
        conn = sqlite3.connect(str(devin_fixture_path))
        count = conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0]
        conn.close()
        assert count > 0, "No sessions in fixture DB"

    def test_message_nodes_has_data(self, devin_fixture_path: Path) -> None:
        """The fixture DB should contain at least one message node."""
        conn = sqlite3.connect(str(devin_fixture_path))
        count = conn.execute("SELECT COUNT(*) FROM message_nodes").fetchone()[0]
        conn.close()
        assert count > 0, "No message_nodes in fixture DB"


@opencode_skip
class TestOpenCodeSchema:
    """Verify our queries work against the real OpenCode DB schema."""

    def test_session_table_has_expected_columns(self, opencode_fixture_path: Path) -> None:
        """The session table must have the columns our queries reference."""
        conn = sqlite3.connect(str(opencode_fixture_path))
        cols = {row[1] for row in conn.execute("PRAGMA table_info(session)").fetchall()}
        conn.close()
        expected = {
            "id",
            "title",
            "directory",
            "time_updated",
            "time_created",
            "model",
            "slug",
            "cost",
            "tokens_input",
            "tokens_output",
            "tokens_reasoning",
            "metadata",
        }
        missing = expected - cols
        assert not missing, f"Missing columns in session table: {missing}"

    def test_message_table_has_expected_columns(self, opencode_fixture_path: Path) -> None:
        """The message table must have the columns our queries reference."""
        conn = sqlite3.connect(str(opencode_fixture_path))
        cols = {row[1] for row in conn.execute("PRAGMA table_info(message)").fetchall()}
        conn.close()
        expected = {"id", "session_id", "data", "time_created"}
        missing = expected - cols
        assert not missing, f"Missing columns in message table: {missing}"

    def test_part_table_has_expected_columns(self, opencode_fixture_path: Path) -> None:
        """The part table must have the columns our queries reference."""
        conn = sqlite3.connect(str(opencode_fixture_path))
        cols = {row[1] for row in conn.execute("PRAGMA table_info(part)").fetchall()}
        conn.close()
        expected = {"id", "data"}
        missing = expected - cols
        assert not missing, f"Missing columns in part table: {missing}"

    def test_session_table_has_data(self, opencode_fixture_path: Path) -> None:
        """The fixture DB should contain at least one session."""
        conn = sqlite3.connect(str(opencode_fixture_path))
        count = conn.execute("SELECT COUNT(*) FROM session").fetchone()[0]
        conn.close()
        assert count > 0, "No sessions in fixture DB"

    def test_message_table_has_data(self, opencode_fixture_path: Path) -> None:
        """The fixture DB should contain at least one message."""
        conn = sqlite3.connect(str(opencode_fixture_path))
        count = conn.execute("SELECT COUNT(*) FROM message").fetchone()[0]
        conn.close()
        assert count > 0, "No messages in fixture DB"


# ─── Adapter functionality tests ───────────────────────────────────────────


@devin_skip
class TestDevinAdapterReal:
    """Test DevinSource against real Devin session data."""

    def test_list_sessions_returns_results(self, devin_source: DevinSource) -> None:
        sessions = devin_source.list_sessions(limit=10)
        assert len(sessions) > 0
        for s in sessions:
            assert s.tool == "devin"
            assert s.message_count >= 0
            # session_id may be empty string for some real sessions (data issue in DB)

    def test_list_sessions_limit(self, devin_source: DevinSource) -> None:
        sessions = devin_source.list_sessions(limit=3)
        assert len(sessions) <= 3

    def test_get_session_real(self, devin_source: DevinSource) -> None:
        """Get a real session and verify it has messages."""
        sessions = devin_source.list_sessions(limit=1)
        assert len(sessions) > 0
        session_id = sessions[0].session_id

        export = devin_source.load_session(session_id)
        assert export.session_id == session_id
        assert export.tool == "devin"
        assert len(export.messages) > 0

        # At least one message should have content
        has_content = any(m.content for m in export.messages)
        assert has_content, "No messages with content found"

    def test_get_session_message_roles(self, devin_source: DevinSource) -> None:
        """Real sessions should have both user and assistant messages."""
        sessions = devin_source.list_sessions(limit=1)
        if not sessions:
            pytest.skip("No sessions")
        export = devin_source.load_session(sessions[0].session_id)
        roles = {m.role for m in export.messages}
        # At least one of these should be present
        assert roles & {"user", "assistant"}, f"Expected user/assistant roles, got: {roles}"

    def test_detect_current_session(self, devin_source: DevinSource) -> None:
        """detect_current_session should return a session ID or None."""
        sessions = devin_source.list_sessions(limit=1)
        if not sessions:
            pytest.skip("No sessions")
        # Just verify it doesn't crash
        result = devin_source.detect_current_session()
        # Result can be None if no session matches CWD
        if result is not None:
            assert isinstance(result, str)


@opencode_skip
class TestOpenCodeAdapterReal:
    """Test OpenCodeSource against real OpenCode session data."""

    def test_list_sessions_returns_results(self, opencode_source: OpenCodeSource) -> None:
        sessions = opencode_source.list_sessions(limit=10)
        assert len(sessions) > 0
        for s in sessions:
            assert s.tool == "opencode"
            assert s.session_id
            assert s.message_count >= 0

    def test_get_session_real(self, opencode_source: OpenCodeSource) -> None:
        """Get a real OpenCode session and verify it has messages."""
        sessions = opencode_source.list_sessions(limit=1)
        if not sessions:
            pytest.skip("No sessions")
        export = opencode_source.load_session(sessions[0].session_id)
        assert export.session_id == sessions[0].session_id
        assert export.tool == "opencode"
        # Real sessions might have messages
        if len(export.messages) > 0:
            has_content = any(m.content for m in export.messages)
            assert has_content, "Messages exist but none have content"

    def test_detect_current_session(self, opencode_source: OpenCodeSource) -> None:
        """detect_current_session should not crash."""
        result = opencode_source.detect_current_session()
        if result is not None:
            assert isinstance(result, str)


# ─── SessionStore integration with real DBs ────────────────────────────────


class TestRealSessionStore:
    """Test SessionStore facade with real adapter instances."""

    def test_list_sessions_all_tools(self, real_store: SessionStore) -> None:
        sessions = real_store.list_sessions()
        assert len(sessions) > 0
        tools = {s.tool for s in sessions}
        # At least one tool should be present
        assert len(tools) >= 1

    def test_detect_tool_returns_result(self, real_store: SessionStore) -> None:
        """detect_tool should return a tool name without crashing."""
        tool = real_store.detect_tool()
        assert tool is not None
        assert tool in {"devin", "opencode"}

    def test_select_message_latest(self, real_store: SessionStore) -> None:
        """select_message('latest') should work against real data."""
        sessions = real_store.list_sessions(limit=5)
        if not sessions:
            pytest.skip("No sessions")
        # Try each session until one works
        for s in sessions:
            try:
                msg = real_store.select_message("latest", session_id=s.session_id, tool=s.tool)
                if msg is not None:
                    assert msg.role in {"user", "assistant", "system"}
                    return
            except Exception:
                continue
        pytest.skip("Could not select a message from any session")
