import os
import sqlite3
import subprocess
import sys
import time
from pathlib import Path

import codex_state


def _state_db(codex_home: Path, status: str, *, version: int = 5) -> Path:
    codex_home.mkdir(parents=True, exist_ok=True)
    db_path = codex_home / f"state_{version}.sqlite"
    connection = sqlite3.connect(db_path)
    with connection:
        connection.execute(
            "CREATE TABLE backfill_state (id INTEGER PRIMARY KEY, status TEXT, "
            "last_watermark TEXT, last_success_at INTEGER, updated_at INTEGER)"
        )
        connection.execute(
            "INSERT INTO backfill_state (id, status, updated_at) VALUES (1, ?, ?)",
            (status, int(time.time())),
        )
    connection.close()
    return db_path


def _status(db_path: Path) -> str:
    connection = sqlite3.connect(db_path)
    try:
        return connection.execute("SELECT status FROM backfill_state WHERE id = 1").fetchone()[0]
    finally:
        connection.close()


def test_a_home_without_a_state_db_needs_no_warmup(tmp_path: Path) -> None:
    assert codex_state.needs_warmup(tmp_path) is False


def test_needs_warmup_reads_the_highest_state_db_version(tmp_path: Path) -> None:
    _state_db(tmp_path, "complete", version=5)
    _state_db(tmp_path, "running", version=10)

    assert codex_state.needs_warmup(tmp_path) is True


def test_a_finished_backfill_needs_no_warmup(tmp_path: Path) -> None:
    _state_db(tmp_path, "complete")

    assert codex_state.needs_warmup(tmp_path) is False


def test_release_orphaned_claim_reopens_a_claim_no_worker_holds(tmp_path: Path) -> None:
    db_path = _state_db(tmp_path, "running")

    assert codex_state.release_orphaned_claim(tmp_path) is True
    assert _status(db_path) == "pending"


def test_release_orphaned_claim_keeps_a_claim_a_live_worker_holds(tmp_path: Path) -> None:
    db_path = _state_db(tmp_path, "running")

    with codex_state.warmup_claim(tmp_path):
        assert codex_state.release_orphaned_claim(tmp_path) is False

    assert _status(db_path) == "running"


def test_release_orphaned_claim_leaves_a_pending_backfill_alone(tmp_path: Path) -> None:
    db_path = _state_db(tmp_path, "pending")

    assert codex_state.release_orphaned_claim(tmp_path) is False
    assert _status(db_path) == "pending"


def test_release_orphaned_claim_keeps_a_claim_an_unmarked_worker_holds(tmp_path: Path) -> None:
    db_path = _state_db(tmp_path, "running")

    hold = "import sqlite3,sys,time; sqlite3.connect(sys.argv[1]); time.sleep(30)"
    holder = subprocess.Popen([sys.executable, "-c", hold, str(db_path)])
    try:
        _wait_until_open(db_path, holder.pid)
        assert codex_state.release_orphaned_claim(tmp_path) is False
    finally:
        holder.kill()
        holder.wait()

    assert _status(db_path) == "running"


def _wait_until_open(db_path: Path, pid: int) -> None:
    deadline = time.monotonic() + 10.0
    fd_dir = Path("/proc") / str(pid) / "fd"
    while time.monotonic() < deadline:
        try:
            if any(entry.resolve() == db_path.resolve() for entry in fd_dir.iterdir()):
                return
        except OSError:
            pass
        time.sleep(0.05)
    raise AssertionError(f"pid {pid} never opened {db_path}")


def test_warmup_claim_marks_progress_only_inside_the_block(tmp_path: Path) -> None:
    assert codex_state.warmup_in_progress(tmp_path) is False

    with codex_state.warmup_claim(tmp_path):
        assert codex_state.warmup_in_progress(tmp_path) is True

    assert codex_state.warmup_in_progress(tmp_path) is False


def test_warmup_in_progress_ignores_a_marker_from_a_dead_process(tmp_path: Path) -> None:
    dead_pid = _dead_pid()
    (tmp_path / ".systray-backfill-worker").write_text(f"{dead_pid}\n", encoding="utf-8")

    assert codex_state.warmup_in_progress(tmp_path) is False


def test_warmup_in_progress_ignores_a_marker_older_than_the_warmup_budget(
    tmp_path: Path,
) -> None:
    marker = tmp_path / ".systray-backfill-worker"
    marker.write_text(f"{os.getpid()}\n", encoding="utf-8")
    stale = time.time() - codex_state.WARMUP_TIMEOUT_S - 1
    os.utime(marker, (stale, stale))

    assert codex_state.warmup_in_progress(tmp_path) is False


def _dead_pid() -> int:
    process = subprocess.Popen([sys.executable, "-c", ""])
    process.wait()
    return process.pid
