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,
  baseSeenNotification,
  baseSentNotification,
} from "../testing/fixtures";
import { run } from "./markNotificationsAsSeen";

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

describe("markNotificationsAsSeen", () => {
  it("adds SEEN to every caller-owned IN_APP Notification where deliveryStatus IN SENT, DELIVERED AND SEEN 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" },
    ]);

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

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

  it("does not modify rows dispatched on non-IN_APP channels (e.g. EMAIL rows terminal at SENT are never stamped SEEN)", 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("stamps seenAt 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.seenAt).toBeInstanceOf(Date);
  });

  it("does not add READ and does not affect unreadCount", 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();
    const nextStatuses = setCall.engagementStatuses as string[];
    expect(nextStatuses).toContain("SEEN");
    expect(nextStatuses).not.toContain("READ");
  });

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

    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 containing ARCHIVED", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ ...baseArchivedNotification, engagementStatuses: ["ARCHIVED"] }]);

    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 excludes operator-owned deliveryStatus rows; a mock
    // that narrows to SENT/DELIVERED returns zero candidates here.
    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("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 on second invocation when no new rows have arrived", 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("SEEN");
    expect(auditValues.occurredBy).toBe("user-1");
  });
});
