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 {
  baseQueuedAudit,
  baseQueuedNotification,
  baseReadAudit,
  baseSentNotification,
} from "../testing/fixtures";
import { run, type AnonymizeNotificationsForUserInput } from "./anonymizeNotificationsForUser";

describe("anonymizeNotificationsForUser", () => {
  const adminCtx: CommandContext = {
    actorId: "data-protection-admin",
    permissions: [],
  };

  const baseInput: AnonymizeNotificationsForUserInput = { userId: "user-1" };

  it("replaces recipientUserId with TOMBSTONED on every Notification where recipientUserId = userId", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [
        { ...baseQueuedNotification, recipientUserId: "TOMBSTONED", payloadVars: null },
        { ...baseSentNotification, recipientUserId: "TOMBSTONED", payloadVars: null },
      ],
      [],
    );

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

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

  it("drops payloadVars on every such Notification", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [{ ...baseQueuedNotification, recipientUserId: "TOMBSTONED", payloadVars: null }],
      [],
    );

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    // First .set() call targets Notification with payloadVars=null in SET.
    const setValues = mock.updates[0]?.updateValues() as
      | { recipientUserId?: string; payloadVars?: string | null }
      | undefined;
    expect(setValues?.recipientUserId).toBe("TOMBSTONED");
    expect(setValues?.payloadVars).toBeNull();
  });

  it("scrubs the rendered subject, body, and htmlBody on every such Notification", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [
        {
          ...baseQueuedNotification,
          recipientUserId: "TOMBSTONED",
          payloadVars: null,
          subject: "TOMBSTONED",
          body: "TOMBSTONED",
          htmlBody: null,
        },
      ],
      [],
    );

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    // Rendered content interpolates the same personal data as payloadVars,
    // so erasure must scrub it too — not just the input variables.
    const setValues = mock.updates[0]?.updateValues() as
      | { subject?: string; body?: string; htmlBody?: string | null }
      | undefined;
    expect(setValues?.subject).toBe("TOMBSTONED");
    expect(setValues?.body).toBe("TOMBSTONED");
    expect(setValues?.htmlBody).toBeNull();
  });

  it("replaces occurredBy with TOMBSTONED on every NotificationDeliveryAudit row where occurredBy = userId", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ ...baseReadAudit, occurredBy: "TOMBSTONED" }]);

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.auditRowsAnonymized).toBe(1);
    }
    const auditSet = mock.updates[1]?.updateValues() as { occurredBy?: string } | undefined;
    expect(auditSet?.occurredBy).toBe("TOMBSTONED");
  });

  it("preserves audit row id, notificationId, eventType, occurredAt, and errorClass", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ ...baseReadAudit, occurredBy: "TOMBSTONED" }]);

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    // The SET clause must not include id / notificationId / eventType /
    // occurredAt / errorClass — only occurredBy + updatedAt.
    const auditSet = mock.updates[1]?.updateValues() as Record<string, unknown> | undefined;
    expect(auditSet).toBeDefined();
    expect(auditSet).not.toHaveProperty("id");
    expect(auditSet).not.toHaveProperty("notificationId");
    expect(auditSet).not.toHaveProperty("eventType");
    expect(auditSet).not.toHaveProperty("occurredAt");
    expect(auditSet).not.toHaveProperty("errorClass");
  });

  it("does not delete any Notification or NotificationDeliveryAudit rows", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], []);

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    expect(mock.deletes.length).toBe(0);
  });

  it("is idempotent on an already-anonymized userId (re-runs as a no-op)", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    // No matching rows on either pass.
    mock.enqueueResults([], [], [], []);

    const first = await mock.withTx((trx) => run(trx, baseInput, adminCtx));
    const second = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(first.ok).toBe(true);
    expect(second.ok).toBe(true);
    if (second.ok) {
      expect(second.value.notificationsAnonymized).toBe(0);
      expect(second.value.auditRowsAnonymized).toBe(0);
    }
  });

  it("a subsequent BOUNCED webhook arriving for an anonymized Notification still writes a BOUNCED audit row and updates deliveryStatus", async () => {
    // This is a contract-level assertion. Anonymization scrubs recipient
    // identity and content (recipientUserId/subject/body tombstoned,
    // payloadVars/htmlBody cleared), but the Notification row itself is
    // preserved, so a subsequent recordDeliveryEvent(BOUNCED) call from the
    // EMAIL adapter still succeeds. This test documents that anonymization
    // does not block post-anonymization webhook ingestion.
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(
      [{ ...baseQueuedNotification, recipientUserId: "TOMBSTONED", payloadVars: null }],
      [],
    );

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      // Notification row is preserved (identity and content scrubbed, row
      // not deleted); the BOUNCED webhook path remains writeable.
      expect(result.value.notificationsAnonymized).toBe(1);
    }
  });

  it("preserves Notifications and audit rows tied to other users", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    // Only rows where recipientUserId / occurredBy match userId-1 are
    // returned. Other-user rows are out of scope of the WHERE clause.
    mock.enqueueResults(
      [{ ...baseQueuedNotification, recipientUserId: "TOMBSTONED", payloadVars: null }],
      [{ ...baseQueuedAudit }],
    );

    const result = await mock.withTx((trx) => run(trx, baseInput, adminCtx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      // Only userId-1's rows were touched.
      expect(result.value.notificationsAnonymized).toBe(1);
      expect(result.value.auditRowsAnonymized).toBe(1);
    }
  });
});
