import { test } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
  claimAiRequest,
  createAiRequest,
  ensureAiRequestsSchema,
  failStaleAiRequests,
  getAiRequest,
  waitForAiRequest,
} from "./aiRequests.ts";

test("ai request queue inserts pending row", () => {
  const { db, cleanup } = tempDb();
  try {
    const row = createAiRequest(db, { instructions: "Be concise.", prompt: "Say hi" });
    assert.equal(row.instructions, "Be concise.");
    assert.equal(row.prompt, "Say hi");
    assert.equal(row.status, "pending");
    assert.equal(row.response, null);
    assert.equal(row.error, null);
    assert.equal(row.claimed_at, null);
    assert.equal(row.claimed_by, null);
    assert.equal(typeof row.created_at, "number");
  } finally {
    cleanup();
  }
});

test("ai request poll returns response when harness marks row done", async () => {
  const { db, cleanup } = tempDb();
  try {
    const row = createAiRequest(db, { prompt: "Generate" });
    setTimeout(() => {
      db.prepare("UPDATE ai_requests SET status='done', response=?, completed_at=? WHERE id=?")
        .run("Generated response", Date.now(), row.id);
    }, 20);

    const completed = await waitForAiRequest(db, row.id, { timeoutMs: 500, pollMs: 10 });
    assert.equal(completed.status, "done");
    assert.equal(completed.response, "Generated response");
  } finally {
    cleanup();
  }
});

test("ai request poll times out honestly", async () => {
  const { db, cleanup } = tempDb();
  try {
    const row = createAiRequest(db, { prompt: "Never completes" });
    await assert.rejects(
      waitForAiRequest(db, row.id, { timeoutMs: 30, pollMs: 10 }),
      /AI generation timed out after 30ms/,
    );
    assert.equal(getAiRequest(db, row.id)?.status, "pending");
  } finally {
    cleanup();
  }
});

test("stale pending ai requests are failed for retry visibility", () => {
  const { db, cleanup } = tempDb();
  try {
    const stale = createAiRequest(db, { prompt: "Old request" });
    const fresh = createAiRequest(db, { prompt: "Fresh request" });
    db.prepare("UPDATE ai_requests SET created_at=? WHERE id=?").run(1_000, stale.id);
    db.prepare("UPDATE ai_requests SET created_at=? WHERE id=?").run(10_000, fresh.id);

    const changed = failStaleAiRequests(db, { nowMs: 10_000, olderThanMs: 5_000 });

    assert.equal(changed, 1);
    assert.equal(getAiRequest(db, stale.id)?.status, "error");
    assert.match(getAiRequest(db, stale.id)?.error ?? "", /expired/);
    assert.equal(getAiRequest(db, fresh.id)?.status, "pending");
  } finally {
    cleanup();
  }
});

test("two concurrent ai request claimers produce exactly one winner", () => {
  const { db, file, cleanup } = tempDb();
  const claimerA = new DatabaseSync(file);
  const claimerB = new DatabaseSync(file);
  try {
    claimerA.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
    claimerB.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
    const row = createAiRequest(db, { prompt: "Claim once" });

    const results = [
      claimAiRequest(claimerA, row.id, { claimedBy: "engine-a", nowMs: 20_000 }),
      claimAiRequest(claimerB, row.id, { claimedBy: "engine-b", nowMs: 20_001 }),
    ];

    assert.equal(results.filter(Boolean).length, 1);
    const claimed = getAiRequest(db, row.id);
    assert.equal(claimed?.status, "processing");
    assert.match(claimed?.claimed_by ?? "", /^engine-[ab]$/);
  } finally {
    claimerA.close();
    claimerB.close();
    cleanup();
  }
});

test("stale processing ai request is rescued back to pending", () => {
  const { db, cleanup } = tempDb();
  try {
    const stale = createAiRequest(db, { prompt: "Owner died" });
    const fresh = createAiRequest(db, { prompt: "Still working" });
    assert.equal(claimAiRequest(db, stale.id, { claimedBy: "dead-owner", nowMs: 1_000 }), true);
    assert.equal(claimAiRequest(db, fresh.id, { claimedBy: "live-owner", nowMs: 9_000 }), true);

    const changed = failStaleAiRequests(db, { nowMs: 10_000, olderThanMs: 5_000 });

    assert.equal(changed, 1);
    assert.equal(getAiRequest(db, stale.id)?.status, "pending");
    assert.equal(getAiRequest(db, stale.id)?.claimed_at, null);
    assert.equal(getAiRequest(db, stale.id)?.claimed_by, null);
    assert.equal(getAiRequest(db, fresh.id)?.status, "processing");
  } finally {
    cleanup();
  }
});

test("stale sweeper leaves done ai requests untouched", () => {
  const { db, cleanup } = tempDb();
  try {
    const done = createAiRequest(db, { prompt: "Already done" });
    db.prepare(`
      UPDATE ai_requests
      SET status='done', response='ok', completed_at=?, claimed_at=?, claimed_by=?
      WHERE id=?;
    `).run(2_000, 1_000, "finished-owner", done.id);

    const changed = failStaleAiRequests(db, { nowMs: 10_000, olderThanMs: 5_000 });

    assert.equal(changed, 0);
    const row = getAiRequest(db, done.id);
    assert.equal(row?.status, "done");
    assert.equal(row?.response, "ok");
    assert.equal(row?.claimed_by, "finished-owner");
  } finally {
    cleanup();
  }
});

test("ai request schema migrates legacy status check before claiming", () => {
  const { db, cleanup } = tempDb({ initialize: false });
  try {
    db.exec(`
      CREATE TABLE ai_requests (
        id TEXT PRIMARY KEY,
        instructions TEXT NOT NULL DEFAULT '',
        prompt TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','done','error')),
        response TEXT,
        error TEXT,
        created_at INTEGER NOT NULL,
        completed_at INTEGER
      );
      INSERT INTO ai_requests(id, instructions, prompt, status, created_at)
      VALUES('legacy', '', 'old table', 'pending', 1);
    `);

    assert.equal(claimAiRequest(db, "legacy", { claimedBy: "migrator", nowMs: 2 }), true);
    assert.equal(getAiRequest(db, "legacy")?.status, "processing");
    assert.equal(getAiRequest(db, "legacy")?.claimed_by, "migrator");
  } finally {
    cleanup();
  }
});

function tempDb(options: { initialize?: boolean } = {}): { db: DatabaseSync; file: string; cleanup: () => void } {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dewx-ai-requests-"));
  const file = path.join(dir, "test.db");
  const db = new DatabaseSync(file);
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
  if (options.initialize ?? true) ensureAiRequestsSchema(db);
  return {
    db,
    file,
    cleanup: () => {
      db.close();
      fs.rmSync(dir, { recursive: true, force: true });
    },
  };
}
