import { describe, expect, it } from 'vitest'
import { clampLimit, decodeCursor, encodeCursor, type Keyset } from './cursor.js'
import { deriveCreatedAt } from './derive.js'
import { InvalidCursorError } from './errors.js'

describe('encodeCursor / decodeCursor', () => {
  it('round-trips (createdAtMs, seq)', () => {
    const key: Keyset = { createdAtMs: 1_700_000_000_000, seq: 42 }
    expect(decodeCursor(encodeCursor(key))).toEqual(key)
  })

  it('round-trips zero values', () => {
    const key: Keyset = { createdAtMs: 0, seq: 0 }
    expect(decodeCursor(encodeCursor(key))).toEqual(key)
  })

  it('produces URL-safe output (no +, /, =)', () => {
    const encoded = encodeCursor({ createdAtMs: 1_700_000_000_000, seq: 99 })
    expect(encoded).not.toMatch(/[+/=]/)
  })

  it('throws InvalidCursorError on garbage input', () => {
    expect(() => decodeCursor('not-valid-base64!!!')).toThrow(InvalidCursorError)
  })

  it('throws InvalidCursorError on valid base64 but wrong shape', () => {
    const bad = btoa(JSON.stringify({ x: 1 })).replace(/=/g, '')
    expect(() => decodeCursor(bad)).toThrow(InvalidCursorError)
  })

  it('throws InvalidCursorError on non-finite numbers', () => {
    // Infinity in JSON serializes to null — guard must reject
    const bad = btoa(JSON.stringify({ c: null, s: null })).replace(/=/g, '')
    expect(() => decodeCursor(bad)).toThrow(InvalidCursorError)
  })
})

describe('clampLimit', () => {
  it('returns default for undefined', () => {
    expect(clampLimit(undefined)).toBe(20)
  })

  it('clamps huge value to max', () => {
    expect(clampLimit(9999)).toBe(200)
  })

  it('returns 1 for 1', () => {
    expect(clampLimit(1)).toBe(1)
  })

  it('returns default for 0 or negative', () => {
    expect(clampLimit(0)).toBe(20)
    expect(clampLimit(-5)).toBe(20)
  })

  it('respects custom defaults', () => {
    expect(clampLimit(undefined, { defaultLimit: 10 })).toBe(10)
    expect(clampLimit(500, { maxLimit: 50 })).toBe(50)
  })

  it('truncates fractional values', () => {
    expect(clampLimit(5.9)).toBe(5)
  })
})

describe('deriveCreatedAt', () => {
  it('derives createdAtMs and re-derives createdAt ISO from it', () => {
    const fixed = '2024-01-15T12:00:00.000Z'
    const result = deriveCreatedAt(() => fixed)
    expect(result.createdAtMs).toBe(Date.parse(fixed))
    expect(result.createdAt).toBe(new Date(Date.parse(fixed)).toISOString())
  })

  it('throws on unparseable timestamp', () => {
    expect(() => deriveCreatedAt(() => 'not-a-date')).toThrow(RangeError)
  })
})
