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.models import MessageModel, SessionModel, SyncLogModel
from session_hub_core.query import QueryAPI
from session_hub_core.sync import SyncEngine
from session_store import SessionExport, SessionInfo, SessionMessage, SessionStore

NOW = datetime.now(UTC)


def make_message(
    session_id: str,
    node_id: str,
    role: str = "user",
    content: str = "hello",
) -> SessionMessage:
    return SessionMessage(
        tool="devin",
        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={"origin": "test"},
    )


def make_export(
    session_id: str,
    messages: list[SessionMessage] | None = None,
    title: str = "Test Session",
    tool: str = "devin",
    model: str | None = "glm-5-2",
    last_activity: datetime | None = None,
) -> SessionExport:
    return SessionExport(
        tool=tool,
        session_id=session_id,
        title=title,
        working_directory="/tmp/work",
        created_at=NOW - timedelta(hours=2),
        last_activity_at=last_activity or NOW - timedelta(minutes=5),
        model=model,
        messages=messages or [],
        metadata={"foo": "bar"},
    )


def make_info(
    session_id: str,
    tool: str = "devin",
    title: str = "Test Session",
    message_count: int = 0,
    last_activity: datetime | None = None,
) -> SessionInfo:
    return SessionInfo(
        tool=tool,
        session_id=session_id,
        title=title,
        message_count=message_count,
        last_activity=last_activity or NOW - timedelta(minutes=5),
        working_directory="/tmp/work",
    )


class FakeStore(SessionStore):
    def __init__(
        self,
        sessions: list[SessionInfo],
        exports: dict[str, SessionExport],
    ) -> None:
        self._sessions = sessions
        self._exports = exports

    def list_sessions(self, tool: str | None = None, limit: int = 20) -> list[SessionInfo]:
        sessions = self._sessions if tool is None else [s for s in self._sessions if s.tool == tool]
        return sessions[:limit]

    def get_session(self, session_id: str, tool: str | None = None) -> SessionExport:
        for export in self._exports.values():
            if export.session_id == session_id and (tool is None or export.tool == tool):
                return export
        raise KeyError(f"no session {tool}/{session_id}")


@pytest.fixture(autouse=True)
def isolated_home(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("HOME", str(tmp_path))


@pytest.fixture(autouse=True)
async def clean_db(session_factory: async_sessionmaker[AsyncSession]) -> None:
    async with session_factory() as conn:
        await conn.execute(text("TRUNCATE sessions, messages, sync_log RESTART IDENTITY CASCADE"))
        await conn.commit()


async def test_new_session_inserted(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    export = make_export(
        "sess-1",
        messages=[make_message("sess-1", "n1"), make_message("sess-1", "n2", role="assistant")],
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        result = await sync.sync_session_export(export)
        assert result.sessions_upserted == 1
        assert result.messages_upserted == 2
        assert result.messages_deleted == 0
        async with session_factory() as conn:
            session = (
                await conn.execute(select(SessionModel).where(SessionModel.session_id == "sess-1"))
            ).scalar_one()
            assert session.host == "test-host"
            assert session.tool == "devin"
            assert session.title == "Test Session"
            assert session.model_name == "glm-5-2"
            assert session.working_directory == "/tmp/work"
            assert session.message_count == 2
            assert session.hidden is False
            assert session.metadata_ == {"foo": "bar"}
            messages = (
                (
                    await conn.execute(
                        select(MessageModel).where(MessageModel.session_pk == session.id)
                    )
                )
                .scalars()
                .all()
            )
            assert len(messages) == 2
            assert {m.node_id for m in messages} == {"n1", "n2"}
    finally:
        await sync.close()


async def test_existing_session_updated(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    export = make_export(
        "sess-1",
        title="Old Title",
        messages=[make_message("sess-1", "n1", content="old content")],
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        await sync.sync_session_export(export)
        updated = make_export(
            "sess-1",
            title="New Title",
            messages=[make_message("sess-1", "n1", content="new content")],
        )
        result = await sync.sync_session_export(updated)
        assert result.sessions_upserted == 1
        async with session_factory() as conn:
            session = (
                await conn.execute(select(SessionModel).where(SessionModel.session_id == "sess-1"))
            ).scalar_one()
            assert session.title == "New Title"
            message = (
                await conn.execute(select(MessageModel).where(MessageModel.node_id == "n1"))
            ).scalar_one()
            assert message.content == "new content"
            count = (
                (
                    await conn.execute(
                        select(MessageModel).where(MessageModel.session_pk == session.id)
                    )
                )
                .scalars()
                .all()
            )
            assert len(count) == 1
    finally:
        await sync.close()


async def test_deleted_messages_removed(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    sync = SyncEngine(db_config, host="test-host")
    try:
        full = make_export(
            "sess-1",
            messages=[
                make_message("sess-1", "n1"),
                make_message("sess-1", "n2"),
                make_message("sess-1", "n3"),
            ],
        )
        await sync.sync_session_export(full)
        trimmed = make_export(
            "sess-1",
            messages=[make_message("sess-1", "n1"), make_message("sess-1", "n2")],
        )
        result = await sync.sync_session_export(trimmed)
        assert result.messages_deleted == 1
        async with session_factory() as conn:
            session = (
                await conn.execute(select(SessionModel).where(SessionModel.session_id == "sess-1"))
            ).scalar_one()
            node_ids = (
                (
                    await conn.execute(
                        select(MessageModel.node_id).where(MessageModel.session_pk == session.id)
                    )
                )
                .scalars()
                .all()
            )
            assert node_ids == ["n1", "n2"]
    finally:
        await sync.close()


async def test_idempotent_resync(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    export = make_export(
        "sess-1",
        messages=[make_message("sess-1", "n1"), make_message("sess-1", "n2")],
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        first = await sync.sync_session_export(export)
        second = await sync.sync_session_export(export)
        assert first.sessions_upserted == 1
        assert second.sessions_upserted == 1
        assert second.messages_upserted == 2
        assert second.messages_deleted == 0
        async with session_factory() as conn:
            sessions = (
                (await conn.execute(select(SessionModel).where(SessionModel.host == "test-host")))
                .scalars()
                .all()
            )
            assert len(sessions) == 1
            messages = (
                (
                    await conn.execute(
                        select(MessageModel).where(MessageModel.session_id == "sess-1")
                    )
                )
                .scalars()
                .all()
            )
            assert len(messages) == 2
    finally:
        await sync.close()


async def test_multi_host_separation(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    export_a = make_export("sess-1", messages=[make_message("sess-1", "n1")])
    sync_a = SyncEngine(db_config, host="host-a")
    sync_b = SyncEngine(db_config, host="host-b")
    try:
        await sync_a.sync_session_export(export_a)
        await sync_b.sync_session_export(export_a)
        async with session_factory() as conn:
            rows = (
                await conn.execute(
                    select(SessionModel.host, SessionModel.session_id)
                    .where(SessionModel.session_id == "sess-1")
                    .order_by(SessionModel.host)
                )
            ).all()
            assert [(r[0], r[1]) for r in rows] == [("host-a", "sess-1"), ("host-b", "sess-1")]
        query = QueryAPI(db_config)
        try:
            deleted = await query.delete_host_data("host-a")
            assert deleted == 1
        finally:
            await query.close()
        async with session_factory() as conn:
            remaining = (
                (
                    await conn.execute(
                        select(SessionModel.host).where(SessionModel.session_id == "sess-1")
                    )
                )
                .scalars()
                .all()
            )
            assert remaining == ["host-b"]
    finally:
        await sync_a.close()
        await sync_b.close()


async def test_sync_tool_incremental_watermark(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    old_activity = NOW - timedelta(hours=2)
    export = make_export(
        "sess-1",
        messages=[make_message("sess-1", "n1")],
        last_activity=old_activity,
    )
    store = FakeStore(
        sessions=[make_info("sess-1", message_count=1, last_activity=old_activity)],
        exports={"sess-1": export},
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        first = await sync.sync_tool("devin", store=store)
        assert first.sessions_upserted == 1
        assert first.sessions_skipped == 0
        second = await sync.sync_tool("devin", store=store)
        assert second.sessions_upserted == 0
        assert second.sessions_skipped == 1
        async with session_factory() as conn:
            log_rows = (
                (
                    await conn.execute(
                        select(SyncLogModel)
                        .where(SyncLogModel.host == "test-host", SyncLogModel.tool == "devin")
                        .order_by(SyncLogModel.id)
                    )
                )
                .scalars()
                .all()
            )
            assert len(log_rows) == 2
            assert all(row.status == "success" for row in log_rows)
            assert log_rows[0].sessions_upserted == 1
            assert log_rows[1].sessions_upserted == 0
            sessions = (
                (
                    await conn.execute(
                        select(SessionModel).where(SessionModel.session_id == "sess-1")
                    )
                )
                .scalars()
                .all()
            )
            assert len(sessions) == 1
    finally:
        await sync.close()


async def test_sync_tool_dry_run_writes_nothing(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    export = make_export(
        "sess-1", messages=[make_message("sess-1", "n1"), make_message("sess-1", "n2")]
    )
    store = FakeStore(
        sessions=[make_info("sess-1", message_count=2)],
        exports={"sess-1": export},
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        result = await sync.sync_tool("devin", store=store, dry_run=True)
        assert result.sessions_upserted == 1
        assert result.messages_upserted == 2
        async with session_factory() as conn:
            sessions = (
                (await conn.execute(select(SessionModel).where(SessionModel.host == "test-host")))
                .scalars()
                .all()
            )
            assert len(sessions) == 0
            log_rows = (await conn.execute(select(SyncLogModel))).scalars().all()
            assert len(log_rows) == 0
    finally:
        await sync.close()


async def test_sync_all_across_tools(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    export_devin = make_export("sess-1", tool="devin", messages=[make_message("sess-1", "n1")])
    export_opencode = make_export(
        "sess-2",
        tool="opencode",
        title="OpenCode Session",
        messages=[make_message("sess-2", "n1")],
    )
    store = FakeStore(
        sessions=[
            make_info("sess-1", tool="devin", message_count=1),
            make_info("sess-2", tool="opencode", message_count=1),
        ],
        exports={"sess-1": export_devin, "sess-2": export_opencode},
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        result = await sync.sync_all(store=store)
        assert result.sessions_upserted == 2
        assert result.messages_upserted == 2
        assert result.errors == []
        async with session_factory() as conn:
            sessions = (
                (await conn.execute(select(SessionModel).where(SessionModel.host == "test-host")))
                .scalars()
                .all()
            )
            assert len(sessions) == 2
    finally:
        await sync.close()


async def test_per_session_error_continues(
    session_factory: async_sessionmaker[AsyncSession], db_config: DatabaseConfig
) -> None:
    store = FakeStore(
        sessions=[make_info("sess-1", message_count=1)],
        exports={},
    )
    sync = SyncEngine(db_config, host="test-host")
    try:
        result = await sync.sync_tool("devin", store=store)
        assert result.sessions_upserted == 0
        assert len(result.errors) == 1
        assert "sess-1" in result.errors[0]
        async with session_factory() as conn:
            log_rows = (
                (
                    await conn.execute(
                        select(SyncLogModel).where(
                            SyncLogModel.host == "test-host", SyncLogModel.tool == "devin"
                        )
                    )
                )
                .scalars()
                .all()
            )
            assert len(log_rows) == 1
            assert log_rows[0].status == "success"
    finally:
        await sync.close()
