import { it, expect, vi, beforeEach, afterEach, describe } from 'vitest'
import { createCache, namespace, type CacheBackend } from './index.js'

function createFakeBackend(): CacheBackend & {
  store: Map<string, { value: string; expiresAt?: number }>
} {
  const store = new Map<string, { value: string; expiresAt?: number }>()
  return {
    store,
    async get(key) {
      const entry = store.get(key)
      if (!entry) return undefined
      if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
        store.delete(key)
        return undefined
      }
      return entry.value
    },
    async set(key, value, ttlSeconds) {
      const expiresAt =
        ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : undefined
      store.set(key, { value, expiresAt })
    },
    async del(key) {
      store.delete(key)
    },
  }
}

it('set + get round-trips a typed object; absent key → undefined', async () => {
  const cache = createCache(createFakeBackend())
  await cache.set('user:1', { name: 'Ada', n: 42 })
  await expect(cache.get<{ name: string; n: number }>('user:1')).resolves.toEqual({
    name: 'Ada',
    n: 42,
  })
  await expect(cache.get('missing')).resolves.toBeUndefined()
})

it('corrupt stored string → get returns undefined without throwing', async () => {
  const backend = createFakeBackend()
  backend.store.set('bad', { value: 'not-json{{{' })
  const cache = createCache(backend)
  await expect(cache.get<{ x: number }>('bad')).resolves.toBeUndefined()
})

it('set(key, undefined) behaves as del', async () => {
  const cache = createCache(createFakeBackend())
  await cache.set('k', { ok: true })
  await cache.set('k', undefined)
  await expect(cache.get('k')).resolves.toBeUndefined()
})

it('del removes a key', async () => {
  const cache = createCache(createFakeBackend())
  await cache.set('k', 'v')
  await cache.del('k')
  await expect(cache.get('k')).resolves.toBeUndefined()
})

it('single-flight: N concurrent cold getOrSet invokes loader exactly once', async () => {
  const cache = createCache(createFakeBackend())
  const loader = vi.fn(async () => ({ id: 7 }))
  const results = await Promise.all(
    Array.from({ length: 10 }, () => cache.getOrSet('cold', loader)),
  )
  expect(loader).toHaveBeenCalledTimes(1)
  for (const value of results) {
    expect(value).toEqual({ id: 7 })
  }
})

it('rejected loader clears in-flight so a later getOrSet re-invokes', async () => {
  const cache = createCache(createFakeBackend())
  const loader = vi
    .fn()
    .mockRejectedValueOnce(new Error('transient'))
    .mockResolvedValueOnce({ ok: true })

  await expect(cache.getOrSet('retry', loader)).rejects.toThrow('transient')
  await expect(cache.getOrSet('retry', loader)).resolves.toEqual({ ok: true })
  expect(loader).toHaveBeenCalledTimes(2)
})

describe('ttl expiry', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date('2026-06-16T12:00:00.000Z'))
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('expired ttl → get miss; getOrSet reloads', async () => {
    const cache = createCache(createFakeBackend())
    const loader = vi.fn(async () => 'fresh')
    await cache.set('ttl-key', 'stale', { ttl: 1 })
    vi.advanceTimersByTime(1001)
    await expect(cache.get('ttl-key')).resolves.toBeUndefined()
    await expect(cache.getOrSet('ttl-key', loader)).resolves.toBe('fresh')
    expect(loader).toHaveBeenCalledTimes(1)
  })
})

it('namespace builds a versioned key prefix', () => {
  expect(namespace('tsi:v1')('deal:7')).toBe('tsi:v1:deal:7')
})
