"""FC-3h: /api/system/backup/* endpoint integration tests."""
from datetime import UTC, datetime, timedelta

import pytest
import pytest_asyncio
from sqlalchemy import select

from backend.app.models import BackupRun

pytestmark = pytest.mark.integration


@pytest_asyncio.fixture
async def _seed_runs(db):
    """Insert 4 BackupRun rows for paging/filter tests."""
    now = datetime.now(UTC)
    for i in range(4):
        db.add(BackupRun(
            kind="db" if i % 2 == 0 else "images",
            status="ok",
            tag=None,
            triggered_by="manual",
            started_at=now - timedelta(seconds=10 - i),
            finished_at=now - timedelta(seconds=9 - i),
            sql_path="/tmp/fake.sql" if i % 2 == 0 else None,
            tar_path=None if i % 2 == 0 else "/tmp/fake.tar.zst",
            size_bytes=100,
            manifest={},
        ))
    await db.commit()


# --- POST /db, /images ----------------------------------------------


@pytest.mark.asyncio
async def test_post_db_dispatches_and_returns_202(client, monkeypatch):
    dispatched = []
    monkeypatch.setattr(
        "backend.app.tasks.backup.backup_db_task.delay",
        lambda **kw: dispatched.append(kw),
    )
    resp = await client.post("/api/system/backup/db", json={})
    assert resp.status_code == 202
    body = await resp.get_json()
    assert body["status"] == "dispatched"
    assert dispatched == [{"tag": None, "triggered_by": "manual"}]


@pytest.mark.asyncio
async def test_post_db_persists_tag(client, monkeypatch):
    dispatched = []
    monkeypatch.setattr(
        "backend.app.tasks.backup.backup_db_task.delay",
        lambda **kw: dispatched.append(kw),
    )
    resp = await client.post(
        "/api/system/backup/db", json={"tag": "pre-cutover"},
    )
    assert resp.status_code == 202
    assert dispatched[0]["tag"] == "pre-cutover"


@pytest.mark.asyncio
async def test_post_db_rejects_tag_too_long(client):
    resp = await client.post(
        "/api/system/backup/db", json={"tag": "x" * 65},
    )
    assert resp.status_code == 400
    body = await resp.get_json()
    assert body["error"] == "invalid_tag"


@pytest.mark.asyncio
async def test_post_images_dispatches(client, monkeypatch):
    dispatched = []
    monkeypatch.setattr(
        "backend.app.tasks.backup.backup_images_task.delay",
        lambda **kw: dispatched.append(kw),
    )
    resp = await client.post("/api/system/backup/images", json={})
    assert resp.status_code == 202


# --- GET /runs -------------------------------------------------------


@pytest.mark.asyncio
async def test_runs_paginated_descending(client, _seed_runs):
    resp = await client.get("/api/system/backup/runs?limit=2")
    body = await resp.get_json()
    assert len(body["runs"]) == 2
    ids = [r["id"] for r in body["runs"]]
    assert ids == sorted(ids, reverse=True)
    assert body["next_cursor"] is not None


@pytest.mark.asyncio
async def test_runs_filter_by_kind_db(client, _seed_runs):
    resp = await client.get("/api/system/backup/runs?kind=db")
    body = await resp.get_json()
    assert all(r["kind"] == "db" for r in body["runs"])


@pytest.mark.asyncio
async def test_runs_filter_by_kind_images(client, _seed_runs):
    resp = await client.get("/api/system/backup/runs?kind=images")
    body = await resp.get_json()
    assert all(r["kind"] == "images" for r in body["runs"])


@pytest.mark.asyncio
async def test_runs_rejects_invalid_kind(client):
    resp = await client.get("/api/system/backup/runs?kind=bogus")
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_runs_rejects_invalid_limit(client):
    resp = await client.get("/api/system/backup/runs?limit=not-int")
    assert resp.status_code == 400


# --- GET /runs/<id> --------------------------------------------------


@pytest.mark.asyncio
async def test_get_run_returns_row(client, _seed_runs):
    list_resp = await client.get("/api/system/backup/runs?limit=1")
    rid = (await list_resp.get_json())["runs"][0]["id"]
    resp = await client.get(f"/api/system/backup/runs/{rid}")
    assert resp.status_code == 200
    body = await resp.get_json()
    assert body["id"] == rid


@pytest.mark.asyncio
async def test_get_run_404(client):
    resp = await client.get("/api/system/backup/runs/999999")
    assert resp.status_code == 404


# --- PATCH /runs/<id> ------------------------------------------------


@pytest.mark.asyncio
async def test_patch_tag_sets_value(client, _seed_runs, db_sync):
    list_resp = await client.get("/api/system/backup/runs?limit=1")
    rid = (await list_resp.get_json())["runs"][0]["id"]

    resp = await client.patch(
        f"/api/system/backup/runs/{rid}",
        json={"tag": "monthly"},
    )
    assert resp.status_code == 200

    tag = db_sync.execute(
        select(BackupRun.tag).where(BackupRun.id == rid)
    ).scalar_one()
    assert tag == "monthly"


@pytest.mark.asyncio
async def test_patch_tag_null_clears(client, _seed_runs, db_sync):
    list_resp = await client.get("/api/system/backup/runs?limit=1")
    rid = (await list_resp.get_json())["runs"][0]["id"]

    await client.patch(f"/api/system/backup/runs/{rid}", json={"tag": "x"})
    await client.patch(f"/api/system/backup/runs/{rid}", json={"tag": None})

    tag = db_sync.execute(
        select(BackupRun.tag).where(BackupRun.id == rid)
    ).scalar_one()
    assert tag is None


# --- POST /runs/<id>/restore ----------------------------------------


@pytest.mark.asyncio
async def test_restore_wrong_confirm_400(client, _seed_runs):
    list_resp = await client.get("/api/system/backup/runs?kind=db&limit=1")
    rid = (await list_resp.get_json())["runs"][0]["id"]

    resp = await client.post(
        f"/api/system/backup/runs/{rid}/restore",
        json={"confirm": "wrong"},
    )
    assert resp.status_code == 400
    body = await resp.get_json()
    assert body["error"] == "confirm_mismatch"
    assert body["expected"] == f"restore-db-{rid}"


@pytest.mark.asyncio
async def test_restore_correct_confirm_dispatches(client, _seed_runs, monkeypatch):
    list_resp = await client.get("/api/system/backup/runs?kind=db&limit=1")
    rid = (await list_resp.get_json())["runs"][0]["id"]

    dispatched = []
    monkeypatch.setattr(
        "backend.app.tasks.backup.restore_db_task.delay",
        lambda **kw: dispatched.append(kw),
    )

    resp = await client.post(
        f"/api/system/backup/runs/{rid}/restore",
        json={"confirm": f"restore-db-{rid}"},
    )
    assert resp.status_code == 202
    assert dispatched == [{"source_backup_run_id": rid}]


@pytest.mark.asyncio
async def test_restore_rejects_non_ok_status(client, db):
    # Seed an error-status row directly.
    db.add(BackupRun(
        kind="db", status="error", tag=None, triggered_by="manual",
        started_at=datetime.now(UTC), finished_at=datetime.now(UTC),
        sql_path="/tmp/fake.sql", manifest={},
    ))
    await db.commit()
    rid_q = await client.get("/api/system/backup/runs?kind=db&limit=1")
    rid = (await rid_q.get_json())["runs"][0]["id"]

    resp = await client.post(
        f"/api/system/backup/runs/{rid}/restore",
        json={"confirm": f"restore-db-{rid}"},
    )
    assert resp.status_code == 400
    body = await resp.get_json()
    assert body["error"] == "not_restorable"


# --- DELETE /runs/<id> ----------------------------------------------


@pytest.mark.asyncio
async def test_delete_wrong_confirm_400(client, _seed_runs):
    list_resp = await client.get("/api/system/backup/runs?limit=1")
    rid = (await list_resp.get_json())["runs"][0]["id"]

    resp = await client.delete(
        f"/api/system/backup/runs/{rid}",
        json={"confirm": "wrong"},
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_delete_correct_confirm_204(client, _seed_runs, db_sync):
    list_resp = await client.get("/api/system/backup/runs?limit=1")
    row = (await list_resp.get_json())["runs"][0]
    rid = row["id"]
    kind = row["kind"]

    resp = await client.delete(
        f"/api/system/backup/runs/{rid}",
        json={"confirm": f"delete-{kind}-{rid}"},
    )
    assert resp.status_code == 204

    surviving = db_sync.execute(
        select(BackupRun.id).where(BackupRun.id == rid)
    ).scalar_one_or_none()
    assert surviving is None


# --- /settings ------------------------------------------------------


@pytest.mark.asyncio
async def test_get_settings_returns_defaults(client):
    resp = await client.get("/api/system/backup/settings")
    assert resp.status_code == 200
    body = await resp.get_json()
    assert body["backup_db_nightly_enabled"] is False
    assert body["backup_db_nightly_hour_utc"] == 3
    assert body["backup_db_keep_last_n"] == 14
    assert body["backup_images_keep_last_n"] == 3


@pytest.mark.asyncio
async def test_patch_settings_updates_values(client):
    resp = await client.patch(
        "/api/system/backup/settings",
        json={
            "backup_db_nightly_enabled": True,
            "backup_db_nightly_hour_utc": 5,
            "backup_db_keep_last_n": 30,
        },
    )
    assert resp.status_code == 200
    body = await resp.get_json()
    assert body["backup_db_nightly_enabled"] is True
    assert body["backup_db_nightly_hour_utc"] == 5
    assert body["backup_db_keep_last_n"] == 30


@pytest.mark.asyncio
async def test_patch_settings_rejects_invalid_bool(client):
    resp = await client.patch(
        "/api/system/backup/settings",
        json={"backup_db_nightly_enabled": "yes"},
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_patch_settings_rejects_hour_out_of_range(client):
    resp = await client.patch(
        "/api/system/backup/settings",
        json={"backup_db_nightly_hour_utc": 24},
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_patch_settings_rejects_keep_n_below_min(client):
    resp = await client.patch(
        "/api/system/backup/settings",
        json={"backup_db_keep_last_n": 0},
    )
    assert resp.status_code == 400
