import { describe, it, expect, vi, beforeEach } from "vitest";
import type { SanitizedConfig } from "payload";
import { PayloadDataRectify } from "@/payload-data-rectify";
import { RecordingAuditLog } from "@repo/core-testing/instrumentation";

type MockPayload = {
  find: ReturnType<typeof vi.fn>;
  update: ReturnType<typeof vi.fn>;
};

function makeMockPayload(): MockPayload {
  return {
    find: vi.fn(),
    update: vi.fn().mockResolvedValue({}),
  };
}

function makeMockConfig(
  collections: Array<{ slug: string; custom?: unknown }>,
): SanitizedConfig {
  return { collections } as unknown as SanitizedConfig;
}

describe("PayloadDataRectify", () => {
  let auditLog: RecordingAuditLog;
  let mockPayload: MockPayload;
  let mockGetPayload: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    auditLog = new RecordingAuditLog();
    mockPayload = makeMockPayload();
    mockGetPayload = vi.fn().mockResolvedValue(mockPayload);
  });

  it("happy path: updates a PII-tagged field and emits RESTRICT with art-16-request reason", async () => {
    const config = makeMockConfig([
      {
        slug: "users",
        custom: {
          subject: { field: "id", kind: "self" },
          pii: { name: { exportable: true } },
        },
      },
    ]);

    mockPayload.find.mockResolvedValue({
      docs: [{ id: "alice", name: "Alice Old" }],
    });

    const rectifier = new PayloadDataRectify(config, auditLog, mockGetPayload);
    await rectifier.updateSubjectField("alice", "users", "name", "Alice New");

    expect(mockPayload.update).toHaveBeenCalledWith({
      collection: "users",
      id: "alice",
      data: { name: "Alice New" },
      overrideAccess: true,
    });

    expect(auditLog.recorded).toHaveLength(1);
    const entry = auditLog.recorded[0]!;
    expect(entry.action).toBe("RESTRICT");
    expect(entry.reason).toBe("art-16-request");
    expect(entry.actorId).toBe("alice");
    expect(entry.changedFields).toContain("name");
  });

  it("updates all subject rows if multiple exist", async () => {
    const config = makeMockConfig([
      {
        slug: "profiles",
        custom: {
          subject: { field: "userId", kind: "owner" },
          pii: { bio: { exportable: true } },
        },
      },
    ]);

    mockPayload.find.mockResolvedValue({
      docs: [
        { id: "p-1", userId: "alice", bio: "old" },
        { id: "p-2", userId: "alice", bio: "old2" },
      ],
    });

    const rectifier = new PayloadDataRectify(config, auditLog, mockGetPayload);
    await rectifier.updateSubjectField("alice", "profiles", "bio", "new bio");

    expect(mockPayload.update).toHaveBeenCalledTimes(2);
  });

  it("throws when collection not found in config", async () => {
    const config = makeMockConfig([{ slug: "users" }]);
    const rectifier = new PayloadDataRectify(config, auditLog, mockGetPayload);

    await expect(
      rectifier.updateSubjectField("alice", "nonexistent", "field", "val"),
    ).rejects.toThrow(/not found/i);
  });

  it("throws when collection has no DSR subject linkage", async () => {
    const config = makeMockConfig([{ slug: "posts" }]);
    const rectifier = new PayloadDataRectify(config, auditLog, mockGetPayload);

    await expect(
      rectifier.updateSubjectField("alice", "posts", "title", "new"),
    ).rejects.toThrow(/no DSR subject linkage/i);
  });

  it("throws when field is not PII-tagged", async () => {
    const config = makeMockConfig([
      {
        slug: "users",
        custom: {
          subject: { field: "id", kind: "self" },
          pii: { email: { exportable: true } },
        },
      },
    ]);

    const rectifier = new PayloadDataRectify(config, auditLog, mockGetPayload);

    await expect(
      rectifier.updateSubjectField("alice", "users", "internalNote", "oops"),
    ).rejects.toThrow(/not tagged as PII/i);
  });

  it("emits audit entry with resource type matching the collection slug", async () => {
    const config = makeMockConfig([
      {
        slug: "users",
        custom: {
          subject: { field: "id", kind: "self" },
          pii: { name: { exportable: true } },
        },
      },
    ]);

    mockPayload.find.mockResolvedValue({ docs: [{ id: "alice" }] });

    const rectifier = new PayloadDataRectify(config, auditLog, mockGetPayload);
    await rectifier.updateSubjectField("alice", "users", "name", "New");

    expect(auditLog.recorded[0]!.resource.type).toBe("users");
  });
});
