import { describe, expect, it } from "vitest";
import { paginate, paginateCursor } from "../db/pagination.js";

// ---------------------------------------------------------------------------
// Test data
// ---------------------------------------------------------------------------

function makeItems(n: number) {
  return Array.from({ length: n }, (_, i) => ({
    id: `id_${String(i + 1).padStart(3, "0")}`,
    name: `Item ${i + 1}`,
  }));
}

const ALL_ITEMS = makeItems(55);

function mockFindMany(items: typeof ALL_ITEMS) {
  return async ({ skip, take }: { skip: number; take: number }) =>
    items.slice(skip, skip + take);
}

function mockCount(items: typeof ALL_ITEMS) {
  return async () => items.length;
}

// ---------------------------------------------------------------------------
// paginate()
// ---------------------------------------------------------------------------

describe("paginate", () => {
  it("returns first page with correct metadata", async () => {
    const result = await paginate(
      mockFindMany(ALL_ITEMS),
      mockCount(ALL_ITEMS),
      { page: 1, limit: 20 },
    );

    expect(result.items).toHaveLength(20);
    expect(result.total).toBe(55);
    expect(result.page).toBe(1);
    expect(result.limit).toBe(20);
    expect(result.nextCursor).toBe("id_020");
  });

  it("returns last page without nextCursor", async () => {
    const result = await paginate(
      mockFindMany(ALL_ITEMS),
      mockCount(ALL_ITEMS),
      { page: 3, limit: 20 },
    );

    expect(result.items).toHaveLength(15);
    expect(result.total).toBe(55);
    expect(result.nextCursor).toBeNull();
  });

  it("skips zero when cursor is provided", async () => {
    let capturedSkip = -1;
    const result = await paginate(
      async ({ skip, take }) => {
        capturedSkip = skip;
        return ALL_ITEMS.slice(skip, skip + take);
      },
      mockCount(ALL_ITEMS),
      { page: 3, limit: 10, cursor: "irrelevant" },
    );

    expect(capturedSkip).toBe(0);
    // When cursor is given skip=0, so we get from the start of the pre-filtered data
    expect(result.items).toHaveLength(10);
  });

  it("uses defaults when page and limit omitted", async () => {
    const result = await paginate(
      mockFindMany(ALL_ITEMS),
      mockCount(ALL_ITEMS),
      {},
    );

    expect(result.page).toBe(1);
    expect(result.limit).toBe(50);
    expect(result.items).toHaveLength(50);
    expect(result.nextCursor).toBe("id_050");
  });

  it("handles empty result set", async () => {
    const result = await paginate(
      mockFindMany([]),
      mockCount([]),
      { page: 1, limit: 20 },
    );

    expect(result.items).toHaveLength(0);
    expect(result.total).toBe(0);
    expect(result.nextCursor).toBeNull();
  });

  it("returns no nextCursor when items exactly equal limit", async () => {
    const items = makeItems(20);
    const result = await paginate(
      mockFindMany(items),
      mockCount(items),
      { page: 1, limit: 20 },
    );

    expect(result.items).toHaveLength(20);
    expect(result.nextCursor).toBeNull();
  });

  it("supports custom getId", async () => {
    const items = [{ uuid: "abc" }, { uuid: "def" }, { uuid: "ghi" }];
    const result = await paginate(
      async ({ skip, take }) => items.slice(skip, skip + take),
      async () => items.length,
      { page: 1, limit: 2 },
      (item) => item.uuid,
    );

    expect(result.nextCursor).toBe("def");
    expect(result.items).toHaveLength(2);
  });
});

// ---------------------------------------------------------------------------
// paginateCursor()
// ---------------------------------------------------------------------------

describe("paginateCursor", () => {
  it("returns items and nextCursor when more exist", async () => {
    const result = await paginateCursor(
      async ({ take }) => ALL_ITEMS.slice(0, take),
      { limit: 10 },
    );

    expect(result.items).toHaveLength(10);
    expect(result.nextCursor).toBe("id_010");
  });

  it("returns null cursor when no more items", async () => {
    const items = makeItems(5);
    const result = await paginateCursor(
      async ({ take }) => items.slice(0, take),
      { limit: 10 },
    );

    expect(result.items).toHaveLength(5);
    expect(result.nextCursor).toBeNull();
  });

  it("handles empty result", async () => {
    const result = await paginateCursor(
      async () => [],
      { limit: 10 },
    );

    expect(result.items).toHaveLength(0);
    expect(result.nextCursor).toBeNull();
  });

  it("uses default limit", async () => {
    const result = await paginateCursor(
      async ({ take }) => ALL_ITEMS.slice(0, take),
      {},
    );

    expect(result.items).toHaveLength(50);
    expect(result.nextCursor).toBe("id_050");
  });
});
