import { describe, it, expect, vi, beforeEach } from 'vitest';

vi.mock('cloudflare:workers', () => ({
  DurableObject: class DurableObject<Env = Cloudflare.Env> {
    protected ctx: DurableObjectState;
    protected env: Env;
    constructor(ctx: DurableObjectState, env: Env) {
      this.ctx = ctx;
      this.env = env;
    }
  },
  get env() {
    return {};
  },
}));

// Mock the reused sweep BEFORE importing the DO.
const runScheduledPublish = vi.hoisted(() => vi.fn());
vi.mock('../workers/cron/run.js', () => ({ runScheduledPublish }));
// Mock the db accessor so alarm() never hits a real driver.
vi.mock('../lib/db.js', () => ({ getFullDb: () => ({ db: {} }) }));

import { ScheduledPublishDO } from './scheduled-publish.js';

function makeDO(secret: string | undefined, initialAlarm: number | null = null) {
  let alarm: number | null = initialAlarm;
  const storage = {
    getAlarm: vi.fn(async () => alarm),
    setAlarm: vi.fn(async (t: number) => { alarm = t; }),
  };
  const ctx = { storage } as unknown as DurableObjectState;
  const env = { CRON_DO_SECRET: secret } as unknown as Cloudflare.Env;
  const obj = new ScheduledPublishDO(ctx, env);
  return { obj, storage };
}

const armReq = (secret?: string) =>
  new Request('https://do/', {
    method: 'POST',
    headers: secret === undefined ? {} : { 'x-mod-cron-secret': secret },
  });

beforeEach(() => { runScheduledPublish.mockReset(); });

describe('ScheduledPublishDO.fetch (arm)', () => {
  it('arms when no alarm pending, returns 200', async () => {
    const { obj, storage } = makeDO('s3cret');
    const res = await obj.fetch(armReq('s3cret'));
    expect(res.status).toBe(200);
    expect(storage.setAlarm).toHaveBeenCalledOnce();
  });
  it('does NOT reset an already-pending alarm', async () => {
    const { obj, storage } = makeDO('s3cret', 1_000_000);
    const res = await obj.fetch(armReq('s3cret'));
    expect(res.status).toBe(200);
    expect(storage.setAlarm).not.toHaveBeenCalled();
  });
  it('403 + no setAlarm on missing header', async () => {
    const { obj, storage } = makeDO('s3cret');
    const res = await obj.fetch(armReq());
    expect(res.status).toBe(403);
    expect(storage.setAlarm).not.toHaveBeenCalled();
  });
  it('403 on wrong secret', async () => {
    const { obj } = makeDO('s3cret');
    expect((await obj.fetch(armReq('nope'))).status).toBe(403);
  });
  it('403 (fail-closed) when CRON_DO_SECRET is empty even with a header', async () => {
    const { obj } = makeDO('');
    expect((await obj.fetch(armReq('anything'))).status).toBe(403);
  });
});

describe('ScheduledPublishDO.alarm', () => {
  it('runs the sweep and reschedules +5min on success', async () => {
    runScheduledPublish.mockResolvedValue({ promoted: 2 });
    const { obj, storage } = makeDO('s3cret');
    await obj.alarm();
    expect(runScheduledPublish).toHaveBeenCalledOnce();
    expect(storage.setAlarm).toHaveBeenCalledOnce();
  });
  it('reschedules +5min on failure and does NOT rethrow', async () => {
    runScheduledPublish.mockRejectedValue(new Error('db down'));
    const { obj, storage } = makeDO('s3cret');
    await expect(obj.alarm()).resolves.toBeUndefined(); // no rethrow
    expect(storage.setAlarm).toHaveBeenCalledOnce();
  });
});
