// Tests for lib/offline/queue.ts — enqueue, dequeue, markFailed
// Uses lightweight mocks instead of fake-indexeddb (not in devDependencies).

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

// ── Mock Dexie db ─────────────────────────────────────────────────────────────

type FakeEntry = {
  id?: number
  table: string
  operation: string
  status: string
  createdAt: string
  attempts: number
  retryCount: number
  permanentlyFailed: boolean
  payload?: Record<string, unknown>
  recordId?: string
  conflictTarget?: string
  lastError?: string
  lastAttemptAt?: string
}

let _nextId = 1
let _store: Map<number, FakeEntry>

function makeFakeSyncQueue() {
  return {
    add: vi.fn(async (entry: Omit<FakeEntry, 'id'>) => {
      const id = _nextId++
      _store.set(id, { ...entry, id })
      return id
    }),
    get: vi.fn(async (id: number) => _store.get(id)),
    update: vi.fn(async (id: number, patch: Partial<FakeEntry>) => {
      const existing = _store.get(id)
      if (existing) _store.set(id, { ...existing, ...patch })
    }),
    delete: vi.fn(async (id: number) => {
      _store.delete(id)
    }),
    where: vi.fn((field: string) => ({
      equals: (val: string) => ({
        sortBy: async () => {
          const rows = Array.from(_store.values()).filter(
            (e) => (e as Record<string, unknown>)[field] === val,
          )
          rows.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
          return rows
        },
        count: async () =>
          Array.from(_store.values()).filter(
            (e) => (e as Record<string, unknown>)[field] === val,
          ).length,
        toArray: async () =>
          Array.from(_store.values()).filter(
            (e) => (e as Record<string, unknown>)[field] === val,
          ),
      }),
    })),
  }
}

vi.mock('@/lib/offline/db', () => ({
  getDB: () => ({ syncQueue: makeFakeSyncQueue() }),
}))

vi.mock('@/lib/offline/registry', () => ({
  getOfflineConfig: vi.fn((table: string) =>
    table === 'lsc_shifts' || table === 'lsc_shift_hours' || table === 'cycle_measurements'
      ? { table, storeName: table, ttlMs: 30_000, queueWrites: true, primaryKey: 'id' }
      : null,
  ),
}))

// Re-import after mocks
import { enqueue, dequeue, markFailed, getAllPending, MAX_RETRY_COUNT } from '../queue'

// ── Helpers ───────────────────────────────────────────────────────────────────

beforeEach(() => {
  _store = new Map()
  _nextId = 1
})

// ── enqueue ───────────────────────────────────────────────────────────────────

describe('enqueue', () => {
  it('adds a pending entry for a registered queueWrites table', async () => {
    await enqueue({ table: 'lsc_shifts', operation: 'insert', payload: { id: 'abc' } })
    const pending = await getAllPending()
    expect(pending).toHaveLength(1)
    expect(pending[0].table).toBe('lsc_shifts')
    expect(pending[0].status).toBe('pending')
  })

  it('silently skips tables not registered with queueWrites:true', async () => {
    await enqueue({ table: 'unregistered_table', operation: 'insert' })
    const pending = await getAllPending()
    expect(pending).toHaveLength(0)
  })

  it('stores payload and recordId', async () => {
    await enqueue({
      table: 'lsc_shift_hours',
      operation: 'upsert',
      payload: { id: 'row-1', ist_output: 5 },
      recordId: 'row-1',
    })
    const pending = await getAllPending()
    expect(pending[0].payload).toEqual({ id: 'row-1', ist_output: 5 })
    expect(pending[0].recordId).toBe('row-1')
  })

  it('sets initial attempts=0 and retryCount=0', async () => {
    await enqueue({ table: 'lsc_shifts', operation: 'delete', recordId: 'xyz' })
    const pending = await getAllPending()
    expect(pending[0].attempts).toBe(0)
    expect(pending[0].retryCount).toBe(0)
  })
})

// ── dequeue ───────────────────────────────────────────────────────────────────

describe('dequeue', () => {
  it('returns null when queue is empty', async () => {
    const result = await dequeue()
    expect(result).toBeNull()
  })

  it('returns the oldest pending entry (FIFO)', async () => {
    await enqueue({ table: 'lsc_shifts', operation: 'insert', payload: { id: 'first' } })
    await enqueue({ table: 'lsc_shifts', operation: 'insert', payload: { id: 'second' } })
    const entry = await dequeue()
    expect(entry?.payload?.id).toBe('first')
  })
})

// ── markFailed ────────────────────────────────────────────────────────────────

describe('markFailed', () => {
  it('increments retryCount and sets lastError', async () => {
    await enqueue({ table: 'lsc_shifts', operation: 'insert', payload: { id: 'x' } })
    const entry = (await getAllPending())[0]
    await markFailed(entry.id!, 'Network error')
    const updated = _store.get(entry.id!)
    expect(updated?.retryCount).toBe(1)
    expect(updated?.lastError).toBe('Network error')
    expect(updated?.status).toBe('error')
  })

  it('marks permanentlyFailed when retryCount >= MAX_RETRY_COUNT', async () => {
    await enqueue({ table: 'lsc_shifts', operation: 'insert', payload: { id: 'x' } })
    const entry = (await getAllPending())[0]
    // Simulate MAX_RETRY_COUNT - 1 existing retries
    _store.set(entry.id!, { ..._store.get(entry.id!)!, retryCount: MAX_RETRY_COUNT - 1 })
    await markFailed(entry.id!, 'Final error')
    const updated = _store.get(entry.id!)
    expect(updated?.permanentlyFailed).toBe(true)
  })

  it('is a no-op for unknown autoIds', async () => {
    await expect(markFailed(9999, 'error')).resolves.toBeUndefined()
  })
})
