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

import {
  buildBackfillJobRows,
  consumePlatformJobsBatch,
  createPlatformJobsRegistry,
  idempotencyKeyForJob,
  type HostInjectedIdempotencyStore,
  type PlatformJobsHandlers,
  type ReportScheduleJob,
} from '../jobs'

function createInMemoryIdempotencyStore(): HostInjectedIdempotencyStore {
  const rows = new Map<string, {
    key: string
    status: string
    firstSeenAt: Date
    processedAt: Date | null
    expiresAt: Date | null
    lastReleasedAt: Date | null
    releaseCount: number
    payload: unknown
  }>()
  let tick = 0
  const now = () => new Date(Date.UTC(2026, 6, 1, 0, 0, tick++))

  return {
    async claim(key, payload) {
      if (rows.has(key)) return false
      rows.set(key, {
        key,
        status: 'processing',
        firstSeenAt: now(),
        processedAt: null,
        expiresAt: null,
        lastReleasedAt: null,
        releaseCount: 0,
        payload: payload ?? null,
      })
      return true
    },
    async seen(key) {
      return !(await this.claim(key))
    },
    async mark(key, ttlSeconds) {
      await this.markProcessed(key, ttlSeconds)
    },
    async markProcessed(key, ttlSeconds) {
      const row = rows.get(key)
      if (!row) return
      row.status = 'processed'
      row.processedAt = now()
      row.expiresAt = ttlSeconds ? new Date(row.processedAt.getTime() + ttlSeconds * 1000) : null
    },
    async release(key) {
      rows.delete(key)
    },
    async list() {
      return [...rows.values()]
    },
  }
}

function createMessage<T extends { type: string }>(body: T) {
  return {
    id: `${body.type}-1`,
    timestamp: new Date(),
    body,
    attempts: 1,
    ack: vi.fn(),
    retry: vi.fn(),
  }
}

describe('jobs platform parity', () => {
  let handlers: PlatformJobsHandlers

  beforeEach(() => {
    handlers = {
      handleInvoiceGenerate: vi.fn(async () => {}),
      handleLeadScoreRecalc: vi.fn(async () => {}),
      handleReportSchedule: vi.fn(async () => {}),
      handleRetainerInvoice: vi.fn(async () => {}),
      handleUniformExport: vi.fn(async () => {}),
    }
  })

  it('claims, enumerates, and marks idempotency records faithfully', async () => {
    const store = createInMemoryIdempotencyStore()

    expect(await store.claim('job:1', { type: 'uniform-format' })).toBe(true)
    expect(await store.claim('job:1', { type: 'uniform-format' })).toBe(false)
    expect(await store.seen('job:1')).toBe(true)

    const claimed = await store.list()
    expect(claimed).toHaveLength(1)
    expect(claimed[0]).toMatchObject({
      key: 'job:1',
      status: 'processing',
      payload: { type: 'uniform-format' },
    })

    await store.markProcessed('job:1', 60)

    const processed = await store.list()
    expect(processed[0]).toMatchObject({
      key: 'job:1',
      status: 'processed',
    })
    expect(processed[0]?.processedAt).toBeInstanceOf(Date)
    expect(processed[0]?.expiresAt).toBeInstanceOf(Date)
  })

  it('dispatches each supported message type and deduplicates double delivery', async () => {
    const registry = createPlatformJobsRegistry(handlers)
    const store = createInMemoryIdempotencyStore()

    const reportJob: ReportScheduleJob = {
      type: 'report.schedule',
      scheduleId: 'sched-1',
      tenantId: 'tenant-1',
      oneOff: true,
    }
    const uniformJob = {
      type: 'uniform-format' as const,
      jobId: 'job-1',
      tenantId: 'tenant-1',
      from: '2026-01-01',
      to: '2026-01-31',
      mode: 'documents' as const,
      userId: 'user-1',
    }

    const duplicateA = createMessage(uniformJob)
    const duplicateB = createMessage(uniformJob)
    const reportMessage = createMessage(reportJob)

    await consumePlatformJobsBatch(
      {
        queue: 'zync-jobs',
        messages: [duplicateA, duplicateB, reportMessage],
        retryAll: vi.fn(),
        ackAll: vi.fn(),
      } as never,
      {} as never,
      registry,
      store,
    )

    expect(handlers.handleUniformExport).toHaveBeenCalledTimes(1)
    expect(handlers.handleReportSchedule).toHaveBeenCalledTimes(1)
    expect(duplicateA.ack).toHaveBeenCalledTimes(1)
    expect(duplicateB.ack).toHaveBeenCalledTimes(1)
    expect(reportMessage.ack).toHaveBeenCalledTimes(1)

    const rows = await store.list()
    expect(rows.map((row) => row.key)).toEqual([
      idempotencyKeyForJob(uniformJob),
      idempotencyKeyForJob(reportJob),
    ])
    expect(rows.every((row) => row.status === 'processed')).toBe(true)
  })

  it('releases the idempotency claim when the handler throws so the queue can retry', async () => {
    handlers.handleLeadScoreRecalc = vi.fn(async () => {
      throw new Error('retry')
    })

    const registry = createPlatformJobsRegistry(handlers)
    const store = createInMemoryIdempotencyStore()
    const msg = createMessage({
      type: 'lead.score_recalc' as const,
      tenantId: 'tenant-1',
      leadId: 'lead-1',
    })

    await consumePlatformJobsBatch(
      {
        queue: 'zync-jobs',
        messages: [msg],
        retryAll: vi.fn(),
        ackAll: vi.fn(),
      } as never,
      {} as never,
      registry,
      store,
    )

    expect(msg.retry).toHaveBeenCalledTimes(1)
    expect(msg.ack).not.toHaveBeenCalled()
    expect(await store.list()).toHaveLength(0)
  })

  it('builds the expected expand/backfill rows from legacy job tables', () => {
    const rows = buildBackfillJobRows({
      invoiceGenerationJobs: [
        {
          id: 'bulk-1',
          tenantId: 'tenant-1',
          status: 'completed',
          errorMessage: null,
          createdAt: new Date('2026-06-01T00:00:00Z'),
          startedAt: new Date('2026-06-01T00:05:00Z'),
          completedAt: new Date('2026-06-01T00:10:00Z'),
          columnMapping: { action: 'invoice_generate', customerIds: ['c1'] },
        },
      ],
      uniformExportJobs: [
        {
          id: 'uniform-1',
          tenantId: 'tenant-1',
          status: 'error',
          errorMessage: 'boom',
          createdAt: new Date('2026-06-02T00:00:00Z'),
        },
      ],
      tenantExportJobs: [
        {
          id: 'export-1',
          tenantId: 'tenant-2',
          status: 'PROCESSING',
          createdAt: new Date('2026-06-03T00:00:00Z'),
          completedAt: null,
        },
      ],
    })

    expect(rows).toHaveLength(3)
    expect(rows).toEqual([
      expect.objectContaining({
        id: 'bulk-1',
        type: 'invoice.generate',
        status: 'completed',
        scheduledFor: new Date('2026-06-01T00:05:00Z'),
        processedAt: new Date('2026-06-01T00:10:00Z'),
      }),
      expect.objectContaining({
        id: 'uniform-1',
        type: 'uniform-format',
        status: 'failed',
        lastError: 'boom',
      }),
      expect.objectContaining({
        id: 'export-1',
        type: 'tenant.export',
        status: 'processing',
      }),
    ])
  })
})
