"""End-to-end backup roundtrip: seed -> export -> wipe -> import -> verify.

Drives the backup service module directly (no HTTP layer) against a fresh
SQLite DB built in the conftest temp data dir. Verifies entity counts and
key fields survive a full round-trip.

Kept under 5s by avoiding the lifespan startup — we build a private engine
in an isolated DB file so we don't share state with other tests in the
session.
"""

from __future__ import annotations

from pathlib import Path

import pytest
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession


@pytest.fixture
async def isolated_engine(tmp_path: Path):
    """A throwaway SQLite engine + freshly created schema for one test.

    Avoids the global engine in ``database.engine`` — tests in the same
    session share that singleton, and recreating tables on it would corrupt
    parallel tests' state.
    """
    # Importing the module registers all SQLModel tables on the metadata.
    from notify_bridge_server.database import models  # noqa: F401

    db_path = tmp_path / "roundtrip.db"
    engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}")

    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)

    yield engine

    await engine.dispose()


async def _seed(session: AsyncSession, user_id: int) -> dict[str, int]:
    """Insert enough rows to exercise the major code paths in import/export."""
    from notify_bridge_server.database.models import (
        EventLog,
        NotificationTarget,
        NotificationTracker,
        ServiceProvider,
        TargetReceiver,
        TelegramBot,
        TrackingConfig,
        User,
    )

    user = User(
        id=user_id,
        username="roundtrip-user",
        hashed_password="hash",
        role="user",
    )
    session.add(user)
    await session.flush()

    bot = TelegramBot(
        user_id=user_id, name="Test bot", token="123456:fake-token-value",
        bot_username="testbot", bot_id=1,
    )
    session.add(bot)
    await session.flush()

    provider = ServiceProvider(
        user_id=user_id, type="immich", name="Immich prod",
        config={"base_url": "https://immich.example.com", "api_key": "secret"},
    )
    session.add(provider)
    await session.flush()

    target = NotificationTarget(
        user_id=user_id, type="telegram", name="My channel",
        config={"bot_token_id": bot.id, "disable_url_preview": True},
    )
    session.add(target)
    await session.flush()

    receiver = TargetReceiver(
        target_id=target.id, name="Channel A",
        config={"chat_id": "-100123"}, receiver_key="-100123", locale="en",
    )
    session.add(receiver)

    tc = TrackingConfig(
        user_id=user_id, provider_type="immich",
        name="Default Immich tracking", track_assets_added=True,
    )
    session.add(tc)
    await session.flush()

    tracker = NotificationTracker(
        user_id=user_id, provider_id=provider.id,
        name="Family album tracker", scan_interval=120,
        collection_ids=["album-uuid-1"],
    )
    session.add(tracker)
    await session.flush()

    # Capture IDs before commit — accessing attributes after commit
    # triggers a refresh that needs an async-IO context the test caller
    # may not be inside. Better to snapshot now and use plain ints later.
    ids = {
        "provider_id": provider.id,
        "target_id": target.id,
        "bot_id": bot.id,
        "tracker_id": tracker.id,
        "tracking_config_id": tc.id,
        "tracker_name": tracker.name,
        "provider_name": provider.name,
    }

    # EventLog rows are NOT in the backup schema — they're operational data,
    # not configuration. Insert a few anyway so we can verify they survive
    # the export step (since export only reads, never writes/wipes them).
    for i in range(3):
        session.add(EventLog(
            user_id=user_id, tracker_id=ids["tracker_id"], tracker_name=ids["tracker_name"],
            provider_id=ids["provider_id"], provider_name=ids["provider_name"],
            event_type="assets_added", collection_id="album-uuid-1",
            collection_name="Family", assets_count=i,
        ))

    await session.commit()

    return ids


async def _wipe_user_owned_rows(engine, user_id: int) -> None:
    """Delete every backup-able row for the user via raw SQL.

    Using ORM-level deletes triggers SQLAlchemy's cascade machinery, which
    lazy-loads relationships in a sync context that the async driver cannot
    serve (MissingGreenlet). Raw DELETE statements skip cascades and let
    SQLite's FKs enforce ordering naturally.

    Order matters: child rows first, then parents.
    """
    from sqlalchemy import text

    statements = (
        "DELETE FROM event_log",
        "DELETE FROM notification_tracker_target",
        "DELETE FROM notification_tracker",
        "DELETE FROM target_receiver",
        "DELETE FROM notification_target",
        "DELETE FROM tracking_config",
        "DELETE FROM service_provider",
        "DELETE FROM template_slot",
        "DELETE FROM template_config",
        "DELETE FROM telegram_bot",
        "DELETE FROM appsetting",
    )

    async with engine.begin() as conn:
        for stmt in statements:
            try:
                await conn.execute(text(stmt))
            except Exception:  # noqa: BLE001 — table may not exist in test schema
                pass


@pytest.mark.asyncio
async def test_export_wipe_import_roundtrip(isolated_engine, tmp_data_dir) -> None:  # noqa: ARG001
    """A full round-trip preserves entity counts and the key fields the
    UI relies on — names, configs (with secrets included), provider
    references via id_map.
    """
    from notify_bridge_server.database.models import (
        NotificationTarget, NotificationTracker, ServiceProvider,
        TargetReceiver, TelegramBot, TrackingConfig,
    )
    from notify_bridge_server.services.backup_schema import (
        ConflictMode, SecretsMode,
    )
    from notify_bridge_server.services.backup_service import (
        export_backup, import_backup,
    )

    user_id = 1

    # ---- Seed ----
    async with AsyncSession(isolated_engine) as session:
        ids = await _seed(session, user_id)

    # ---- Export with secrets included so import sees real values ----
    async with AsyncSession(isolated_engine) as session:
        backup = await export_backup(
            session, user_id, secrets_mode=SecretsMode.INCLUDE,
        )

    assert len(backup.data.providers) == 1
    assert len(backup.data.telegram_bots) == 1
    assert len(backup.data.targets) == 1
    assert len(backup.data.targets[0].receivers) == 1
    assert len(backup.data.tracking_configs) == 1
    assert len(backup.data.notification_trackers) == 1
    assert backup.data.providers[0].config["api_key"] == "secret"

    # ---- Wipe ----
    await _wipe_user_owned_rows(isolated_engine, user_id)

    async with AsyncSession(isolated_engine) as session:
        result = await session.exec(
            select(ServiceProvider).where(ServiceProvider.user_id == user_id)
        )
        assert result.all() == []

    # ---- Import ----
    async with AsyncSession(isolated_engine) as session:
        result = await import_backup(
            session, user_id, backup, conflict_mode=ConflictMode.SKIP,
        )

    assert result.errors == [], f"Import errors: {result.errors}"
    assert result.created > 0

    # ---- Verify the entities are back ----
    async with AsyncSession(isolated_engine) as session:
        providers = (await session.exec(
            select(ServiceProvider).where(ServiceProvider.user_id == user_id)
        )).all()
        assert len(providers) == 1
        prov = providers[0]
        assert prov.name == "Immich prod"
        assert prov.config["base_url"] == "https://immich.example.com"
        # Secrets imported intact when SecretsMode.INCLUDE was used at export.
        assert prov.config["api_key"] == "secret"

        bots = (await session.exec(
            select(TelegramBot).where(TelegramBot.user_id == user_id)
        )).all()
        assert len(bots) == 1
        assert bots[0].name == "Test bot"

        targets = (await session.exec(
            select(NotificationTarget).where(NotificationTarget.user_id == user_id)
        )).all()
        assert len(targets) == 1
        receivers = (await session.exec(
            select(TargetReceiver).where(TargetReceiver.target_id == targets[0].id)
        )).all()
        assert len(receivers) == 1
        assert receivers[0].config["chat_id"] == "-100123"

        tcs = (await session.exec(
            select(TrackingConfig).where(TrackingConfig.user_id == user_id)
        )).all()
        assert len(tcs) == 1
        assert tcs[0].name == "Default Immich tracking"

        trackers = (await session.exec(
            select(NotificationTracker).where(NotificationTracker.user_id == user_id)
        )).all()
        assert len(trackers) == 1
        # provider_id was remapped via id_map — original provider id may have
        # changed across the wipe, so just check it links to a real row.
        assert trackers[0].provider_id == prov.id
        assert trackers[0].scan_interval == 120
        assert trackers[0].collection_ids == ["album-uuid-1"]
