"""Async job backbone (#25): enqueue → claim → run → poll/cancel, plus the
race-safety and crash-recovery guarantees the worker relies on."""
import threading

import pytest
from fastapi.testclient import TestClient

import app
import db
import jobs
import worker

client = TestClient(app.app)
GW = {"X-Tenant": "hexa", "X-Scopes": "api:audio:*"}
OTHER = {"X-Tenant": "someone-else", "X-Scopes": "api:audio:*"}


@pytest.fixture(autouse=True)
def _clean_jobs():
    """Each test starts with an empty jobs table (shared temp DB across the suite)."""
    db.init()
    with db.cursor() as c:
        c.execute("DELETE FROM jobs")
    jobs.HANDLERS.pop("_fake", None)
    yield


# ── enqueue / lifecycle ─────────────────────────────────────────────────────────
def test_enqueue_is_pending_then_claim_runs_it():
    j = jobs.enqueue("hexa", "_fake", {"x": 1})
    assert j["status"] == "pending" and j["job_id"].startswith("j_")

    claimed = jobs.claim()
    assert claimed["id"] == j["job_id"] and claimed["status"] == "running"
    # queue now empty
    assert jobs.claim() is None


def test_enqueue_with_result_is_born_done_idempotent_fastpath():
    j = jobs.enqueue("hexa", "_fake", {"x": 1}, result={"answer": 42})
    assert j["status"] == "done" and j["result"] == {"answer": 42}
    assert j["progress"] == 1.0
    # a done job is never claimed by the worker
    assert jobs.claim() is None


def test_finish_and_fail_requeue_then_cap():
    j = jobs.enqueue("hexa", "_fake")
    jid = j["job_id"]
    jobs.claim()
    jobs.set_progress(jid, 0.5)
    assert jobs.get(jid)["progress"] == 0.5

    # fail with requeue: attempt 1 → back to pending
    assert jobs.fail(jid, "boom", requeue=True) == "pending"
    jobs.claim()                                  # attempt 2
    assert jobs.fail(jid, "boom", requeue=True) == "pending"
    jobs.claim()                                  # attempt 3 == MAX_ATTEMPTS
    assert jobs.fail(jid, "boom", requeue=True) == "error"
    assert jobs.get(jid)["error"] == "boom"


def test_recover_requeues_inflight():
    j = jobs.enqueue("hexa", "_fake")
    jobs.claim()                                  # now 'running'
    assert jobs.get(j["job_id"])["status"] == "running"
    n = jobs.recover()                            # simulate worker reboot
    assert n == 1
    assert jobs.get(j["job_id"])["status"] == "pending"


# ── race-safety: a job is claimed by exactly one worker ─────────────────────────
def test_no_double_claim_under_concurrency():
    N = 25
    ids = {jobs.enqueue("hexa", "_fake")["job_id"] for _ in range(N)}
    claimed: list[str] = []
    lock = threading.Lock()

    def drain():
        while True:
            job = jobs.claim()
            if job is None:
                return
            with lock:
                claimed.append(job["id"])

    threads = [threading.Thread(target=drain) for _ in range(6)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert sorted(claimed) == sorted(ids)         # every job claimed…
    assert len(claimed) == len(set(claimed)) == N  # …exactly once


# ── worker dispatch ─────────────────────────────────────────────────────────────
def test_worker_runs_handler_to_done():
    @jobs.handler("_fake")
    def _run(job, progress):
        progress(0.3)
        assert job["params"] == {"k": "v"}        # params parsed for the handler
        return {"echo": job["params"]}

    jid = jobs.enqueue("hexa", "_fake", {"k": "v"})["job_id"]
    worker.run_one(jobs.claim())
    done = jobs.get(jid)
    assert done["status"] == "done" and done["result"] == {"echo": {"k": "v"}}


def test_worker_missing_handler_errors_no_requeue():
    jid = jobs.enqueue("hexa", "_nohandler")["job_id"]
    worker.run_one(jobs.claim())
    assert jobs.get(jid)["status"] == "error"


def test_worker_handler_exception_requeues():
    @jobs.handler("_fake")
    def _boom(job, progress):
        raise RuntimeError("nope")

    jid = jobs.enqueue("hexa", "_fake")["job_id"]
    worker.run_one(jobs.claim())
    assert jobs.get(jid)["status"] == "pending"   # transient → requeued under cap


def test_worker_respects_cancel_midflight():
    @jobs.handler("_fake")
    def _cancel_then_progress(job, progress):
        jobs.cancel(job["id"])                     # cancelled by someone else
        progress(0.5)                              # next checkpoint raises _Cancelled
        return {"should": "not reach"}

    jid = jobs.enqueue("hexa", "_fake")["job_id"]
    worker.run_one(jobs.claim())
    assert jobs.get(jid)["status"] == "cancelled"


# ── HTTP poll / cancel ──────────────────────────────────────────────────────────
def test_http_poll_and_ownership():
    jid = jobs.enqueue("hexa", "_fake", result={"ok": True})["job_id"]

    r = client.get(f"/jobs/{jid}", headers=GW)
    assert r.status_code == 200
    body = r.json()
    assert body["status"] == "done" and body["result"] == {"ok": True}

    # another tenant must NOT see it (404, not 403 — no existence leak)
    assert client.get(f"/jobs/{jid}", headers=OTHER).status_code == 404
    # unknown id
    assert client.get("/jobs/j_doesnotexist", headers=GW).status_code == 404


def test_http_cancel():
    jid = jobs.enqueue("hexa", "_fake")["job_id"]
    r = client.delete(f"/jobs/{jid}", headers=GW)
    assert r.status_code == 200 and r.json()["status"] == "cancelled"
    # idempotent
    assert client.delete(f"/jobs/{jid}", headers=GW).json()["status"] == "cancelled"
    # other tenant can't cancel
    jid2 = jobs.enqueue("hexa", "_fake")["job_id"]
    assert client.delete(f"/jobs/{jid2}", headers=OTHER).status_code == 404
