from datetime import UTC, datetime, timedelta

import pytest
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from session_hub_core.config import DatabaseConfig
from session_hub_core.exceptions import QueryError, SessionNotFoundError
from session_hub_core.models import SessionModel
from session_hub_core.query import QueryAPI
from session_hub_core.sync import SyncEngine
from session_store import SessionExport, SessionMessage

NOW = datetime.now(UTC)
HOST = "query-host"


def make_message(
    tool: str,
    session_id: str,
    node_id: str,
    role: str = "user",
    content: str = "hello",
) -> SessionMessage:
    return SessionMessage(
        tool=tool,
        session_id=session_id,
        node_id=node_id,
        parent_node_id=None,
        message_id=f"msg-{node_id}",
        role=role,
        content=content,
        created_at=NOW - timedelta(hours=1),
        metadata={},
    )


def make_export(
    tool: str,
    session_id: str,
    title: str,
    directory: str,
    messages: list[SessionMessage],
) -> SessionExport:
    return SessionExport(
        tool=tool,
        session_id=session_id,
        title=title,
        working_directory=directory,
        created_at=NOW - timedelta(hours=2),
        last_activity_at=NOW - timedelta(minutes=5),
        model=None,
        messages=messages,
        metadata={"note": "seed"},
    )


@pytest.fixture(autouse=True)
async def seed_data(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    async with session_factory() as conn:
        await conn.execute(text("TRUNCATE sessions, messages, sync_log RESTART IDENTITY CASCADE"))
        await conn.commit()
    sync = SyncEngine(db_config, host=HOST)
    try:
        await sync.sync_session_export(
            make_export(
                "devin",
                "sess-a",
                "Fix postgres connection",
                "/work/backend",
                [
                    make_message(
                        "devin",
                        "sess-a",
                        "n1",
                        role="assistant",
                        content="the postgres connection keeps failing",
                    ),
                    make_message(
                        "devin",
                        "sess-a",
                        "n2",
                        role="user",
                        content="please check the database logs",
                    ),
                ],
            )
        )
        await sync.sync_session_export(
            make_export(
                "opencode",
                "sess-b",
                "Write parser tests",
                "/work/tests",
                [
                    make_message(
                        "opencode",
                        "sess-b",
                        "n1",
                        role="assistant",
                        content="added unit tests for the parser",
                    )
                ],
            )
        )
        await sync.sync_session_export(
            make_export(
                "copilot",
                "sess-c",
                "Refactor CLI",
                "/work/cli",
                [make_message("copilot", "sess-c", "n1", content="rename the flag")],
            )
        )
    finally:
        await sync.close()


async def test_list_sessions_all(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    query = QueryAPI(db_config)
    try:
        sessions = await query.list_sessions()
        assert {s.session_id for s in sessions} == {"sess-a", "sess-b", "sess-c"}
        assert all(s.host == HOST for s in sessions)
        activities = [s.last_activity_at for s in sessions]
        assert activities == sorted([a for a in activities if a is not None], reverse=True)
    finally:
        await query.close()


async def test_list_sessions_filters(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        devin = await query.list_sessions(tool="devin")
        assert [s.session_id for s in devin] == ["sess-a"]
        by_host = await query.list_sessions(host="other-host")
        assert by_host == []
        by_dir = await query.list_sessions(working_directory="work/back")
        assert [s.session_id for s in by_dir] == ["sess-a"]
        limited = await query.list_sessions(limit=2)
        assert len(limited) == 2
    finally:
        await query.close()


async def test_get_session_with_messages(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        detail = await query.get_session(HOST, "devin", "sess-a")
        assert detail.title == "Fix postgres connection"
        assert len(detail.messages) == 2
        assert {m.node_id for m in detail.messages} == {"n1", "n2"}
        assert detail.metadata == {"note": "seed"}
        assert detail.messages[0].role == "assistant"
    finally:
        await query.close()


async def test_get_session_not_found(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        with pytest.raises(SessionNotFoundError):
            await query.get_session(HOST, "devin", "does-not-exist")
    finally:
        await query.close()


async def test_search_messages_fts(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        results = await query.search_messages("postgres", mode="fts")
        assert len(results) == 1
        assert results[0].session_id == "sess-a"
        assert results[0].session_title == "Fix postgres connection"
        assert results[0].session_tool == "devin"
        assert results[0].rank > 0
        assert "postgres" in results[0].content
    finally:
        await query.close()


async def test_search_messages_fts_filters(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        results = await query.search_messages("parser", tool="devin", mode="fts")
        assert results == []
        results = await query.search_messages("parser", tool="opencode", mode="fts")
        assert len(results) == 1
        results = await query.search_messages("the", role="user", mode="fts")
        assert all(r.role == "user" for r in results)
    finally:
        await query.close()


async def test_search_messages_fuzzy(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        results = await query.search_messages("postgrees", mode="fuzzy")
        assert len(results) == 1
        assert results[0].session_title == "Fix postgres connection"
        assert results[0].rank > 0
    finally:
        await query.close()


async def test_search_messages_semantic_requires_embeddings(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        with pytest.raises(QueryError):
            await query.search_messages("postgres", mode="semantic")
        with pytest.raises(QueryError):
            await query.search_messages("postgres", mode="hybrid")
    finally:
        await query.close()


async def test_search_messages_unknown_mode(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        with pytest.raises(QueryError):
            await query.search_messages("postgres", mode="bogus")
    finally:
        await query.close()


async def test_search_sessions_fts(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        results = await query.search_sessions("postgres", mode="fts")
        assert [r.session_id for r in results] == ["sess-a"]
    finally:
        await query.close()


async def test_search_sessions_fuzzy(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        results = await query.search_sessions("parsr", mode="fuzzy")
        assert [r.session_id for r in results] == ["sess-b"]
    finally:
        await query.close()


async def test_get_stats(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        stats = await query.get_stats()
        assert stats.total_sessions == 3
        assert stats.total_messages == 4
        assert stats.by_host[HOST].sessions == 3
        assert stats.by_host[HOST].messages == 4
        assert stats.by_tool["devin"].sessions == 1
        assert stats.by_tool["opencode"].messages == 1
        assert stats.oldest_session is not None
        assert stats.newest_session is not None
    finally:
        await query.close()


async def test_get_hosts(db_config: DatabaseConfig) -> None:
    query = QueryAPI(db_config)
    try:
        hosts = await query.get_hosts()
        assert len(hosts) == 1
        host_info = hosts[0]
        assert host_info.host == HOST
        assert host_info.session_count == 3
        assert host_info.message_count == 4
    finally:
        await query.close()


async def test_delete_host_data(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    query = QueryAPI(db_config)
    try:
        deleted = await query.delete_host_data(HOST)
        assert deleted == 3
        async with session_factory() as conn:
            sessions = (
                (await conn.execute(select(SessionModel).where(SessionModel.host == HOST)))
                .scalars()
                .all()
            )
            assert sessions == []
            messages = (
                await conn.execute(
                    text("SELECT COUNT(*) FROM messages WHERE host = :host"),
                    {"host": HOST},
                )
            ).scalar_one()
            assert messages == 0
    finally:
        await query.close()
