import { RenewalsService } from "./renewals.service";

/**
 * The renewal sweep's half of the unified notification log.
 *
 * `RenewalNotice` only records that a policy WAS notified — it has no way to
 * say a send failed or that a customer had no address. Those rows exist only
 * in `email_notification_log`, so they are what these tests pin down.
 */

const POLICY_ID = "policy-1";
const CUSTOMER_ID = "cust-1";

function makePolicy(email: string | null) {
  return {
    id: POLICY_ID,
    policyNumber: "700442181",
    policyTo: new Date("2026-09-01T00:00:00.000Z"),
    netPremium: null,
    policyFee: null,
    total: null,
    currency: "MXN",
    coveragesJson: null,
    customer: {
      id: CUSTOMER_ID,
      name: "ACME SA DE CV",
      nameMissing: false,
      email,
      phone: null,
      mobile: null,
      addressLine1: null,
      addressLine2: null,
      city: null,
      state: null,
      zipCode: null,
      country: null,
    },
    policyType: { name: "AUTO" },
    insuranceProvider: { name: "GMX" },
    vehicles: [],
    renewalNotices: [],
  };
}

function build(overrides: {
  policies?: ReturnType<typeof makePolicy>[];
  sendImpl?: () => Promise<{ messageId: string; response: string }>;
}) {
  const policies = overrides.policies ?? [makePolicy("cliente@example.com")];

  const record = jest.fn().mockResolvedValue(undefined);
  const send =
    overrides.sendImpl ??
    jest.fn().mockResolvedValue({ messageId: "ses-1", response: "{}" });

  const prisma = {
    // Only generation 1 has a candidate; the other two cadences return none,
    // so a sweep produces exactly one outcome to assert on.
    policy: {
      findMany: jest
        .fn()
        .mockResolvedValueOnce(policies)
        .mockResolvedValue([]),
      findFirst: jest.fn().mockResolvedValue(policies[0]),
    },
    renewalNotice: { upsert: jest.fn().mockResolvedValue({}) },
    scheduledJobState: {
      upsert: jest.fn().mockResolvedValue({}),
      updateMany: jest.fn().mockResolvedValue({ count: 1 }),
      findUniqueOrThrow: jest.fn().mockResolvedValue({ lastSuccessfulAt: null }),
      update: jest.fn().mockResolvedValue({}),
    },
  };

  // `register` is a no-op here: these tests drive the sweep directly, so no
  // cron job is ever installed.
  const schedule = { register: jest.fn().mockResolvedValue(undefined) };
  const service = new RenewalsService(
    prisma as never,
    { available: true, send } as never,
    { log: jest.fn() } as never,
    { record } as never,
    schedule as never,
  );

  return { service, record, send, prisma };
}

describe("renewal notices write the shared notification log", () => {
  it("records a SENT row tagged RENEWAL_NOTICE / POLICIES", async () => {
    const { service, record, prisma } = build({});

    await service.sweep("user-1");

    expect(record).toHaveBeenCalledTimes(1);
    const row = record.mock.calls[0][0];
    expect(row).toMatchObject({
      notificationType: "RENEWAL_NOTICE",
      servicio: "POLICIES",
      status: "SENT",
      customerId: CUSTOMER_ID,
      customerEmail: "cliente@example.com",
      providerMessageId: "ses-1",
      debug: false,
    });
    // `level` carries the aviso generation, not an alert colour.
    expect(row.level).toBe(1);
    expect(row.subject).toContain("700442181");
    expect(row.bodySnapshot).toContain("ACME SA DE CV");
    // The gating row is still written — the log does not replace it.
    expect(prisma.renewalNotice.upsert).toHaveBeenCalledTimes(1);
  });

  it("records a FAILED row and no gating row when the send throws", async () => {
    const { service, record, prisma } = build({
      sendImpl: jest.fn().mockRejectedValue(new Error("SES rejected")),
    });

    const result = await service.sweep("user-1");

    expect(result.sent).toBe(0);
    expect(result.failed).toBe(1);
    expect(record).toHaveBeenCalledTimes(1);
    expect(record.mock.calls[0][0]).toMatchObject({
      status: "FAILED",
      error: "SES rejected",
      notificationType: "RENEWAL_NOTICE",
    });
    // Nothing was delivered, so nothing may gate tomorrow's retry.
    expect(prisma.renewalNotice.upsert).not.toHaveBeenCalled();
  });

  it("records SKIPPED_NO_EMAIL for a candidate with no address", async () => {
    const { service, record, send, prisma } = build({
      policies: [makePolicy("   ")],
    });

    const result = await service.sweep("user-1");

    expect(result.skipped).toBe(1);
    expect(send).not.toHaveBeenCalled();
    expect(prisma.renewalNotice.upsert).not.toHaveBeenCalled();
    expect(record.mock.calls[0][0]).toMatchObject({
      status: "SKIPPED_NO_EMAIL",
      customerEmail: "",
    });
  });

  it("diverts a debug sweep and leaves the notice pending", async () => {
    const { service, record, send, prisma } = build({});

    const result = await service.sweep("user-1", { debug: true });

    expect(result.sent).toBe(1);
    expect(result.debug).toBe(true);
    // The customer's own address is never contacted.
    expect(jest.mocked(send).mock.calls[0][0]).toMatchObject({
      to: "rmancinas@freakma.net",
      xTracking: "debug",
    });
    expect(record.mock.calls[0][0]).toMatchObject({
      status: "SENT",
      customerEmail: "rmancinas@freakma.net",
      debug: true,
    });
    // The letter is still owed, so nothing may gate it: no RenewalNotice row,
    // and `lastSuccessfulAt` must not advance past the days we only tested.
    expect(prisma.renewalNotice.upsert).not.toHaveBeenCalled();
    const release = prisma.scheduledJobState.update.mock.calls.at(-1)?.[0];
    expect(release.data.lastSuccessfulAt).toBeUndefined();
  });

  it("sends one notice on demand in debug without marking it sent", async () => {
    const { service, send, prisma } = build({});

    const result = await service.sendOne(POLICY_ID, 1, "user-1", {
      debug: true,
    });

    expect(result.debug).toBe(true);
    expect(result.to).toBe("rmancinas@freakma.net");
    expect(send).toHaveBeenCalledTimes(1);
    expect(prisma.renewalNotice.upsert).not.toHaveBeenCalled();
  });

  it("does not fail a delivered notice when the log write throws", async () => {
    const { service, record } = build({});
    record.mockRejectedValue(new Error("log table gone"));

    const result = await service.sweep("user-1");

    // The mail went out and the gating row was written; a lost audit row must
    // not report that as a failure, which would re-send tomorrow.
    expect(result.sent).toBe(1);
    expect(result.failed).toBe(0);
  });
});
