import { describe, it, expect, vi } from "vitest";
import { PayloadConsent } from "@/payload-consent";
import { RecordingAuditLog } from "@repo/core-testing/instrumentation";

// Minimal Payload mock — only the methods PayloadConsent uses.
function makePayloadMock(initialConsentState: unknown[] = []) {
  const db: Record<string, unknown[]> = { user_1: initialConsentState };
  const findByID = vi.fn(async ({ id }: { id: string }) => ({
    id,
    consentState: db[id] ?? [],
  }));
  const update = vi.fn(
    async ({ id, data }: { id: string; data: Record<string, unknown> }) => {
      db[id] = data["consentState"] as unknown[];
      return { id };
    },
  );
  const getPayload = vi.fn(async () => ({ findByID, update }));
  return { getPayload, findByID, update, db };
}

async function makeConsent(opts?: {
  initial?: unknown[];
  auditLog?: RecordingAuditLog;
}) {
  const mock = makePayloadMock(opts?.initial);
  const auditLog = opts?.auditLog ?? new RecordingAuditLog();
  const consent = new PayloadConsent(
    "user_1",
    {} as never,
    auditLog,
    mock.getPayload,
  );
  await consent.load();
  return { consent, auditLog, ...mock };
}

describe("PayloadConsent.isGranted", () => {
  it("returns false for an unknown category before any grant", async () => {
    const { consent } = await makeConsent();
    expect(consent.isGranted("analytics")).toBe(false);
  });

  it("returns true after grant and false after withdraw", async () => {
    const { consent } = await makeConsent();
    await consent.grant("analytics");
    expect(consent.isGranted("analytics")).toBe(true);
    await consent.withdraw("analytics");
    expect(consent.isGranted("analytics")).toBe(false);
  });
});

describe("PayloadConsent.grant", () => {
  it("writes state to Payload via update", async () => {
    const { consent, update } = await makeConsent();
    await consent.grant("analytics");

    expect(update).toHaveBeenCalledOnce();
    const call = update.mock.calls[0]![0] as {
      collection: string;
      id: string;
      data: Record<string, unknown>;
      overrideAccess: boolean;
    };
    expect(call.collection).toBe("users");
    expect(call.id).toBe("user_1");
    expect(call.overrideAccess).toBe(true);
    const state = call.data["consentState"] as Array<Record<string, unknown>>;
    expect(state).toHaveLength(1);
    expect(state[0]!["category"]).toBe("analytics");
    expect(state[0]!["state"]).toBe("granted");
    expect(state[0]!["grantedAt"]).toBeTruthy();
  });

  it("emits a CONSENT_GRANT audit entry with correct shape", async () => {
    const { consent, auditLog } = await makeConsent();
    await consent.grant("functional");

    expect(auditLog.recorded).toHaveLength(1);
    const entry = auditLog.recorded[0]!;
    expect(entry.action).toBe("CONSENT_GRANT");
    expect(entry.actorId).toBe("user_1");
    expect(entry.actorType).toBe("user");
    expect(entry.resource).toEqual({ type: "consent", id: "functional" });
    expect(entry.outcome).toBe("success");
    expect(entry.containsPii).toBe(false);
  });

  it("preserves bannerVersion, policyVersion, method in state round-trip", async () => {
    const { consent: consent1, getPayload } = await makeConsent();
    await consent1.grant("marketing", {
      bannerVersion: "v2",
      policyVersion: "2026-01",
      method: "banner-accept",
    });

    // Simulate reload: new instance reads from Payload
    const consent2 = new PayloadConsent(
      "user_1",
      {} as never,
      new RecordingAuditLog(),
      getPayload,
    );
    await consent2.load();

    const categories = consent2.getCategories();
    expect(categories).toHaveLength(1);
    const marketing = categories[0]!;
    expect(marketing.category).toBe("marketing");
    expect(marketing.state).toBe("granted");
    expect(marketing.bannerVersion).toBe("v2");
    expect(marketing.policyVersion).toBe("2026-01");
    expect(marketing.method).toBe("banner-accept");
    expect(marketing.grantedAt).toBeInstanceOf(Date);
  });
});

describe("PayloadConsent.withdraw", () => {
  it("sets state to denied and persists to Payload", async () => {
    const { consent, update } = await makeConsent();
    await consent.grant("analytics");
    update.mockClear();
    await consent.withdraw("analytics");

    expect(update).toHaveBeenCalledOnce();
    const withdrawCall = update.mock.calls[0]![0] as {
      collection: string;
      id: string;
      data: Record<string, unknown>;
      overrideAccess: boolean;
    };
    const state = withdrawCall.data["consentState"] as Array<
      Record<string, unknown>
    >;
    expect(state[0]!["state"]).toBe("denied");
    expect(state[0]!["withdrawnAt"]).toBeTruthy();
  });

  it("emits a CONSENT_WITHDRAW audit entry", async () => {
    const { consent, auditLog } = await makeConsent();
    await consent.grant("analytics");
    auditLog.reset();
    await consent.withdraw("analytics");

    expect(auditLog.recorded).toHaveLength(1);
    expect(auditLog.recorded[0]!.action).toBe("CONSENT_WITHDRAW");
    expect(auditLog.recorded[0]!.resource.id).toBe("analytics");
  });
});

describe("PayloadConsent.getCategories", () => {
  it("returns all categories after multiple grants", async () => {
    const { consent } = await makeConsent();
    await consent.grant("necessary");
    await consent.grant("analytics");
    await consent.grant("marketing");

    const cats = consent.getCategories();
    expect(cats).toHaveLength(3);
    const granted = cats.filter((c) => c.state === "granted");
    expect(granted).toHaveLength(3);
  });

  it("reflects withdrawn state in category list", async () => {
    const { consent } = await makeConsent();
    await consent.grant("analytics");
    await consent.withdraw("analytics");

    const cats = consent.getCategories();
    const analytics = cats.find((c) => c.category === "analytics");
    expect(analytics?.state).toBe("denied");
    expect(analytics?.withdrawnAt).toBeInstanceOf(Date);
  });
});

describe("PayloadConsent.load", () => {
  it("hydrates cache from existing Payload state", async () => {
    const existingState = [
      {
        category: "functional",
        state: "granted",
        grantedAt: new Date("2026-01-15T10:00:00.000Z").toISOString(),
        bannerVersion: "v1",
        policyVersion: "2025-12",
        method: "banner-accept",
      },
    ];
    const { consent } = await makeConsent({ initial: existingState });

    expect(consent.isGranted("functional")).toBe(true);
    const cats = consent.getCategories();
    expect(cats[0]!.bannerVersion).toBe("v1");
    expect(cats[0]!.policyVersion).toBe("2025-12");
    expect(cats[0]!.method).toBe("banner-accept");
    expect(cats[0]!.grantedAt).toBeInstanceOf(Date);
  });

  it("handles missing consentState field gracefully (empty user doc)", async () => {
    const findByID = vi.fn(async () => ({ id: "user_1" }));
    const update = vi.fn(async () => ({}));
    const getPayload = vi.fn(async () => ({ findByID, update }));
    const consent = new PayloadConsent(
      "user_1",
      {} as never,
      new RecordingAuditLog(),
      getPayload,
    );
    await consent.load();
    expect(consent.getCategories()).toHaveLength(0);
  });
});

describe("PayloadConsent — before load", () => {
  it("isGranted returns false when load was not called", () => {
    const { getPayload } = makePayloadMock();
    const consent = new PayloadConsent(
      "user_1",
      {} as never,
      new RecordingAuditLog(),
      getPayload,
    );
    expect(consent.isGranted("analytics")).toBe(false);
  });
});

describe("PayloadConsent.load — deserializeEntry branches", () => {
  it("handles an entry with withdrawnAt set (covers withdrawnAt ternary branch)", async () => {
    const existingState = [
      {
        category: "analytics",
        state: "denied",
        grantedAt: new Date("2026-01-01T00:00:00.000Z").toISOString(),
        withdrawnAt: new Date("2026-01-15T00:00:00.000Z").toISOString(),
      },
    ];
    const { consent } = await makeConsent({ initial: existingState });

    expect(consent.isGranted("analytics")).toBe(false);
    const cats = consent.getCategories();
    expect(cats[0]!.withdrawnAt).toBeInstanceOf(Date);
    expect(cats[0]!.grantedAt).toBeInstanceOf(Date);
  });

  it("handles an entry with null grantedAt (withdraw before any grant)", async () => {
    const { consent: consent1, getPayload } = await makeConsent();
    // Withdraw without a prior grant — grantedAt stays undefined → persisted as null
    await consent1.withdraw("analytics");

    const consent2 = new PayloadConsent(
      "user_1",
      {} as never,
      new RecordingAuditLog(),
      getPayload,
    );
    await consent2.load();
    const cats = consent2.getCategories();
    expect(cats[0]!.state).toBe("denied");
    expect(cats[0]!.grantedAt).toBeUndefined();
    expect(cats[0]!.withdrawnAt).toBeInstanceOf(Date);
  });
});
