// Project-ID allocation — core invariant: an abandoned "Neues Projekt" form
// must NEVER consume a sequence number. Preview reads only; allocation (save)
// goes through the atomic next_project_id() RPC.
import { describe, it, expect, vi } from 'vitest'
import type { SupabaseClient } from '@supabase/supabase-js'
import {
  formatProjectCode,
  previewNextProjectCode,
  allocateNextProjectCode,
} from '@/lib/projects/project-id'

// Minimal mock of the admin client: a from().select().eq().maybeSingle() chain
// for the sequence read, plus rpc() for allocation. We assert which path runs
// so "preview never consumes" is enforced structurally.
function makeAdmin(opts: {
  sequenceRow?: { last_number: number } | null
  sequenceError?: unknown
  rpcResult?: string
  rpcError?: unknown
}) {
  const rpc = vi.fn(async () => ({
    data: opts.rpcResult ?? null,
    error: opts.rpcError ?? null,
  }))
  const maybeSingle = vi.fn(async () => ({
    data: opts.sequenceRow ?? null,
    error: opts.sequenceError ?? null,
  }))
  const eq = vi.fn(() => ({ maybeSingle }))
  const select = vi.fn(() => ({ eq }))
  const from = vi.fn(() => ({ select }))
  const admin = { from, rpc } as unknown as SupabaseClient
  return { admin, from, rpc, select, eq, maybeSingle }
}

describe('formatProjectCode', () => {
  it('zero-pads to YYYY_NNN', () => {
    expect(formatProjectCode(2026, 1)).toBe('2026_001')
    expect(formatProjectCode(2026, 42)).toBe('2026_042')
    expect(formatProjectCode(2026, 100)).toBe('2026_100')
  })
})

describe('previewNextProjectCode', () => {
  it('returns last_number + 1 WITHOUT consuming an id (no rpc call)', async () => {
    const { admin, from, rpc, eq } = makeAdmin({ sequenceRow: { last_number: 5 } })
    const result = await previewNextProjectCode(admin, 2026)
    expect(result).toEqual({ ok: true, projectCode: '2026_006' })
    expect(from).toHaveBeenCalledWith('project_id_sequence')
    expect(eq).toHaveBeenCalledWith('year', 2026)
    // The invariant: preview must NEVER call the allocating RPC.
    expect(rpc).not.toHaveBeenCalled()
  })

  it('treats a missing sequence row as 0 → first id of the year', async () => {
    const { admin, rpc } = makeAdmin({ sequenceRow: null })
    const result = await previewNextProjectCode(admin, 2026)
    expect(result).toEqual({ ok: true, projectCode: '2026_001' })
    expect(rpc).not.toHaveBeenCalled()
  })

  it('returns ok:false on a read error', async () => {
    const { admin } = makeAdmin({ sequenceError: { message: 'boom' } })
    const result = await previewNextProjectCode(admin, 2026)
    expect(result.ok).toBe(false)
  })
})

describe('allocateNextProjectCode', () => {
  it('consumes the next id via next_project_id() and never reads the sequence directly', async () => {
    const { admin, from, rpc } = makeAdmin({ rpcResult: '2026_007' })
    const result = await allocateNextProjectCode(admin)
    expect(result).toEqual({ ok: true, projectCode: '2026_007' })
    expect(rpc).toHaveBeenCalledWith('next_project_id')
    // Allocation goes through the atomic RPC only — no direct table read/write.
    expect(from).not.toHaveBeenCalled()
  })

  it('returns ok:false when the RPC errors', async () => {
    const { admin } = makeAdmin({ rpcError: { message: 'nope' } })
    const result = await allocateNextProjectCode(admin)
    expect(result.ok).toBe(false)
  })
})
