"""Tests for services/db_maintenance.py — mocks the engine (no real DB).

Verifies that VACUUM (ANALYZE) is issued once per allowlisted table, that the
allowlist is closed (an arbitrary name can't be vacuumed), and that a single
table failure doesn't abort the rest of the sweep.
"""
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from scribe.services.db_maintenance import MAINTENANCE_TABLES, run_maintenance
from tests.helpers import make_mock_session


def _mock_engine(exec_side_effect=None):
    """An engine whose connect() yields a conn with a recording exec_driver_sql."""
    conn = MagicMock()
    autocommit = MagicMock()
    autocommit.exec_driver_sql = AsyncMock(side_effect=exec_side_effect)
    # On AsyncConnection, execution_options() is a coroutine — mock it as async
    # so the test exercises the real (awaited) call shape.
    conn.execution_options = AsyncMock(return_value=autocommit)
    cm = AsyncMock()
    cm.__aenter__ = AsyncMock(return_value=conn)
    cm.__aexit__ = AsyncMock(return_value=False)
    engine = MagicMock()
    engine.connect.return_value = cm
    return engine, autocommit


@pytest.mark.asyncio
async def test_vacuums_each_allowlisted_table_once():
    engine, autocommit = _mock_engine()
    with patch("scribe.services.db_maintenance.engine", engine), \
         patch("scribe.services.db_maintenance.set_admin_setting", AsyncMock()):
        summary = await run_maintenance()

    issued = [c.args[0] for c in autocommit.exec_driver_sql.await_args_list]
    assert issued == [f"VACUUM (ANALYZE) {t}" for t in MAINTENANCE_TABLES]
    assert all(r["ok"] for r in summary["tables"])
    assert len(summary["tables"]) == len(MAINTENANCE_TABLES)
    # AUTOCOMMIT was requested (and awaited) — VACUUM can't run in a transaction.
    conn = engine.connect.return_value.__aenter__.return_value
    conn.execution_options.assert_awaited_once_with(isolation_level="AUTOCOMMIT")


@pytest.mark.asyncio
async def test_allowlist_is_closed():
    """A caller-supplied name not on the allowlist is silently ignored."""
    engine, autocommit = _mock_engine()
    with patch("scribe.services.db_maintenance.engine", engine), \
         patch("scribe.services.db_maintenance.set_admin_setting", AsyncMock()):
        summary = await run_maintenance(tables=["app_logs", "users; DROP TABLE notes"])

    issued = [c.args[0] for c in autocommit.exec_driver_sql.await_args_list]
    assert issued == ["VACUUM (ANALYZE) app_logs"]
    assert [r["table"] for r in summary["tables"]] == ["app_logs"]


@pytest.mark.asyncio
async def test_one_table_failure_does_not_abort_the_rest():
    # First table raises, the remaining tables still get vacuumed.
    calls = {"n": 0}

    def flaky(_sql):
        calls["n"] += 1
        if calls["n"] == 1:
            raise RuntimeError("boom")
        return MagicMock()

    engine, autocommit = _mock_engine(exec_side_effect=flaky)
    with patch("scribe.services.db_maintenance.engine", engine), \
         patch("scribe.services.db_maintenance.set_admin_setting", AsyncMock()):
        summary = await run_maintenance()

    assert autocommit.exec_driver_sql.await_count == len(MAINTENANCE_TABLES)
    assert summary["tables"][0]["ok"] is False
    assert summary["tables"][0]["error"] == "boom"
    assert all(r["ok"] for r in summary["tables"][1:])


def _health_session(db_bytes, rows):
    s = make_mock_session()
    size_res = MagicMock()
    size_res.scalar.return_value = db_bytes
    rows_res = MagicMock()
    rows_res.mappings.return_value.all.return_value = rows
    s.execute = AsyncMock(side_effect=[size_res, rows_res])
    return s


@pytest.mark.asyncio
async def test_table_health_shapes_rows_and_db_size():
    from datetime import datetime, timezone

    from scribe.services.db_maintenance import get_table_health
    vac = datetime(2026, 6, 14, 4, 0, tzinfo=timezone.utc)
    rows = [
        {"table_name": "notes", "live": 1000, "dead": 300, "dead_pct": 23.1,
         "total_bytes": 5_000_000, "mod_since_analyze": 50,
         "last_vacuum": vac, "last_analyze": None},
    ]
    with patch("scribe.services.db_maintenance.async_session",
               return_value=_health_session(42_000_000, rows)):
        health = await get_table_health()

    assert health["db_bytes"] == 42_000_000
    t = health["tables"][0]
    assert t["table"] == "notes"
    assert t["dead_pct"] == 23.1
    assert t["last_vacuum"] == vac.isoformat()
    assert t["last_analyze"] is None  # null timestamp passes through as None


@pytest.mark.asyncio
async def test_summary_is_persisted_as_admin_setting():
    engine, _ = _mock_engine()
    setter = AsyncMock()
    with patch("scribe.services.db_maintenance.engine", engine), \
         patch("scribe.services.db_maintenance.set_admin_setting", setter):
        await run_maintenance()
    setter.assert_awaited_once()
    assert setter.await_args.args[0] == "db_maintenance_last_run"
