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 { ForbiddenError } from "../lib/errors.generated";
import {
  baseFailedAudit,
  baseQueuedAudit,
  baseReadAudit,
  baseSentAudit,
} from "../testing/fixtures";
import { run } from "./searchNotificationDeliveryAudit";

describe("searchNotificationDeliveryAudit", () => {
  const userCtx: QueryContext = {
    actorId: "user-1",
    permissions: [],
  };

  // The caller-owned parent notification probe result, returned before the
  // audit rows on every self-scoped search.
  const ownedParent = [{ id: "notification-1" }];

  it("returns rows matching the supplied eventType filter", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(ownedParent, [{ ...baseSentAudit }]);

    const result = await run(mock.db, { eventType: "SENT" }, userCtx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items).toHaveLength(1);
      expect(result.value.items[0]?.eventType).toBe("SENT");
    }
  });

  it("returns rows whose occurredAt falls within the supplied dateRange", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(ownedParent, [{ ...baseQueuedAudit }, { ...baseSentAudit }]);

    const result = await run(
      mock.db,
      {
        dateRange: {
          from: "2024-01-01T00:00:00.000Z",
          to: "2024-12-31T23:59:59.000Z",
        },
      },
      userCtx,
    );

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

  it("returns rows for the supplied recipientUserId when it matches the caller", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ id: "notification-1" }], [{ ...baseReadAudit, occurredBy: "user-1" }]);

    const result = await run(mock.db, { recipientUserId: "user-1" }, userCtx);

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

  it("returns rows whose parent Notification matches the supplied sourceType + sourceId", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ id: "notification-1" }], [{ ...baseQueuedAudit }]);

    const result = await run(mock.db, { sourceType: "PURCHASE_ORDER", sourceId: "po-1" }, userCtx);

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

  it("combines multiple filters with AND logic", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ id: "notification-1" }], [{ ...baseSentAudit }]);

    const result = await run(
      mock.db,
      {
        eventType: "SENT",
        sourceType: "PURCHASE_ORDER",
        sourceId: "po-1",
        dateRange: { from: "2024-01-01T00:00:00.000Z" },
      },
      userCtx,
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items).toHaveLength(1);
      expect(result.value.items[0]?.eventType).toBe("SENT");
    }
  });

  it("returns rows ordered by occurredAt descending", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(ownedParent, [{ ...baseSentAudit }, { ...baseQueuedAudit }]);

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

    expect(result.ok).toBe(true);
    if (result.ok) {
      // Default ordering is occurredAt desc; the mock returns rows as-is.
      expect(result.value.items).toHaveLength(2);
    }
  });

  it("rejects a caller searching for another user's rows with FORBIDDEN", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();

    const result = await run(mock.db, { recipientUserId: "user-other" }, userCtx);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(ForbiddenError);
    }
  });

  it("scopes searches without recipientUserId to the caller's own rows", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ id: "notification-1" }], [{ ...baseReadAudit, occurredBy: "user-1" }]);

    const result = await run(mock.db, { eventType: "READ" }, userCtx);

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

  it("returns an empty list when no rows match the filters", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([]);

    const result = await run(mock.db, { eventType: "BOUNCED" }, userCtx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.items).toEqual([]);
    }
  });

  it("returns anonymized rows with structural fields preserved and freeform fields cleared", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(ownedParent, [
      {
        ...baseFailedAudit,
        occurredBy: "TOMBSTONED",
        errorDetail: null,
      },
    ]);

    const result = await run(mock.db, { eventType: "FAILED" }, userCtx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      const row = result.value.items[0];
      expect(row?.occurredBy).toBe("TOMBSTONED");
      expect(row?.errorDetail).toBeNull();
      expect(row?.id).toBeDefined();
      expect(row?.notificationId).toBeDefined();
      expect(row?.errorClass).toBe("AdapterTransportError");
    }
  });

  it("returns a non-anonymized row's freeform fields intact (the query never redacts at read time)", async () => {
    // Anonymization is applied at-rest by anonymizeNotificationsForUser / the
    // retention sweep, NOT by this read query. This row has NOT been swept, so
    // its freeform fields must come back verbatim — proving the query faithfully
    // returns stored values rather than blanket-clearing (or fabricating) them.
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults(ownedParent, [
      {
        ...baseFailedAudit,
        occurredBy: "user-7",
        errorDetail: "chat.postMessage failed: channel_not_found",
      },
    ]);

    const result = await run(mock.db, { eventType: "FAILED" }, userCtx);

    expect(result.ok).toBe(true);
    if (result.ok) {
      const row = result.value.items[0];
      expect(row?.occurredBy).toBe("user-7");
      expect(row?.errorDetail).toBe("chat.postMessage failed: channel_not_found");
    }
  });
});
