import { it, expect, vi } from 'vitest'
import { createJobRegistry } from './index.js'
import { JobRunnerDO, REARM_DELAY_MS, type DurableObjectAlarmContext } from './do-runner.js'
import type { JobEnvelope } from './index.js'

type Env = Record<string, never>

function okSchema() {
  return {
    '~standard': {
      version: 1 as const,
      vendor: 'test',
      validate: (value: unknown) => ({ value: value as { n: number } }),
    },
  }
}

function fakeCtx(): DurableObjectAlarmContext & { alarmAt: () => number | null } {
  let alarm: number | null = null
  return {
    setAlarm: vi.fn(async (ts: number | Date) => {
      alarm = typeof ts === 'number' ? ts : ts.getTime()
    }),
    getAlarm: vi.fn(async () => alarm),
    alarmAt: () => alarm,
  }
}

class TestRunner extends JobRunnerDO<Env> {
  constructor(
    ctx: DurableObjectAlarmContext,
    registry: ReturnType<typeof createJobRegistry<Env>>,
    private queue: JobEnvelope[],
  ) {
    super(ctx, registry, {})
  }

  protected async pullNext(): Promise<JobEnvelope | null> {
    return this.queue.shift() ?? null
  }

  protected async hasBacklog(): Promise<boolean> {
    return this.queue.length > 0
  }
}

it('nudge sets alarm when none pending', async () => {
  const ctx = fakeCtx()
  const registry = createJobRegistry<Env>()
  const runner = new TestRunner(ctx, registry, [])
  await runner.nudge()
  expect(ctx.setAlarm).toHaveBeenCalledOnce()
  expect(ctx.alarmAt()).toBeTypeOf('number')
})

it('nudge is no-op when alarm already pending', async () => {
  const ctx = fakeCtx()
  await ctx.setAlarm(Date.now() + 5000)
  const registry = createJobRegistry<Env>()
  const runner = new TestRunner(ctx, registry, [])
  await runner.nudge()
  expect(ctx.setAlarm).toHaveBeenCalledOnce()
})

it('alarm re-arms when backlog remains', async () => {
  const ctx = fakeCtx()
  const registry = createJobRegistry<Env>()
  registry.register('a', okSchema(), async () => {})
  const runner = new TestRunner(ctx, registry, [
    { type: 'a', payload: { n: 1 } },
    { type: 'a', payload: { n: 2 } },
  ])
  const before = Date.now()
  await runner.alarm()
  expect(ctx.setAlarm).toHaveBeenCalledOnce()
  const scheduled = ctx.alarmAt()!
  expect(scheduled).toBeGreaterThanOrEqual(before + REARM_DELAY_MS - 50)
})

it('alarm does not re-arm when backlog is drained', async () => {
  const ctx = fakeCtx()
  const registry = createJobRegistry<Env>()
  registry.register('solo', okSchema(), async () => {})
  const runner = new TestRunner(ctx, registry, [{ type: 'solo', payload: { n: 1 } }])
  await runner.alarm()
  expect(ctx.setAlarm).not.toHaveBeenCalled()
})

it('alarm re-arms in finally when handler throws and backlog remains', async () => {
  const ctx = fakeCtx()
  const registry = createJobRegistry<Env>()
  registry.register('boom', okSchema(), async () => {
    throw new Error('transient')
  })
  const runner = new TestRunner(ctx, registry, [
    { type: 'boom', payload: { n: 1 } },
    { type: 'boom', payload: { n: 2 } },
  ])
  await expect(runner.alarm()).rejects.toThrow('transient')
  expect(ctx.setAlarm).toHaveBeenCalledOnce()
})
