import { describe, it, expect, vi, beforeEach } from "vitest";

// Regression coverage for the "database is locked" fix (#228 follow-up).
//
// tauri-plugin-sql loads SQLite through a default multi-connection sqlx pool
// and exposes no transaction primitive, so a BEGIN and its COMMIT issued as
// separate db.execute calls could land on different pooled connections and
// strand an open write transaction (= permanent lock). db.ts funnels every
// operation through one FIFO lock, and withTransaction holds it for the whole
// BEGIN..COMMIT. These tests assert that serialization, without a real SQLite.

// Ordered log of every fake-db operation's start/end, shared with the fake db.
const events: string[] = [];
// Hoisted handle so the (hoisted) vi.mock factory can reach the per-test db.
const h = vi.hoisted(() => ({ fakeDb: null as unknown as FakeDb }));

interface FakeDb {
  execute: (q: string, p?: unknown[]) => Promise<{ rowsAffected: number }>;
  select: (q: string, p?: unknown[]) => Promise<unknown[]>;
  close: () => Promise<void>;
}

vi.mock("@tauri-apps/plugin-sql", () => ({
  default: { load: vi.fn(async () => h.fakeDb) },
}));
vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(async () => false) }));

import { connectToProfile, getDb, withTransaction } from "./db";

// Each op records start, yields a real macrotask (a window for interleaving if
// the lock were broken), then records end. The lock must keep them ordered.
function makeFakeDb(): FakeDb {
  return {
    execute: vi.fn(async (q: string) => {
      events.push(`start:${q}`);
      await new Promise((r) => setTimeout(r, 2));
      events.push(`end:${q}`);
      return { rowsAffected: 0 };
    }),
    select: vi.fn(async (q: string) => {
      events.push(`start:${q}`);
      await new Promise((r) => setTimeout(r, 2));
      events.push(`end:${q}`);
      return [];
    }),
    close: vi.fn(async () => {}),
  };
}

beforeEach(async () => {
  events.length = 0;
  h.fakeDb = makeFakeDb();
  await connectToProfile("test.db");
});

describe("db serialization (database-locked fix)", () => {
  it("serializes concurrent execute calls — no interleaving", async () => {
    const db = await getDb();
    // Fire both without awaiting between them: they race for the lock.
    await Promise.all([db.execute("A"), db.execute("B")]);
    // A must fully complete before B starts (FIFO, no overlap).
    expect(events).toEqual(["start:A", "end:A", "start:B", "end:B"]);
  });

  it("holds the lock across a whole withTransaction — an outside read cannot interleave", async () => {
    const db = await getDb();
    // Queue the transaction first, then an outside read.
    const txn = withTransaction(async (tx) => {
      await tx.execute("BEGIN");
      await tx.execute("WRITE");
      await tx.execute("COMMIT");
    });
    const outside = db.select("READ");
    await Promise.all([txn, outside]);
    // The outside read runs entirely AFTER the transaction's COMMIT — the
    // pre-fix bug was exactly an outside read interleaving mid-transaction.
    expect(events).toEqual([
      "start:BEGIN",
      "end:BEGIN",
      "start:WRITE",
      "end:WRITE",
      "start:COMMIT",
      "end:COMMIT",
      "start:READ",
      "end:READ",
    ]);
  });

  it("lets a nested getDb() call run directly inside a transaction — no deadlock", async () => {
    // A service called inside withTransaction might reach for getDb() (the
    // serialized proxy) instead of the threaded handle. The reentrancy guard
    // must run it directly rather than queue it behind the held lock (deadlock).
    const result = await withTransaction(async () => {
      const inner = await getDb();
      await inner.execute("NESTED");
      return "ok";
    });
    expect(result).toBe("ok");
    expect(events).toEqual(["start:NESTED", "end:NESTED"]);
  });

  it("releases the lock after a failed transaction so later ops still run", async () => {
    const db = await getDb();
    await expect(
      withTransaction(async (tx) => {
        await tx.execute("BEGIN");
        throw new Error("boom");
      })
    ).rejects.toThrow("boom");
    // The lock must be free again: a subsequent op completes (no stuck queue).
    await db.execute("AFTER");
    expect(events).toContain("start:AFTER");
    expect(events).toContain("end:AFTER");
  });

  it("withTransaction rejects when no profile is connected", async () => {
    const { closeDb } = await import("./db");
    await closeDb();
    await expect(withTransaction(async () => undefined)).rejects.toThrow(
      /No database connection/
    );
  });
});
