// @vitest-environment jsdom
// src/lib/offline/sync.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import "fake-indexeddb/auto";
import { enqueueAction } from "./queue";
import { syncPending, calculateBackoff, BACKOFF_SCHEDULE, MAX_ATTEMPTS } from "./sync";

beforeEach(async () => {
  const { getOfflineDB } = await import("./db");
  const db = await getOfflineDB();
  await db.clear("queue");
});

describe("offline sync — backoff", () => {
  it("BACKOFF_SCHEDULE returns 1s, 4s, 16s, 60s, 300s", () => {
    expect(BACKOFF_SCHEDULE).toEqual([1000, 4000, 16000, 60000, 300000]);
  });

  it("MAX_ATTEMPTS is 5", () => {
    expect(MAX_ATTEMPTS).toBe(5);
  });

  it("calculateBackoff(0) returns the first delay", () => {
    expect(calculateBackoff(0)).toBe(1000);
  });

  it("calculateBackoff(4) returns the last delay", () => {
    expect(calculateBackoff(4)).toBe(300000);
  });

  it("calculateBackoff(99) caps at MAX_ATTEMPTS (gives up)", () => {
    expect(calculateBackoff(99)).toBe(300000);
  });
});

describe("offline sync — replay", () => {
  it("syncPending posts each action and marks it synced on success", async () => {
    const a = await enqueueAction("markOrderReady", { orderId: "abc" });
    const dispatcher = vi.fn().mockResolvedValue({ ok: true });
    const result = await syncPending(dispatcher, { online: true, sleep: () => Promise.resolve() });
    expect(dispatcher).toHaveBeenCalledWith("markOrderReady", { orderId: "abc" }, a.id);
    expect(result.synced).toBe(1);
    expect(result.conflicts).toBe(0);
    expect(result.failed).toBe(0);
  });

  it("syncPending marks action as conflict when dispatcher returns conflict", async () => {
    await enqueueAction("markOrderReady", { orderId: "abc" });
    const dispatcher = vi.fn().mockResolvedValue({ ok: false, conflict: "stale" });
    const result = await syncPending(dispatcher, { online: true, sleep: () => Promise.resolve() });
    expect(result.conflicts).toBe(1);
    const { getQueuedActions } = await import("./queue");
    const all = await getQueuedActions();
    expect(all[0].status).toBe("conflict");
  });

  it("syncPending skips when offline", async () => {
    await enqueueAction("markOrderReady", { orderId: "abc" });
    const dispatcher = vi.fn();
    const result = await syncPending(dispatcher, { online: false, sleep: () => Promise.resolve() });
    expect(dispatcher).not.toHaveBeenCalled();
    expect(result.synced).toBe(0);
  });

  it("syncPending is a no-op for an empty queue and does not call the dispatcher", async () => {
    const dispatcher = vi.fn();
    const result = await syncPending(dispatcher, { online: true, sleep: () => Promise.resolve() });
    expect(dispatcher).not.toHaveBeenCalled();
    expect(result).toEqual({ synced: 0, conflicts: 0, failed: 0 });
  });

  it("syncPending moves an action to failed once max attempts is reached", async () => {
    // Seed an action that's already at the max-attempt threshold
    await enqueueAction("markOrderReady", { orderId: "abc" }, { id: "max-attempts-id" });
    const { getOfflineDB } = await import("./db");
    const db = await getOfflineDB();
    const tx = db.transaction("queue", "readwrite");
    const a = await tx.store.get("max-attempts-id");
    if (!a) throw new Error("seed not found");
    a.attempts = MAX_ATTEMPTS; // 5
    await tx.store.put(a);
    await tx.done;

    const dispatcher = vi.fn();
    const result = await syncPending(dispatcher, { online: true, sleep: () => Promise.resolve() });
    expect(dispatcher).not.toHaveBeenCalled();
    expect(result.failed).toBe(1);
    expect(result.synced).toBe(0);
    expect(result.conflicts).toBe(0);

    const { getQueuedActions } = await import("./queue");
    const all = await getQueuedActions();
    expect(all).toHaveLength(1);
    expect(all[0].status).toBe("failed");
    expect(all[0].error).toBe("max attempts reached");
  });

  it("syncPending skips actions whose backoff window has not yet elapsed", async () => {
    await enqueueAction("markOrderReady", { orderId: "abc" }, { id: "backoff-id" });
    // Pretend a previous attempt happened 100ms ago. With attempts=0, the
    // backoff schedule says we must wait 1000ms — so we should skip.
    const fixedNow = 1_700_000_000_000;
    const { getOfflineDB } = await import("./db");
    const db = await getOfflineDB();
    const tx = db.transaction("queue", "readwrite");
    const a = await tx.store.get("backoff-id");
    if (!a) throw new Error("seed not found");
    a.attempts = 0;
    a.lastAttemptAt = fixedNow - 100;
    await tx.store.put(a);
    await tx.done;

    const dispatcher = vi.fn();
    const result = await syncPending(dispatcher, {
      online: true,
      sleep: () => Promise.resolve(),
      now: () => fixedNow,
    });
    expect(dispatcher).not.toHaveBeenCalled();
    expect(result.synced).toBe(0);
    expect(result.conflicts).toBe(0);
    expect(result.failed).toBe(0);

    // Action is still pending (not advanced to in-flight or failed)
    const { getQueuedActions } = await import("./queue");
    const all = await getQueuedActions();
    expect(all).toHaveLength(1);
    expect(all[0].status).toBe("pending");
  });

  it("syncPending marks the action as failed when the dispatcher throws", async () => {
    const a = await enqueueAction("markOrderReady", { orderId: "abc" });
    const dispatcher = vi.fn().mockRejectedValue(new Error("network down"));
    const result = await syncPending(dispatcher, { online: true, sleep: () => Promise.resolve() });
    expect(dispatcher).toHaveBeenCalledWith("markOrderReady", { orderId: "abc" }, a.id);
    expect(result.failed).toBe(1);
    expect(result.synced).toBe(0);
    expect(result.conflicts).toBe(0);

    const { getQueuedActions } = await import("./queue");
    const all = await getQueuedActions();
    expect(all).toHaveLength(1);
    expect(all[0].status).toBe("failed");
    expect(all[0].error).toBe("network down");
  });

  it("syncPending leaves queued actions untouched when offline even if the queue is non-empty", async () => {
    await enqueueAction("markOrderReady", { orderId: "abc" });
    await enqueueAction("markOrderReady", { orderId: "def" });
    const dispatcher = vi.fn();
    const result = await syncPending(dispatcher, { online: false, sleep: () => Promise.resolve() });
    expect(dispatcher).not.toHaveBeenCalled();
    expect(result).toEqual({ synced: 0, conflicts: 0, failed: 0 });

    const { getQueuedActions } = await import("./queue");
    const all = await getQueuedActions();
    expect(all).toHaveLength(2);
    expect(all.every((x) => x.status === "pending")).toBe(true);
  });
});
