import { it, expect, vi } from 'vitest'
import {
  createJobRegistry,
  TerminalJobError,
  type IdempotencyStore,
  type JobEnvelope,
} from './index.js'

type Env = { tag: string }

function okSchema() {
  return {
    '~standard': {
      version: 1 as const,
      vendor: 'test',
      validate: (value: unknown) =>
        typeof value === 'object' && value !== null && 'n' in value
          ? { value: value as { n: number } }
          : { issues: [{ message: 'bad payload' }] },
    },
  }
}

function rejectSchema() {
  return {
    '~standard': {
      version: 1 as const,
      vendor: 'test',
      validate: () => ({ issues: [{ message: 'invalid' }] }),
    },
  }
}

it('register + dispatch routes by type to the right handler', async () => {
  const registry = createJobRegistry<Env>()
  const a = vi.fn()
  const b = vi.fn()
  registry.register('a', okSchema(), async (env, payload) => {
    a(env, payload)
  })
  registry.register('b', okSchema(), async (env, payload) => {
    b(env, payload)
  })

  await registry.dispatch({ tag: 'env' }, { type: 'b', payload: { n: 2 } })
  expect(a).not.toHaveBeenCalled()
  expect(b).toHaveBeenCalledWith({ tag: 'env' }, { n: 2 })
})

it('unknown type → completes without throw (ack path), handler not called', async () => {
  const registry = createJobRegistry<Env>()
  const handle = vi.fn()
  registry.register('known', okSchema(), handle)
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})

  await expect(
    registry.dispatch({ tag: 'env' }, { type: 'missing', payload: {} }),
  ).resolves.toBeUndefined()
  expect(handle).not.toHaveBeenCalled()
  expect(warn).toHaveBeenCalled()
  warn.mockRestore()
})

it('strict:true + unknown type → throws (backstop path)', async () => {
  const registry = createJobRegistry<Env>()
  await expect(
    registry.dispatch({ tag: 'env' }, { type: 'missing', payload: {} }, { strict: true }),
  ).rejects.toThrow('unknown job type: missing')
})

it('schema rejects → ack-malformed path, handler not called', async () => {
  const registry = createJobRegistry<Env>()
  const handle = vi.fn()
  registry.register('bad', rejectSchema(), handle)
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})

  await expect(
    registry.dispatch({ tag: 'env' }, { type: 'bad', payload: {} }),
  ).resolves.toBeUndefined()
  expect(handle).not.toHaveBeenCalled()
  expect(warn).toHaveBeenCalled()
  warn.mockRestore()
})

it('TerminalJobError → onTerminalFailure fired, no throw; transient Error rethrows', async () => {
  const registry = createJobRegistry<Env>()
  const onFail = vi.fn()
  registry.register('term', okSchema(), async () => {
    throw new TerminalJobError('done')
  })
  registry.register('transient', okSchema(), async () => {
    throw new Error('retry me')
  })

  await expect(
    registry.dispatch({ tag: 'env' }, { type: 'term', payload: { n: 1 } }, { onTerminalFailure: onFail }),
  ).resolves.toBeUndefined()
  expect(onFail).toHaveBeenCalledWith(expect.any(TerminalJobError), { tag: 'env' }, {
    type: 'term',
    payload: { n: 1 },
  })

  await expect(
    registry.dispatch({ tag: 'env' }, { type: 'transient', payload: { n: 1 } }),
  ).rejects.toThrow('retry me')
})

it('idempotency: seen → skip handler; unseen → run + mark', async () => {
  const registry = createJobRegistry<Env>()
  const handle = vi.fn()
  registry.register('work', okSchema(), handle)

  const seenStore: IdempotencyStore = {
    seen: vi.fn(async () => true),
    mark: vi.fn(async () => {}),
  }
  await registry.dispatch(
    { tag: 'env' },
    { type: 'work', payload: { n: 1 } },
    { idempotency: { key: 'k1', store: seenStore } },
  )
  expect(handle).not.toHaveBeenCalled()
  expect(seenStore.mark).not.toHaveBeenCalled()

  const freshStore: IdempotencyStore = {
    seen: vi.fn(async () => false),
    mark: vi.fn(async () => {}),
  }
  await registry.dispatch(
    { tag: 'env' },
    { type: 'work', payload: { n: 2 } } satisfies JobEnvelope,
    { idempotency: { key: 'k2', store: freshStore } },
  )
  expect(handle).toHaveBeenCalledOnce()
  expect(freshStore.mark).toHaveBeenCalledWith('k2')
})
