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

import {
  canLoadMore,
  createPagedStore,
  emptyPaged,
  pageParams,
  SNAPSHOT_MAX_ITEMS,
  stepPaged,
  type Page,
  type PagedSlice,
  type PagedSnapshot,
} from '@/lib/paged-resource'
import {type Snapshot} from '@/lib/snapshot-cache'
import {resetAllStores} from '@/lib/store-reset'

type Item = {id: string}

const fakeSnapshot = <T>(
  initial?: T,
): Snapshot<T> & {
  current: () => T | undefined
} => {
  let value = initial
  return {
    read: () => value,
    write: (next) => {
      value = next
    },
    clear: () => {
      value = undefined
    },
    current: () => value,
  }
}

const page = (
  ids: string[],
  nextCursor: string | null,
): Page<Item> => ({
  items: ids.map((id) => ({id})),
  nextCursor,
})

const runStep = async (
  current: PagedSlice<Item>,
  result: Page<Item> | Error,
  mode: 'refresh' | 'more',
  isStale: () => boolean = () => false,
) => {
  let slice = current
  await stepPaged(
    slice,
    () =>
      result instanceof Error
        ? Promise.reject(result)
        : Promise.resolve(result),
    mode,
    (patch) => {
      slice = {...slice, ...patch(slice)}
    },
    isStale,
  )
  return slice
}

describe('stepPaged', () => {
  test('a refresh replaces items and stores the cursor', async () => {
    const seeded = {...emptyPaged<Item>(), items: [{id: 'old'}]}
    const slice = await runStep(
      seeded,
      page(['a', 'b'], 'cur'),
      'refresh',
    )
    expect(slice.items.map((i) => i.id)).toEqual(['a', 'b'])
    expect(slice.cursor).toBe('cur')
    expect(slice.reachedEnd).toBe(false)
    expect(slice.loading).toBe(false)
  })

  test('a loadMore appends and a null cursor marks the end', async () => {
    const seeded = {
      ...emptyPaged<Item>(),
      items: [{id: 'a'}],
      cursor: 'cur',
    }
    const slice = await runStep(seeded, page(['b'], null), 'more')
    expect(slice.items.map((i) => i.id)).toEqual(['a', 'b'])
    expect(slice.reachedEnd).toBe(true)
  })

  test('a stale response never commits', async () => {
    const seeded = {...emptyPaged<Item>(), items: [{id: 'fresh'}]}
    const slice = await runStep(
      seeded,
      page(['stale'], null),
      'refresh',
      () => true,
    )
    expect(slice.items.map((i) => i.id)).toEqual(['fresh'])
    // the loading flag set before the fetch stays for the newer run to clear
    expect(slice.loading).toBe(true)
  })

  test('a failure surfaces the error and clears loading', async () => {
    const slice = await runStep(
      emptyPaged<Item>(),
      new Error('boom'),
      'refresh',
    )
    expect(slice.error).not.toBeNull()
    expect(slice.loading).toBe(false)
  })
})

describe('canLoadMore', () => {
  test('requires a settled slice with a cursor', () => {
    expect(canLoadMore(emptyPaged())).toBe(false)
    expect(canLoadMore({...emptyPaged(), cursor: 'c'})).toBe(true)
    expect(
      canLoadMore({...emptyPaged(), cursor: 'c', loading: true}),
    ).toBe(false)
    expect(
      canLoadMore({...emptyPaged(), cursor: 'c', loadingMore: true}),
    ).toBe(false)
    expect(
      canLoadMore({...emptyPaged(), cursor: 'c', reachedEnd: true}),
    ).toBe(false)
  })
})

describe('pageParams', () => {
  test('omits the cursor on the first page', () => {
    expect(pageParams()).toEqual({limit: '20'})
    expect(pageParams('abc')).toEqual({limit: '20', cursor: 'abc'})
  })
})

describe('createPagedStore', () => {
  test('an older refresh cannot clobber a newer one', async () => {
    const resolvers: ((p: Page<Item>) => void)[] = []
    const store = createPagedStore<Item>(
      () => new Promise((resolve) => resolvers.push(resolve)),
    )
    const first = store.getState().load()
    const second = store.getState().load()
    resolvers[1]!(page(['new'], null))
    await second
    resolvers[0]!(page(['old'], null))
    await first
    expect(store.getState().items.map((i) => i.id)).toEqual(['new'])
  })

  test('seeds items, cursor and reachedEnd from a snapshot', () => {
    const store = createPagedStore<Item>(
      () => Promise.resolve(page(['fresh'], null)),
      fakeSnapshot<PagedSnapshot<Item>>({
        items: [{id: 'cached'}],
        cursor: 'cur',
        reachedEnd: false,
      }),
    )
    const state = store.getState()
    expect(state.items.map((i) => i.id)).toEqual(['cached'])
    expect(state.cursor).toBe('cur')
    expect(canLoadMore(state)).toBe(true)
  })

  test('persists a load commit to the snapshot', async () => {
    const snapshot = fakeSnapshot<PagedSnapshot<Item>>()
    const store = createPagedStore<Item>(
      () => Promise.resolve(page(['a'], 'cur')),
      snapshot,
    )
    await store.getState().load()
    expect(snapshot.current()).toEqual({
      items: [{id: 'a'}],
      cursor: 'cur',
      reachedEnd: false,
    })
  })

  test('a failed load leaves the snapshot untouched', async () => {
    const cached = {
      items: [{id: 'cached'}],
      cursor: null,
      reachedEnd: false,
    }
    const snapshot = fakeSnapshot<PagedSnapshot<Item>>(cached)
    const store = createPagedStore<Item>(
      () => Promise.reject(new Error('boom')),
      snapshot,
    )
    await store.getState().load()
    expect(snapshot.current()).toEqual(cached)
    expect(store.getState().items.map((i) => i.id)).toEqual([
      'cached',
    ])
  })

  test('persists optimistic prepend, replace and remove', async () => {
    const snapshot = fakeSnapshot<PagedSnapshot<Item>>()
    const store = createPagedStore<Item>(
      () => Promise.resolve(page(['a'], null)),
      snapshot,
    )
    await store.getState().load()
    store.getState().prepend({id: 'b'})
    expect(snapshot.current()?.items.map((i) => i.id)).toEqual([
      'b',
      'a',
    ])
    store.getState().replace('b', {id: 'c'})
    expect(snapshot.current()?.items.map((i) => i.id)).toEqual([
      'c',
      'a',
    ])
    store.getState().removeItem('c')
    expect(snapshot.current()?.items.map((i) => i.id)).toEqual(['a'])
  })

  test('caps the snapshot and drops the cursor past the cap', async () => {
    const ids = Array.from(
      {length: SNAPSHOT_MAX_ITEMS + 1},
      (_, i) => `i${i}`,
    )
    const snapshot = fakeSnapshot<PagedSnapshot<Item>>()
    const store = createPagedStore<Item>(
      () => Promise.resolve(page(ids, 'cur')),
      snapshot,
    )
    await store.getState().load()
    const persisted = snapshot.current()
    expect(persisted?.items).toHaveLength(SNAPSHOT_MAX_ITEMS)
    // resuming pagination from the old cursor would skip the dropped tail
    expect(persisted?.cursor).toBeNull()
    expect(persisted?.reachedEnd).toBe(false)
  })

  test('reset clears the snapshot', async () => {
    const snapshot = fakeSnapshot<PagedSnapshot<Item>>()
    const store = createPagedStore<Item>(
      () => Promise.resolve(page(['a'], null)),
      snapshot,
    )
    await store.getState().load()
    resetAllStores()
    expect(snapshot.current()).toBeUndefined()
    expect(store.getState().items).toEqual([])
  })
})
