// Regression guard for PR #1381 review P1: on Cloudflare the queue shim only
// writes a DELAYED job to D1 (with will_run_at, later dispatched by the cron)
// when it is persisted. A delayed job pushed with persist:false is silently
// dropped — which is exactly how the webhook retry ladder lost deliveries after
// the first failure. These tests pin that invariant so callers always persist
// delayed retries.

// A single shared store mock so we can assert what the shim wrote.
const mockStore = {
  addJob: jest.fn().mockResolvedValue(undefined),
  deleteJob: jest.fn().mockResolvedValue(true),
  getJob: jest.fn(),
  getJobs: jest.fn(),
  getScheduledJobs: jest.fn(),
  updateJob: jest.fn(),
  isCancelled: jest.fn().mockResolvedValue(false),
  findJobs: jest.fn(),
};

jest.mock('../../../api/src/store/models/job', () => ({ Job: { findAll: jest.fn() } }));
jest.mock('../../../api/src/libs/queue/store', () => ({
  __esModule: true,
  default: jest.fn(() => mockStore),
}));

import createQueue, { setCFQueue, flushPendingJobs } from '../../shims/queue';

const futureRunAt = () => Math.floor(Date.now() / 1000) + 600; // +10 min → delayed

describe('shim queue — delayed job persistence (PR #1381 P1)', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    setCFQueue(null); // no CF Queue binding; delayed path is independent of it anyway
  });

  it('persists a delayed job to D1 (with will_run_at) when persist=true', async () => {
    const q = createQueue({ name: 'webhook-persist-true', onJob: jest.fn() });
    q.push({ id: 'j1', job: { v: 1 }, runAt: futureRunAt(), persist: true });
    await flushPendingJobs();

    expect(mockStore.addJob).toHaveBeenCalledTimes(1);
    const attrs = mockStore.addJob.mock.calls[0]![2];
    expect(attrs.will_run_at).toBeGreaterThan(Date.now());
    expect(attrs.delay).toBeGreaterThan(0);
  });

  it('DROPS a delayed job (no D1 write) when persist=false — the lost-retry bug class', async () => {
    const q = createQueue({ name: 'webhook-persist-false', onJob: jest.fn() });
    q.push({ id: 'j2', job: { v: 2 }, runAt: futureRunAt(), persist: false });
    await flushPendingJobs();

    expect(mockStore.addJob).not.toHaveBeenCalled();
  });
});

describe('shim queue — inline fallback keeps the D1 backup on failure (PR #1381 re-review P1)', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('does NOT delete the persisted row when CF Queue send fails AND the inline handler fails', async () => {
    setCFQueue({ send: jest.fn().mockRejectedValue(new Error('429 Too Many Requests')) } as any);
    const onJob = jest.fn().mockRejectedValue(new Error('handler boom'));
    const q = createQueue({ name: 'webhook-fallback-fail', onJob, options: { maxRetries: 1 } });

    q.push({ id: 'wj1', job: { v: 1 }, persist: true }); // immediate
    await flushPendingJobs();

    expect(mockStore.addJob).toHaveBeenCalledTimes(1); // durable backup written
    expect(onJob).toHaveBeenCalled(); // inline attempt ran
    expect(mockStore.deleteJob).not.toHaveBeenCalled(); // backup retained → cron can retry
    setCFQueue(null);
  });

  it('deletes the persisted row when the inline fallback SUCCEEDS', async () => {
    setCFQueue({ send: jest.fn().mockRejectedValue(new Error('429')) } as any);
    const onJob = jest.fn().mockResolvedValue(undefined);
    const q = createQueue({ name: 'webhook-fallback-ok', onJob, options: { maxRetries: 1 } });

    q.push({ id: 'wj2', job: { v: 2 }, persist: true });
    await flushPendingJobs();

    expect(onJob).toHaveBeenCalled();
    expect(mockStore.deleteJob).toHaveBeenCalledWith('wj2'); // deleted only on success
    setCFQueue(null);
  });
});
