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

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

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

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

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

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

  it("returns an empty bundle when no collections have custom.subject", async () => {
    const config = makeMockConfig([{ slug: "posts" }]);
    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    expect(bundle.subjectId).toBe("alice");
    expect(bundle.format).toBe("json");
    expect(bundle.data).toEqual({});
    expect(mockPayload.find).not.toHaveBeenCalled();
  });

  it("happy path — self role: includes exportable PII fields only", async () => {
    const config = makeMockConfig([
      {
        slug: "users",
        custom: {
          subject: { field: "id", kind: "self" },
          pii: {
            email: { exportable: true },
            name: { exportable: true },
            secret: { exportable: false },
          },
        },
      },
    ]);

    mockPayload.find.mockResolvedValue({
      docs: [{ id: "alice", email: "a@ex.com", name: "Alice", secret: "x" }],
    });

    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    expect(bundle.data["users"]?.asSelf).toHaveLength(1);
    const row = bundle.data["users"]!.asSelf![0]!;
    expect(row).toMatchObject({
      id: "alice",
      email: "a@ex.com",
      name: "Alice",
    });
    expect(row).not.toHaveProperty("secret");
    expect(bundle.data["users"]?.asReference).toBeUndefined();
  });

  it("happy path — owner role: includes exportable PII fields", async () => {
    const config = makeMockConfig([
      {
        slug: "orders",
        custom: {
          subject: { field: "userId", kind: "owner" },
          pii: { shippingAddress: { exportable: true } },
        },
      },
    ]);

    mockPayload.find.mockResolvedValue({
      docs: [{ id: "order-1", userId: "alice", shippingAddress: "1 Main St" }],
    });

    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    const row = bundle.data["orders"]?.asSelf?.[0];
    expect(row).toMatchObject({ id: "order-1", shippingAddress: "1 Main St" });
  });

  it("happy path — reference role: returns SubjectReference, not row content", async () => {
    const config = makeMockConfig([
      {
        slug: "comments",
        custom: {
          subject: { field: "mentionedUser", kind: "reference" },
        },
      },
    ]);

    mockPayload.find.mockResolvedValue({
      docs: [{ id: "comment-42", mentionedUser: "alice", body: "hello" }],
    });

    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    expect(bundle.data["comments"]?.asSelf).toBeUndefined();
    const refs = bundle.data["comments"]?.asReference;
    expect(refs).toHaveLength(1);
    expect(refs![0]).toEqual({
      rowId: "comment-42",
      linkedField: "mentionedUser",
      linkedThrough: "comments",
    });
    // Row content must NOT appear in the reference bucket
    expect(JSON.stringify(refs)).not.toContain("hello");
  });

  it("skips collections that return no matching rows", async () => {
    const config = makeMockConfig([
      {
        slug: "orders",
        custom: {
          subject: { field: "userId", kind: "owner" },
          pii: { total: { exportable: true } },
        },
      },
    ]);

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

    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    expect(bundle.data).toEqual({});
  });

  it("emits an EXPORT audit entry", async () => {
    const config = makeMockConfig([{ slug: "posts" }]);
    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    await exporter.exportSubjectData("alice", "json");

    expect(auditLog.recorded).toHaveLength(1);
    const entry = auditLog.recorded[0]!;
    expect(entry.action).toBe("EXPORT");
    expect(entry.actorId).toBe("alice");
    expect(entry.resource.type).toBe("subject-data");
    expect(entry.outcome).toBe("success");
  });

  it("attaches @context when format is json-ld", async () => {
    const config = makeMockConfig([{ slug: "posts" }]);
    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json-ld");

    expect(bundle["@context"]).toBeDefined();
    expect(typeof bundle["@context"]).toBe("object");
    const ctx = bundle["@context"] as Record<string, unknown>;
    expect(ctx["@vocab"]).toBe("https://schema.org/");
    expect(ctx["dsr"]).toBe("https://w3.org/ns/dpv#");
    expect(ctx["subjectId"]).toBe("identifier");
  });

  it("does NOT attach @context when format is json", async () => {
    const config = makeMockConfig([{ slug: "posts" }]);
    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    expect(bundle["@context"]).toBeUndefined();
  });

  it("multi-collection: walks all subject-linked collections", async () => {
    const config = makeMockConfig([
      {
        slug: "users",
        custom: {
          subject: { field: "id", kind: "self" },
          pii: { email: { exportable: true } },
        },
      },
      {
        slug: "orders",
        custom: {
          subject: { field: "userId", kind: "owner" },
          pii: { total: { exportable: true } },
        },
      },
      { slug: "posts" }, // no subject linkage — should be skipped
    ]);

    mockPayload.find
      .mockResolvedValueOnce({ docs: [{ id: "alice", email: "a@ex.com" }] })
      .mockResolvedValueOnce({
        docs: [{ id: "o-1", userId: "alice", total: 99 }],
      });

    const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
    const bundle = await exporter.exportSubjectData("alice", "json");

    expect(Object.keys(bundle.data)).toEqual(["users", "orders"]);
  });
});
