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

import {type Slot} from '@/lib/async-slot'
import {
  createKeyedDetailStore,
  patchData,
  persistKeyedDetail,
  setDetail,
  type KeyedDetail,
} from '@/lib/keyed-resource'
import {type KeyedSnapshot} from '@/lib/keyed-snapshot'
import {resetAllStores} from '@/lib/store-reset'

// a minimal zustand-like store over a KeyedDetail — just enough surface for
// persistKeyedDetail to subscribe and reset
const fakeStore = <T>() => {
  let state: KeyedDetail<T> = {byId: {}}
  const listeners = new Set<
    (s: KeyedDetail<T>, p: KeyedDetail<T>) => void
  >()
  return {
    getState: () => state,
    setState: (partial: Pick<KeyedDetail<T>, 'byId'>) => {
      const prev = state
      state = {...state, ...partial}
      for (const fn of listeners) fn(state, prev)
    },
    subscribe: (
      fn: (s: KeyedDetail<T>, p: KeyedDetail<T>) => void,
    ) => {
      listeners.add(fn)
      return () => listeners.delete(fn)
    },
  }
}

const slot = <T>(data: T): Slot<T> => ({
  data,
  loading: false,
  error: null,
})

describe('patchData', () => {
  test('patches a loaded id in place', () => {
    const byId = setDetail<number>({}, 'a', 1)
    const next = patchData(byId, 'a', (n) => n + 1)
    expect(next['a']?.data).toBe(2)
    expect(next).not.toBe(byId)
  })

  test('no-op when the id is absent or the patch changes nothing', () => {
    const byId = setDetail<number>({}, 'a', 1)
    expect(patchData(byId, 'missing', (n) => n + 1)).toBe(byId)
    expect(patchData(byId, 'a', (n) => n)).toBe(byId)
  })
})

describe('createKeyedDetailStore', () => {
  test('loads each id into its own slot', async () => {
    const store = createKeyedDetailStore(async (id: string) =>
      id.toUpperCase(),
    )
    await store.getState().load('a')
    expect(store.getState().byId['a']?.data).toBe('A')
    expect(store.getState().byId['a']?.loading).toBe(false)
    expect(store.getState().byId['b']).toBeUndefined()
  })

  test('seeds from the snapshot and resets to empty', async () => {
    const log: string[] = []
    const snapshot = {
      seed: () => ({a: slot<string | undefined>('cached')}),
      sync: () => log.push('sync'),
      clear: () => log.push('clear'),
    } satisfies KeyedSnapshot<Slot<string | undefined>>
    const store = createKeyedDetailStore(
      async (id: string) => id.toUpperCase(),
      snapshot,
    )
    expect(store.getState().byId['a']?.data).toBe('cached')

    resetAllStores()
    expect(store.getState().byId).toEqual({})
    expect(log).toContain('clear')
  })
})

describe('persistKeyedDetail', () => {
  test('persists on byId change, resets in the safe order', () => {
    const log: string[] = []
    const snapshot = {
      seed: () => ({}),
      sync: () => log.push('sync'),
      clear: () => log.push('clear'),
    } satisfies KeyedSnapshot<Slot<number | undefined>>
    const guard = {invalidate: () => log.push('invalidate')}
    const store = fakeStore<number>()

    persistKeyedDetail(store, snapshot, guard)

    // a byId commit persists through the snapshot
    store.setState({byId: {a: slot(1)}})
    expect(log).toEqual(['sync'])

    // reset runs in the one safe order: invalidate in-flight loads, empty byId
    // (which the persist subscriber writes through as a sync), then clear disk
    // last — so no bytes of the previous account survive
    log.length = 0
    resetAllStores()
    expect(log).toEqual(['invalidate', 'sync', 'clear'])
  })
})
