"""SQLite backend tests for RCA state persistence."""

from __future__ import annotations

import concurrent.futures
import sqlite3
from contextlib import closing
from pathlib import Path

import pytest

from llmdebug import _json as json
from llmdebug import state_db
from llmdebug.rca_state import advance_rca_attempt, get_rca_history, get_rca_status


def test_sqlite_backend_persists_and_reads(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.setenv("LLMDEBUG_RCA_BACKEND", "sqlite")
    out_dir = str(tmp_path)

    first = advance_rca_attempt(
        out_dir=out_dir,
        session_id="sess-1",
        action="bind_evidence",
        failure_signature="sig-1",
        evidence_refs=["r1"],
    )
    second = advance_rca_attempt(
        out_dir=out_dir,
        session_id="sess-1",
        action="hypothesize",
        failure_signature="sig-1",
        evidence_refs=["r1"],
        hypothesis="h1",
        experiment_plan="p1",
    )

    status = get_rca_status(out_dir=out_dir, session_id="sess-1")
    assert status is not None
    assert status["attempt_id"] == second["attempt_id"]

    history = get_rca_history(out_dir=out_dir, session_id="sess-1", limit=10)
    assert len(history) == 2
    assert history[0]["attempt_id"] == second["attempt_id"]
    assert history[1]["attempt_id"] == first["attempt_id"]

    db_path = Path(out_dir) / "llmdebug_state.sqlite3"
    assert db_path.exists()

    with closing(sqlite3.connect(db_path)) as conn:
        row = conn.execute("SELECT COUNT(*) FROM rca_attempts").fetchone()
    assert row is not None
    assert int(row[0]) == 2


def test_sqlite_backend_migrates_jsonl_and_keeps_backup(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.setenv("LLMDEBUG_RCA_BACKEND", "sqlite")
    out_dir = str(tmp_path)
    jsonl_path = Path(out_dir) / "rca_attempts.jsonl"

    records = [
        {
            "attempt_id": "a1",
            "session_id": "sess-a",
            "updated_at": "2026-02-16T10:00:00Z",
            "created_at": "2026-02-16T10:00:00Z",
            "failure_signature": "sig-a",
            "state": "captured",
        },
        {
            "attempt_id": "a2",
            "session_id": "sess-a",
            "updated_at": "2026-02-16T10:01:00Z",
            "created_at": "2026-02-16T10:00:00Z",
            "failure_signature": "sig-a",
            "state": "evidence_bound",
        },
    ]
    jsonl_path.write_text(
        "\n".join([json.dumps(records[0]), "{bad-json", json.dumps(records[1])]) + "\n",
        encoding="utf-8",
    )

    history = get_rca_history(out_dir=out_dir, session_id="sess-a", limit=10)
    assert len(history) == 2
    assert history[0]["attempt_id"] == "a2"

    backup_path = Path(f"{jsonl_path}.bak")
    assert backup_path.exists()
    assert not jsonl_path.exists()

    db_path = Path(out_dir) / "llmdebug_state.sqlite3"
    with closing(sqlite3.connect(db_path)) as conn:
        row = conn.execute("SELECT COUNT(*) FROM rca_attempts").fetchone()
    assert row is not None
    assert int(row[0]) == 2


def test_sqlite_backend_concurrent_appends_are_queryable(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.setenv("LLMDEBUG_RCA_BACKEND", "sqlite")
    out_dir = str(tmp_path)
    writes = 64

    def _append(i: int) -> None:
        advance_rca_attempt(
            out_dir=out_dir,
            session_id="sess-concurrent",
            action="bind_evidence",
            failure_signature=f"sig-{i % 3}",
            evidence_refs=[f"ref-{i}"],
        )

    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
        list(executor.map(_append, range(writes)))

    history = get_rca_history(out_dir=out_dir, session_id="sess-concurrent", limit=200)
    assert len(history) == writes


def test_enable_wal_mode_retries_transient_database_locked(monkeypatch) -> None:
    class _FlakyConn:
        def __init__(self) -> None:
            self.calls = 0

        def execute(self, sql: str) -> None:
            assert sql == "PRAGMA journal_mode=WAL"
            self.calls += 1
            if self.calls < 3:
                raise sqlite3.OperationalError("database is locked")

    sleeps: list[float] = []
    monkeypatch.setattr(state_db.time, "sleep", lambda value: sleeps.append(float(value)))

    conn = _FlakyConn()
    state_db._enable_wal_mode(conn, attempts=5, base_sleep_sec=0.001)

    assert conn.calls == 3
    assert sleeps == [0.001, 0.002]


def test_enable_wal_mode_does_not_retry_non_lock_errors(monkeypatch) -> None:
    class _BadConn:
        def execute(self, sql: str) -> None:
            assert sql == "PRAGMA journal_mode=WAL"
            raise sqlite3.OperationalError('near "wal": syntax error')

    slept = {"value": False}
    monkeypatch.setattr(state_db.time, "sleep", lambda _value: slept.update(value=True))

    with pytest.raises(sqlite3.OperationalError, match="syntax error"):
        state_db._enable_wal_mode(_BadConn(), attempts=3, base_sleep_sec=0.001)

    assert slept["value"] is False


def test_configure_connection_invalid_busy_timeout_falls_back(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    class _Conn:
        def __init__(self) -> None:
            self.commands: list[str] = []

        def execute(self, sql: str) -> None:
            self.commands.append(sql)

    monkeypatch.setenv("LLMDEBUG_DB_BUSY_TIMEOUT_MS", "nope")
    monkeypatch.setattr(state_db, "_configure_wal_mode_once", lambda _conn, *, db_path: None)

    conn = _Conn()
    state_db._configure_connection(conn, db_path=tmp_path / "llmdebug_state.sqlite3")

    assert conn.commands[0] == f"PRAGMA busy_timeout={state_db._BUSY_TIMEOUT_MS}"
    assert "PRAGMA synchronous=NORMAL" in conn.commands
    assert "PRAGMA temp_store=MEMORY" in conn.commands
