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

import {
  chunk,
  findById,
  groupBy,
  moveItem,
  patchById,
  pickPresent,
  removeById,
  replaceById,
} from './collections'

describe('pickPresent', () => {
  const map = new Map([
    ['a', 1],
    ['b', 2],
  ])

  test('returns values in key order, skipping missing keys', () => {
    expect(pickPresent(map, ['b', 'z', 'a'])).toEqual([2, 1])
  })

  test('keeps array values intact', () => {
    const lists = new Map([['a', [1, 2]]])
    expect(pickPresent(lists, ['a', 'b'])).toEqual([[1, 2]])
  })
})

describe('by-id list edits', () => {
  const items = [
    {id: 'a', n: 1},
    {id: 'b', n: 2},
  ]

  test('findById returns the match or undefined', () => {
    expect(findById(items, 'b')).toEqual({id: 'b', n: 2})
    expect(findById(items, 'z')).toBeUndefined()
  })

  test('replaceById swaps the match and keeps other identities', () => {
    const out = replaceById(items, 'b', {id: 'b', n: 9})
    expect(out).toEqual([
      {id: 'a', n: 1},
      {id: 'b', n: 9},
    ])
    // untouched row keeps its reference; the input is not mutated
    expect(out[0]).toBe(items[0])
    expect(items[1]).toEqual({id: 'b', n: 2})
  })

  test('replaceById is a no-op copy when absent', () => {
    expect(replaceById(items, 'z', {id: 'z', n: 0})).toEqual(items)
  })

  test('patchById maps only the match', () => {
    expect(
      patchById(items, 'a', (i) => ({...i, n: i.n + 10})),
    ).toEqual([
      {id: 'a', n: 11},
      {id: 'b', n: 2},
    ])
  })

  test('removeById drops the match, no-op when absent', () => {
    expect(removeById(items, 'a')).toEqual([{id: 'b', n: 2}])
    expect(removeById(items, 'z')).toEqual(items)
  })
})

describe('groupBy', () => {
  test('groups values by key, preserving input order', () => {
    const out = groupBy(
      [
        {k: 'a', v: 1},
        {k: 'b', v: 2},
        {k: 'a', v: 3},
      ],
      (r) => r.k,
      (r) => r.v,
    )
    expect([...out.keys()]).toEqual(['a', 'b'])
    expect(out.get('a')).toEqual([1, 3])
    expect(out.get('b')).toEqual([2])
  })

  test('returns an empty map for no rows', () => {
    expect(
      groupBy(
        [],
        (r) => r,
        (r) => r,
      ).size,
    ).toBe(0)
  })
})

describe('chunk', () => {
  test('splits into fixed-size batches', () => {
    expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]])
  })

  test('returns one batch when smaller than size', () => {
    expect(chunk([1, 2], 5)).toEqual([[1, 2]])
  })

  test('returns empty for no items', () => {
    expect(chunk([], 3)).toEqual([])
  })
})

describe('moveItem', () => {
  test('moves an item forward', () => {
    expect(moveItem(['a', 'b', 'c', 'd'], 0, 2)).toEqual([
      'b',
      'c',
      'a',
      'd',
    ])
  })

  test('moves an item backward', () => {
    expect(moveItem(['a', 'b', 'c', 'd'], 3, 1)).toEqual([
      'a',
      'd',
      'b',
      'c',
    ])
  })

  test('clamps the target index into range', () => {
    expect(moveItem(['a', 'b', 'c'], 0, 99)).toEqual(['b', 'c', 'a'])
    expect(moveItem(['a', 'b', 'c'], 2, -5)).toEqual(['c', 'a', 'b'])
  })

  test('returns a copy when from is out of range', () => {
    const items = ['a', 'b']
    const out = moveItem(items, 5, 0)
    expect(out).toEqual(items)
    expect(out).not.toBe(items)
  })

  test('does not mutate the input', () => {
    const items = ['a', 'b', 'c']
    moveItem(items, 0, 2)
    expect(items).toEqual(['a', 'b', 'c'])
  })
})
