import type { CommandContext } 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,
  baseFailedNotification,
  baseQueuedNotification,
  baseReadNotification,
  baseSeenNotification,
  baseSentNotification,
} from "../testing/fixtures";
import { run } from "./markAllNotificationsAsRead";

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

describe("markAllNotificationsAsRead", () => {
  it("transitions every caller-owned Notification where deliveryStatus IN SENT, DELIVERED AND READ not in engagementStatuses AND ARCHIVED not in engagementStatuses", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([
      { ...baseSentNotification, id: "n-a" },
      { ...baseDeliveredNotification, id: "n-b" },
      { ...baseSeenNotification, id: "n-c" },
    ]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

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

  it("adds SEEN alongside READ when SEEN was not previously present", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseSentNotification, id: "n-a", engagementStatuses: [] }]);

    await mock.withTx((trx) => run(trx, {}, ctx));

    const setCall = mock.updates[0]?.updateValues();
    expect(setCall.engagementStatuses).toEqual(["SEEN", "READ"]);
  });

  it("stamps readAt on each transitioned row", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseSentNotification, id: "n-a" }]);

    await mock.withTx((trx) => run(trx, {}, ctx));

    const setCall = mock.updates[0]?.updateValues();
    expect(setCall.readAt).toBeInstanceOf(Date);
  });

  it("does not modify rows already containing READ", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseReadNotification }]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(0);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("does not modify rows already containing ARCHIVED", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseArchivedNotification }]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(0);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("does not modify rows where deliveryStatus is QUEUED, FAILED, or BOUNCED", async () => {
    // The selection filter narrows on deliveryStatus IN {SENT, DELIVERED}; a
    // mock that respects that filter returns no operator-owned rows.
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(0);
    }
    expect(mock.updates.length).toBe(0);
    void baseQueuedNotification;
    void baseFailedNotification;
  });

  it("scopes the candidate set to the IN_APP channel; EMAIL rows are never stamped READ", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    // The IN_APP join excludes rows dispatched on other channels (e.g. EMAIL),
    // so the candidate set is empty when the user only has EMAIL rows.
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(0);
    }
    expect(mock.updates.length).toBe(0);
    // The SELECT carries the same IN_APP channel join as listInboxNotifications.
    expect(mock.selects[0]?.sql).toContain("NotificationChannel");
    expect(mock.selects[0]?.parameters).toContain("IN_APP");
  });

  it("does not modify rows owned by other users (recipient-scoped)", async () => {
    // The selection filter scopes by recipientUserId == caller.userId.
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(0);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("returns transitionedCount=0 and persists no changes when no rows match (idempotent)", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(0);
    }
    expect(mock.updates.length).toBe(0);
    expect(mock.inserts.length).toBe(0);
  });

  it("writes one NotificationDeliveryAudit row per transitioned Notification", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([
      { ...baseSentNotification, id: "n-a" },
      { ...baseDeliveredNotification, id: "n-b" },
    ]);

    await mock.withTx((trx) => run(trx, {}, ctx));

    expect(mock.inserts.length).toBe(2);
    const auditValues = mock.inserts[0]?.insertValues();
    expect(auditValues.eventType).toBe("READ");
    expect(auditValues.occurredBy).toBe("user-1");
  });

  it("causes unreadCount returned by the inbox feed to become zero on the next query", async () => {
    // Behavior verified by listInboxNotifications: every caller-owned row that
    // satisfied the unread predicate now contains READ, so unreadCount is zero.
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([
      { ...baseSentNotification, id: "n-a" },
      { ...baseDeliveredNotification, id: "n-b" },
    ]);

    const result = await mock.withTx((trx) => run(trx, {}, ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.transitionedCount).toBe(2);
    }
  });
});
