import { clampLimit } from './cursor.js'
import type { RecordStore } from './types.js'

type JsonRecord = Record<string, unknown>

function stableStringify(value: unknown): string {
  if (value === null || typeof value !== 'object') {
    return JSON.stringify(value)
  }
  if (Array.isArray(value)) {
    return `[${value.map((item) => stableStringify(item)).join(',')}]`
  }
  const object = value as Record<string, unknown>
  const keys = Object.keys(object).sort()
  return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(',')}}`
}

function deepEqual(a: unknown, b: unknown): boolean {
  return stableStringify(a) === stableStringify(b)
}

function assert(condition: boolean, message: string): asserts condition {
  if (!condition) {
    throw new Error(message)
  }
}

function assertDeepEqual(actual: unknown, expected: unknown, label: string): void {
  if (!deepEqual(actual, expected)) {
    throw new Error(
      `${label}: expected ${stableStringify(expected)} but got ${stableStringify(actual)}`,
    )
  }
}

type MakeStore = (opts?: {
  now?: () => string
  makeId?: () => string
}) => RecordStore<JsonRecord> | Promise<RecordStore<JsonRecord>>

async function freshStore(
  makeStore: MakeStore,
  opts?: { now?: () => string; makeId?: () => string },
): Promise<RecordStore<JsonRecord>> {
  return await makeStore(opts)
}

export async function runRecordStoreConformance(makeStore: MakeStore): Promise<void> {
  await scenarioAppendThenListNewestFirst(makeStore)
  await scenarioCursorRoundTrip(makeStore)
  await scenarioLimitClamp(makeStore)
  await scenarioEmptyStore(makeStore)
  await scenarioSameCreatedAtMsBatch(makeStore)
  await scenarioCanonicalization(makeStore)
  await scenarioImmutabilityAliasing(makeStore)
}

async function scenarioAppendThenListNewestFirst(makeStore: MakeStore): Promise<void> {
  let appendCount = 0
  const store = await freshStore(makeStore, {
    now: () => (appendCount === 0 ? '2026-01-01T00:00:00.000Z' : '2026-01-02T00:00:00.000Z'),
    makeId: () => {
      appendCount += 1
      return `id-${appendCount}`
    },
  })

  await store.append({ n: 1 })
  await store.append({ n: 2 })

  const page = await store.list()
  assert(page.items.length === 2, 'append-then-list: expected 2 items')
  assertDeepEqual(page.items[0]!.record, { n: 2 }, 'append-then-list: newest first')
  assertDeepEqual(page.items[1]!.record, { n: 1 }, 'append-then-list: older second')
}

async function scenarioCursorRoundTrip(makeStore: MakeStore): Promise<void> {
  let appendCount = 0
  const store = await freshStore(makeStore, {
    now: () => {
      appendCount += 1
      const day = String(appendCount).padStart(2, '0')
      return `2026-01-${day}T00:00:00.000Z`
    },
    makeId: () => {
      return `id-${appendCount}`
    },
  })

  const total = 7
  for (let i = 1; i <= total; i++) {
    await store.append({ n: i })
  }

  const full = await store.list({ limit: total })
  const fullRecords = full.items.map((item) => item.record)

  const pageSize = 3
  const collected: JsonRecord[] = []
  let cursor: string | null | undefined = undefined

  for (;;) {
    const page = await store.list({
      limit: pageSize,
      ...(cursor ? { cursor } : {}),
    })
    collected.push(...page.items.map((item) => item.record))
    if (page.nextCursor === null) {
      break
    }
    cursor = page.nextCursor
  }

  assert(collected.length === total, 'cursor round-trip: collected count mismatch')
  assertDeepEqual(collected, fullRecords, 'cursor round-trip: items mismatch')

  const seen = new Set(collected.map((item) => stableStringify(item)))
  assert(seen.size === total, 'cursor round-trip: duplicate items detected')
}

async function scenarioLimitClamp(makeStore: MakeStore): Promise<void> {
  const store = await freshStore(makeStore)

  for (let i = 0; i < 5; i++) {
    await store.append({ n: i })
  }

  const cases: Array<{ limit: number | undefined; expected: number }> = [
    { limit: undefined, expected: 50 },
    { limit: Number.NaN, expected: 50 },
    { limit: 0, expected: 50 },
    { limit: -1, expected: 50 },
    { limit: 9999, expected: 200 },
    { limit: 2, expected: 2 },
  ]

  for (const { limit, expected } of cases) {
    const clamped = clampLimit(limit)
    assert(clamped === expected, `limit clamp: clampLimit(${String(limit)}) expected ${expected}`)
    const page = await store.list({ limit })
    assert(
      page.items.length === Math.min(expected, 5),
      `limit clamp: list(limit=${String(limit)}) returned ${page.items.length} items`,
    )
  }
}

async function scenarioEmptyStore(makeStore: MakeStore): Promise<void> {
  const store = await freshStore(makeStore)
  const page = await store.list()
  assertDeepEqual(page, { items: [], nextCursor: null }, 'empty store')
}

async function scenarioSameCreatedAtMsBatch(makeStore: MakeStore): Promise<void> {
  const fixedNow = '2026-06-17T12:00:00.000Z'
  let idCounter = 0

  const store = await freshStore(makeStore, {
    now: () => fixedNow,
    makeId: () => `batch-id-${++idCounter}`,
  })

  const batchSize = 5
  for (let i = 1; i <= batchSize; i++) {
    await store.append({ n: i })
  }

  const full = await store.list({ limit: batchSize })
  assert(
    full.items.every((item) => item.createdAt === new Date(Date.parse(fixedNow)).toISOString()),
    'same-createdAtMs batch: createdAt must be canonical',
  )

  const expectedOrder = Array.from({ length: batchSize }, (_, i) => ({ n: batchSize - i }))
  assertDeepEqual(
    full.items.map((item) => item.record),
    expectedOrder,
    'same-createdAtMs batch: newest-inserted first via seq',
  )

  const pageSize = 2
  const collected: JsonRecord[] = []
  let cursor: string | null | undefined = undefined

  for (;;) {
    const page = await store.list({
      limit: pageSize,
      ...(cursor ? { cursor } : {}),
    })
    collected.push(...page.items.map((item) => item.record))
    if (page.nextCursor === null) {
      break
    }
    cursor = page.nextCursor
  }

  assert(collected.length === batchSize, 'same-createdAtMs batch: pagination count mismatch')
  assertDeepEqual(collected, expectedOrder, 'same-createdAtMs batch: pagination order mismatch')

  const seen = new Set(collected.map((item) => stableStringify(item)))
  assert(seen.size === batchSize, 'same-createdAtMs batch: duplicate items across pages')
}

async function scenarioCanonicalization(makeStore: MakeStore): Promise<void> {
  const storeNoMillis = await freshStore(makeStore, {
    now: () => '2026-01-01T00:00:00Z',
    makeId: () => 'canon-1',
  })
  const noMillis = await storeNoMillis.append({ tag: 'no-millis' })
  assert(
    noMillis.createdAt === '2026-01-01T00:00:00.000Z',
    `canonicalization: no-millis clock stored ${noMillis.createdAt}`,
  )

  const storeOffset = await freshStore(makeStore, {
    now: () => '2026-01-01T02:00:00+02:00',
    makeId: () => 'canon-2',
  })
  const offset = await storeOffset.append({ tag: 'offset' })
  assert(
    offset.createdAt === '2026-01-01T00:00:00.000Z',
    `canonicalization: offset clock stored ${offset.createdAt}`,
  )
}

async function scenarioImmutabilityAliasing(makeStore: MakeStore): Promise<void> {
  const store = await freshStore(makeStore, {
    now: () => '2026-01-01T00:00:00.000Z',
    makeId: () => 'alias-1',
  })

  const input = { nested: { value: 1 }, list: [1, 2] }
  await store.append(input satisfies JsonRecord)

  input.nested = { value: 999 }
  input.list.push(3)
  ;(input as JsonRecord).extra = true

  const page = await store.list()
  assertDeepEqual(
    page.items[0]!.record,
    { nested: { value: 1 }, list: [1, 2] },
    'immutability: stored record must not reflect caller mutation',
  )
}
