import type { QueryContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import {
  baseArchivedNotification,
  baseDeliveredNotification,
  baseSentNotification,
} from "../testing/fixtures";
import { run } from "./listInboxNotifications";

const ctx: QueryContext = { actorId: "user-1", permissions: [] };

describe("listInboxNotifications", () => {
  it("returns the caller's feed rows", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [
        { ...baseSentNotification, id: "n-a", createdAt: new Date("2024-03-01") },
        { ...baseDeliveredNotification, id: "n-b", createdAt: new Date("2024-02-01") },
      ],
      [{ count: 0 }],
    );

    const result = await run(mock.db, {}, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items).toHaveLength(2);
      expect(result.value.items[0]?.id).toBe("n-a");
    }
  });

  it("scopes the feed to the caller, IN_APP, deliverable statuses, ordered by createdAt desc", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseSentNotification }], [{ count: 0 }]);

    await run(mock.db, {}, ctx);

    const feed = mock.selects[0];
    expect(feed.parameters).toContain(ctx.actorId);
    expect(feed.parameters).toContain("IN_APP");
    expect(feed.parameters).toContain("SENT");
    expect(feed.parameters).toContain("DELIVERED");
    expect(feed.sql).toMatch(/order by .*"createdAt" desc/i);
  });

  it("excludes archived rows from the feed via archivedAt IS NULL by default", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseSentNotification }], [{ count: 0 }]);

    await run(mock.db, {}, ctx);

    expect(mock.selects[0].sql).toMatch(/"archivedAt" is null/i);
  });

  it("does not filter archivedAt on the feed when includeArchived = true", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [
        { ...baseSentNotification, id: "n-a" },
        { ...baseArchivedNotification, id: "n-arch" },
      ],
      [{ count: 0 }],
    );

    const result = await run(mock.db, { includeArchived: true }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items.map((i) => i.id)).toEqual(["n-a", "n-arch"]);
    }
    expect(mock.selects[0].sql).not.toMatch(/"archivedAt" is null/i);
  });

  it("applies limit + 1 and offset to the feed query", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [
        { ...baseSentNotification, id: "n-1" },
        { ...baseDeliveredNotification, id: "n-2" },
      ],
      [{ count: 0 }],
    );

    const result = await run(mock.db, { limit: 1, offset: 5 }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items).toHaveLength(1);
      expect(result.value.hasNextPage).toBe(true);
    }
    expect(mock.selects[0].sql).toMatch(/limit/i);
    expect(mock.selects[0].sql).toMatch(/offset/i);
  });

  it("returns unreadCount from the count query", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ count: 3 }]);

    const result = await run(mock.db, {}, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.unreadCount).toBe(3);
    }
  });

  it("counts unread over IN_APP, deliverable, unread, non-archived rows", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ count: 0 }]);

    await run(mock.db, {}, ctx);

    const count = mock.selects[1];
    expect(count.parameters).toContain("IN_APP");
    expect(count.parameters).toContain(ctx.actorId);
    expect(count.sql).toMatch(/"readAt" is null/i);
    expect(count.sql).toMatch(/"archivedAt" is null/i);
  });

  it("keeps the unread filters even when includeArchived = true", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ count: 1 }]);

    const result = await run(mock.db, { includeArchived: true }, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.unreadCount).toBe(1);
    }
    expect(mock.selects[1].sql).toMatch(/"archivedAt" is null/i);
    expect(mock.selects[1].sql).toMatch(/"readAt" is null/i);
  });

  it("issues exactly one feed query and one count query, both IN_APP-scoped", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseSentNotification }], [{ count: 0 }]);

    await run(mock.db, {}, ctx);

    const inAppSelects = mock.selects.filter((s) => s.parameters.includes("IN_APP"));
    expect(inAppSelects).toHaveLength(2);
  });

  it("returns an empty list with unreadCount = 0 when the caller has no qualifying rows", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], []);

    const result = await run(mock.db, {}, ctx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items).toHaveLength(0);
      expect(result.value.unreadCount).toBe(0);
    }
  });

  it("does not mutate any row", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseSentNotification }], [{ count: 0 }]);

    await run(mock.db, {}, ctx);

    expect(mock.updates).toHaveLength(0);
  });
});
