import {describe, expect, test} from 'bun:test'

import {createKeyedSnapshot} from '@/lib/keyed-snapshot'
import {type Snapshot} from '@/lib/snapshot-cache'

// in-memory entries snapshot, no dedupe (dedupe is snapshot-cache's job) so a
// write count reflects createKeyedSnapshot's own decisions
const fakeEntries = <P>(
  initial?: [string, P][],
): Snapshot<[string, P][]> & {
  current: () => [string, P][] | undefined
  writes: () => number
} => {
  let value = initial
  let writes = 0
  return {
    read: () => value,
    write: (next) => {
      writes++
      value = next
    },
    clear: () => {
      value = undefined
    },
    current: () => value,
    writes: () => writes,
  }
}

// a slot-like value to exercise project (data-only) + skip-undefined
type Cell = {data: string | undefined}
const ids = (entries?: [string, unknown][]) =>
  entries?.map(([id]) => id)

const keyed = (entries: Snapshot<[string, string][]>, max = 30) =>
  createKeyedSnapshot<Cell, string>(entries, {
    max,
    project: (cell) => cell.data,
    revive: (data) => ({data}),
  })

describe('createKeyedSnapshot', () => {
  test('seed revives persisted entries into values', () => {
    const entries = fakeEntries<string>([
      ['a', 'A'],
      ['b', 'B'],
    ])
    expect(keyed(entries).seed()).toEqual({
      a: {data: 'A'},
      b: {data: 'B'},
    })
  })

  test('seed returns empty when nothing persisted', () => {
    expect(keyed(fakeEntries<string>()).seed()).toEqual({})
  })

  test('sync persists a newly-changed id', () => {
    const entries = fakeEntries<string>()
    const snap = keyed(entries)
    snap.seed()
    snap.sync({a: {data: 'A'}}, {})
    expect(entries.current()).toEqual([['a', 'A']])
  })

  test('sync skips a value whose projection is undefined', () => {
    const entries = fakeEntries<string>()
    const snap = keyed(entries)
    snap.seed()
    // a slot that is loading with no data yet must never hit disk
    snap.sync({a: {data: undefined}}, {})
    expect(entries.writes()).toBe(0)
    expect(entries.current()).toBeUndefined()
  })

  test('sync is a no-op when no reference changed', () => {
    const entries = fakeEntries<string>()
    const snap = keyed(entries)
    snap.seed()
    const state = {a: {data: 'A'}}
    snap.sync(state, {})
    expect(entries.writes()).toBe(1)
    // a fan-out commit that touched nothing this cache holds: no disk hit
    snap.sync(state, state)
    expect(entries.writes()).toBe(1)
  })

  test('sync persists a removal so a deleted id never reseeds', () => {
    const entries = fakeEntries<string>([['a', 'A']])
    const snap = keyed(entries)
    const seeded = snap.seed()
    const {a: _removed, ...rest} = seeded
    snap.sync(rest, seeded)
    expect(entries.current()).toEqual([])
  })

  test('sync ignores a removal of a never-persisted id', () => {
    const entries = fakeEntries<string>()
    const snap = keyed(entries)
    snap.seed()
    // a loading-only slot (projection undefined) came and went: no disk hit
    const prev = {a: {data: undefined}}
    snap.sync({}, prev)
    expect(entries.writes()).toBe(0)
  })

  test('caps to the most-recently-touched ids, evicting the coldest', () => {
    const entries = fakeEntries<string>()
    const snap = keyed(entries, 2)
    const a = {data: 'A'}
    const b = {data: 'B'}
    const c = {data: 'C'}
    snap.seed()
    snap.sync({a}, {})
    snap.sync({a, b}, {a})
    snap.sync({a, b, c}, {a, b})
    // a is the coldest and falls out; newest first
    expect(ids(entries.current())).toEqual(['c', 'b'])
  })

  test('re-touching an evicted id brings it back to the front', () => {
    const entries = fakeEntries<string>()
    const snap = keyed(entries, 2)
    const a = {data: 'A'}
    const b = {data: 'B'}
    const c = {data: 'C'}
    snap.seed()
    snap.sync({a}, {})
    snap.sync({a, b}, {a})
    snap.sync({a, b, c}, {a, b}) // -> [c, b], a evicted
    snap.sync({a: {data: 'A2'}, b, c}, {a, b, c}) // touch a
    expect(ids(entries.current())).toEqual(['a', 'c'])
    expect(entries.current()).toEqual([
      ['a', 'A2'],
      ['c', 'C'],
    ])
  })

  test('seed order carries into eviction', () => {
    const entries = fakeEntries<string>([
      ['a', 'A'],
      ['b', 'B'],
    ])
    const snap = keyed(entries, 2)
    // seed sets order [a, b]; reuse its refs so a/b read as unchanged, exactly
    // as a zustand commit preserves untouched slot refs
    const seeded = snap.seed()
    snap.sync({...seeded, c: {data: 'C'}}, seeded)
    // b was older than a in seed order, so b is evicted
    expect(ids(entries.current())).toEqual(['c', 'a'])
  })

  test('clear wipes disk and resets order', () => {
    const entries = fakeEntries<string>([['a', 'A']])
    const snap = keyed(entries)
    snap.seed()
    snap.clear()
    expect(entries.current()).toBeUndefined()
    expect(snap.seed()).toEqual({})
  })
})
