import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { SanitizedConfig } from "payload";
import type { IJobQueue } from "@/jobs/job-queue.interface";
import type { AuditLogProtocol } from "@/di/bind-protocols";
import {
  parseDurationMs,
  scheduleDelayMs,
  buildPurgeHandler,
  registerRetentionPurgeJobs,
  type PayloadPurgeApi,
  type RetentionPurgeJobDeps,
} from "./retention-purge.job";

// ---- test helpers ----

type MockCollection = {
  slug: string;
  custom?: { retention?: Record<string, unknown> };
  fields?: Array<{ name?: string; custom?: { pii?: unknown } }>;
};

function makeConfig(collections: MockCollection[]): SanitizedConfig {
  return { collections } as unknown as SanitizedConfig;
}

function makeQueue() {
  const enqueue = vi.fn().mockResolvedValue({ jobId: "job-1" });
  const queue = { enqueue } as unknown as IJobQueue;
  return { queue, enqueue };
}

function makePayloadApi(
  docs: Array<Record<string, unknown>> = [],
): PayloadPurgeApi {
  return {
    find: vi.fn().mockResolvedValue({ docs }),
    update: vi.fn().mockResolvedValue({}),
    delete: vi.fn().mockResolvedValue({}),
  };
}

function makeAuditLog(): {
  auditLog: AuditLogProtocol;
  record: ReturnType<typeof vi.fn>;
} {
  const record = vi.fn().mockResolvedValue(undefined);
  return { auditLog: { record } as AuditLogProtocol, record };
}

// ---- parseDurationMs ----

describe("parseDurationMs", () => {
  it("parses years: P2Y → 2 × 365 days", () => {
    expect(parseDurationMs("P2Y")).toBe(2 * 365 * 86_400_000);
  });

  it("parses months: P1M → 30 days", () => {
    expect(parseDurationMs("P1M")).toBe(30 * 86_400_000);
  });

  it("parses weeks: P1W → 7 days", () => {
    expect(parseDurationMs("P1W")).toBe(7 * 86_400_000);
  });

  it("parses days: P30D → 30 days", () => {
    expect(parseDurationMs("P30D")).toBe(30 * 86_400_000);
  });

  it("combines components: P1Y2M3D", () => {
    expect(parseDurationMs("P1Y2M3D")).toBe(
      365 * 86_400_000 + 60 * 86_400_000 + 3 * 86_400_000,
    );
  });

  it("returns 0 for P0D", () => {
    expect(parseDurationMs("P0D")).toBe(0);
  });

  it("returns 0 for unrecognised strings", () => {
    expect(parseDurationMs("invalid")).toBe(0);
    expect(parseDurationMs("")).toBe(0);
    expect(parseDurationMs("PT2H")).toBe(0);
  });
});

// ---- scheduleDelayMs ----

describe("scheduleDelayMs", () => {
  it("returns 1 day for 'daily'", () => {
    expect(scheduleDelayMs("daily")).toBe(86_400_000);
  });

  it("returns 7 days for 'weekly'", () => {
    expect(scheduleDelayMs("weekly")).toBe(7 * 86_400_000);
  });

  it("returns 30 days for 'monthly'", () => {
    expect(scheduleDelayMs("monthly")).toBe(30 * 86_400_000);
  });

  it("falls back to 1 day for cron-style strings", () => {
    expect(scheduleDelayMs("0 3 * * 0")).toBe(86_400_000);
  });
});

// ---- registerRetentionPurgeJobs ----

describe("registerRetentionPurgeJobs", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("enqueues one job per collection with a purgeSchedule", async () => {
    const { queue, enqueue } = makeQueue();
    const config = makeConfig([
      { slug: "users", custom: { retention: { purgeSchedule: "daily" } } },
      { slug: "articles", custom: { retention: { purgeSchedule: "weekly" } } },
    ]);
    await registerRetentionPurgeJobs({ queue, config, getPayload: vi.fn() });
    expect(enqueue).toHaveBeenCalledTimes(2);
  });

  it("skips collections without a retention config", async () => {
    const { queue, enqueue } = makeQueue();
    const config = makeConfig([{ slug: "media" }]);
    await registerRetentionPurgeJobs({ queue, config, getPayload: vi.fn() });
    expect(enqueue).not.toHaveBeenCalled();
  });

  it("uses the correct taskSlug and runAt for each schedule type", async () => {
    const { queue, enqueue } = makeQueue();
    const config = makeConfig([
      { slug: "users", custom: { retention: { purgeSchedule: "weekly" } } },
    ]);
    await registerRetentionPurgeJobs({ queue, config, getPayload: vi.fn() });
    expect(enqueue).toHaveBeenCalledWith(
      "retention-purge--users",
      {},
      { runAt: new Date("2026-01-08T00:00:00.000Z") },
    );
  });

  it("schedules daily purge 1 day from now", async () => {
    const { queue, enqueue } = makeQueue();
    const config = makeConfig([
      { slug: "sessions", custom: { retention: { purgeSchedule: "daily" } } },
    ]);
    await registerRetentionPurgeJobs({ queue, config, getPayload: vi.fn() });
    expect(enqueue).toHaveBeenCalledWith(
      "retention-purge--sessions",
      {},
      { runAt: new Date("2026-01-02T00:00:00.000Z") },
    );
  });
});

// ---- buildPurgeHandler — input validation ----

describe("buildPurgeHandler — input validation", () => {
  it("throws when the collection slug is not found in the config", () => {
    const { queue } = makeQueue();
    const config = makeConfig([]);
    expect(() =>
      buildPurgeHandler("missing", { queue, config, getPayload: vi.fn() }),
    ).toThrow("collection not found: missing");
  });

  it("throws when the collection has no retention config", () => {
    const { queue } = makeQueue();
    const config = makeConfig([{ slug: "media" }]);
    expect(() =>
      buildPurgeHandler("media", { queue, config, getPayload: vi.fn() }),
    ).toThrow("no retention config on collection: media");
  });
});

// ---- buildPurgeHandler — hard-delete branch ----

describe("buildPurgeHandler — hard-delete", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("queries by createdAt for from-creation trigger", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([]);
    const config = makeConfig([
      {
        slug: "users",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P2Y", trigger: "from-creation" },
            postDeletion: {
              action: "hard-delete",
              duration: "P30D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("users", deps)();

    expect(payload.find).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          createdAt: {
            less_than: new Date(
              Date.now() - parseDurationMs("P2Y"),
            ).toISOString(),
          },
        },
      }),
    );
  });

  it("queries by updatedAt for from-last-access trigger", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([]);
    const config = makeConfig([
      {
        slug: "sessions",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P30D", trigger: "from-last-access" },
            postDeletion: {
              action: "hard-delete",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("sessions", deps)();

    expect(payload.find).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { updatedAt: { less_than: expect.any(String) } },
      }),
    );
    expect(payload.find).not.toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({ createdAt: expect.anything() }),
      }),
    );
  });

  it("deletes each returned row", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "row-1" }, { id: "row-2" }]);
    const config = makeConfig([
      {
        slug: "users",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "hard-delete",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("users", deps)();

    expect(payload.delete).toHaveBeenCalledTimes(2);
    expect(payload.delete).toHaveBeenCalledWith({
      collection: "users",
      id: "row-1",
      overrideAccess: true,
    });
    expect(payload.delete).toHaveBeenCalledWith({
      collection: "users",
      id: "row-2",
      overrideAccess: true,
    });
    expect(payload.update).not.toHaveBeenCalled();
  });

  it("re-enqueues itself for the next purge cycle", async () => {
    const { queue, enqueue } = makeQueue();
    const payload = makePayloadApi([]);
    const config = makeConfig([
      {
        slug: "users",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "hard-delete",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("users", deps)();

    expect(enqueue).toHaveBeenCalledOnce();
    expect(enqueue).toHaveBeenCalledWith(
      "retention-purge--users",
      {},
      { runAt: new Date("2026-01-02T00:00:00.000Z") },
    );
  });

  it("defaults to hard-delete when postDeletion is not declared", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "row-x" }]);
    const config = makeConfig([
      {
        slug: "logs",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("logs", deps)();

    expect(payload.delete).toHaveBeenCalledWith({
      collection: "logs",
      id: "row-x",
      overrideAccess: true,
    });
    expect(payload.update).not.toHaveBeenCalled();
  });
});

// ---- buildPurgeHandler — pseudonymize branch ----

describe("buildPurgeHandler — pseudonymize", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("nulls only PII-annotated fields for each matched row", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "row-2" }]);
    const config = makeConfig([
      {
        slug: "contacts",
        custom: {
          retention: {
            purgeSchedule: "monthly",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "pseudonymize",
              duration: "P30D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [
          { name: "email", custom: { pii: { category: "contact-email" } } },
          { name: "phone", custom: { pii: { category: "contact-phone" } } },
          { name: "status" }, // no pii — must NOT be nulled
        ],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("contacts", deps)();

    expect(payload.update).toHaveBeenCalledWith({
      collection: "contacts",
      id: "row-2",
      data: { email: null, phone: null },
      overrideAccess: true,
    });
    expect(payload.delete).not.toHaveBeenCalled();
  });

  it("skips update when no PII fields are declared on the collection", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "row-3" }]);
    const config = makeConfig([
      {
        slug: "tags",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "pseudonymize",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [
          { name: "label" }, // no pii
        ],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await buildPurgeHandler("tags", deps)();

    expect(payload.update).not.toHaveBeenCalled();
    expect(payload.delete).not.toHaveBeenCalled();
  });
});

// ---- buildPurgeHandler — audit emission ----

describe("buildPurgeHandler — audit emission", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("emits one audit record per processed row", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "a" }, { id: "b" }]);
    const { auditLog, record } = makeAuditLog();
    const config = makeConfig([
      {
        slug: "users",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "hard-delete",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
      auditLog,
    };
    await buildPurgeHandler("users", deps)();

    expect(record).toHaveBeenCalledTimes(2);
    expect(record).toHaveBeenCalledWith(
      expect.objectContaining({
        actorId: "system",
        actorType: "system",
        action: "DELETE",
        reason: "retention-policy",
        outcome: "success",
        from: { ipTruncated: "system", userAgent: "background-job" },
      }),
    );
  });

  it("includes resource type and id in the audit entry", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "row-42" }]);
    const { auditLog, record } = makeAuditLog();
    const config = makeConfig([
      {
        slug: "orders",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "hard-delete",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
      auditLog,
    };
    await buildPurgeHandler("orders", deps)();

    expect(record).toHaveBeenCalledWith(
      expect.objectContaining({
        resource: { type: "orders", id: "row-42" },
      }),
    );
  });

  it("gracefully skips audit emission when auditLog is not provided", async () => {
    const { queue } = makeQueue();
    const payload = makePayloadApi([{ id: "x" }]);
    const config = makeConfig([
      {
        slug: "users",
        custom: {
          retention: {
            purgeSchedule: "daily",
            activeRetention: { duration: "P1Y", trigger: "from-creation" },
            postDeletion: {
              action: "hard-delete",
              duration: "P0D",
              trigger: "after-deletion",
            },
          },
        },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
    };
    await expect(buildPurgeHandler("users", deps)()).resolves.toBeUndefined();
  });

  it("skips all processing and audit when activeRetention is not declared", async () => {
    const { queue, enqueue } = makeQueue();
    const payload = makePayloadApi([{ id: "y" }]);
    const { auditLog, record } = makeAuditLog();
    const config = makeConfig([
      {
        slug: "logs",
        custom: { retention: { purgeSchedule: "daily" } },
        fields: [],
      },
    ]);
    const deps: RetentionPurgeJobDeps = {
      queue,
      config,
      getPayload: vi.fn().mockResolvedValue(payload),
      auditLog,
    };
    await buildPurgeHandler("logs", deps)();

    expect(payload.find).not.toHaveBeenCalled();
    expect(record).not.toHaveBeenCalled();
    // Still re-enqueues for the next cycle
    expect(enqueue).toHaveBeenCalledOnce();
  });
});
