/* eslint-disable @typescript-eslint/no-shadow */
import AsyncStorage from '@react-native-async-storage/async-storage'
import {App} from '@yoroi/types'

import {parseSafe} from '../../utils/parsers'
import {mountAsyncMultiStorage, mountAsyncStorage} from './async-storage'

describe('prefixed storage', () => {
  beforeEach(() => AsyncStorage.clear())

  it('getAllKeys, setItem, getItem, removeItem, clear', async () => {
    const storage = mountAsyncStorage({path: '/'})
    await storage
      .getAllKeys()
      .then((keys: ReadonlyArray<string>) => expect(keys).toEqual([]))

    await storage.setItem('item1', item1)
    expect(await storage.getItem('item1')).toEqual(item1)
    await storage.setItem('item2', item2)
    expect(await storage.getItem('item2')).toEqual(item2)
    expect(await storage.getItem('does not exist')).toEqual(null)
    expect(await storage.getAllKeys()).toEqual(['item1', 'item2'])

    await storage.removeItem('item1')
    expect(await storage.getItem('item1')).toEqual(null)
    expect(await storage.getAllKeys()).toEqual(['item2'])

    await storage.clear()
    expect(await storage.getAllKeys()).toEqual([])
  })

  it('getAllKeys, setItem, getItem, removeItem, clear, with prefix', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})
    const storage = rootStorage.join('prefix/')
    expect(await storage.getAllKeys()).toEqual([])

    await storage.setItem('item1', item1)
    expect(await storage.getItem('item1')).toEqual(item1)
    await storage.setItem('item2', item2)
    expect(await storage.getItem('item2')).toEqual(item2)
    expect(await storage.getItem('does not exist')).toEqual(null)
    expect(await storage.getAllKeys()).toEqual(['item1', 'item2'])

    await storage.removeItem('item1')
    expect(await storage.getItem('item1')).toEqual(null)
    expect(await storage.getAllKeys()).toEqual(['item2'])

    await storage.clear()
    expect(await storage.getAllKeys()).toEqual([])
  })

  it('getAllKeys, multiSet, multiGet, multiRemove', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})
    const storage = rootStorage.join('prefix/')

    await storage.multiSet<any>([
      ['item1', item1],
      ['item2', item2],
    ])
    const keys = await storage.getAllKeys()
    expect(keys).toEqual(['item1', 'item2'])

    const items1 = await storage.multiGet(['item1', 'item2'])
    expect(items1).toEqual([
      ['item1', {a: 123, b: 234}],
      ['item2', {c: 123, d: 234}],
    ])

    await storage.multiRemove(['item1', 'item2'])
    const items2 = await storage.multiGet(['item1', 'item2'])
    expect(items2).toEqual([
      ['item1', null],
      ['item2', null],
    ])

    await storage
      .getAllKeys()
      .then((keys: ReadonlyArray<string>) => expect(keys).toEqual([]))
  })

  it('getAllKeys', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})

    const storage1 = rootStorage.join('prefix/1/')
    expect(await storage1.getAllKeys()).toEqual([])
    await storage1.setItem('key1', item1)
    await storage1.setItem('key2', item2)
    expect(await storage1.getAllKeys()).toEqual(['key1', 'key2'])

    const storage2 = rootStorage.join('prefix/2/3/')
    expect(await storage2.getAllKeys()).toEqual([])
    await storage2.setItem('key1', item1)
    await storage2.setItem('key2', item2)
    expect(await storage2.getAllKeys()).toEqual(['key1', 'key2'])

    const storage = rootStorage.join('prefix/')
    expect(await storage.getAllKeys()).toEqual([])
    await storage.setItem('key1', item1)
    await storage.setItem('key2', item2)
    expect(await storage.getAllKeys()).toEqual(['key1', 'key2'])
  })

  it('join', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})

    const root = rootStorage.join('/')
    await root.setItem('key1', item1)
    await root.setItem('key2', item2)
    expect(await root.getAllKeys()).toEqual(['key1', 'key2'])

    const storage = root.join('dir/')
    expect(await storage.getAllKeys()).toEqual([])

    await storage.setItem('item1', item1)
    expect(await storage.getItem('item1')).toEqual(item1)
    await storage.setItem('item2', item2)
    expect(await storage.getItem('item2')).toEqual(item2)
    expect(await storage.getItem('does not exist')).toEqual(null)
    expect(await storage.getAllKeys()).toEqual(['item1', 'item2'])

    await storage.removeItem('item1')
    expect(await storage.getItem('item1')).toEqual(null)
    expect(await storage.getAllKeys()).toEqual(['item2'])

    await storage.clear()
    expect(await storage.getAllKeys()).toEqual([])
  })

  describe('stringify/parse', () => {
    it('getItem, setItem', async () => {
      const storage = mountAsyncStorage({path: '/'})
      const item = 'text'
      const storedItem = 'item123'

      await storage.setItem('item', item, (data: unknown) => {
        expect(data).toBe(item)
        return storedItem
      }) // overrides JSON.stringify

      const parsedResult = await storage.getItem('item', (data: unknown) => {
        expect(data).toBe(storedItem)
        return item
      }) // overrides JSON.parse

      expect(parsedResult).toBe(item)
    })

    it('multiGet, multiSet', async () => {
      const storage = mountAsyncStorage({path: '/'})
      const item1 = 'item1'
      const storedItem1 = `${item1}-modified`
      const item2 = 'item2'
      const storedItem2 = `${item2}-modified`
      const tuples: [string, unknown][] = [
        ['item1', item1],
        ['item2', item2],
      ]

      await storage.multiSet<any>(tuples, (data: unknown) => {
        expect([item1, item2]).toContain(data)
        return `${data}-modified`
      }) // overrides JSON.stringify

      const parsedResult = await storage.multiGet(
        ['item1', 'item2'],
        (data: string | null) => {
          expect([storedItem1, storedItem2]).toContain(data)
          return data?.slice(0, 5)
        },
      ) // overrides JSON.parse

      expect(parsedResult).toEqual(tuples)
    })
  })

  it('clears sub-storage', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})
    const storage1 = rootStorage.join('1/')
    const storage2 = storage1.join('2/')
    const storage3 = storage2.join('3/')

    await rootStorage.setItem('a', '000')
    await rootStorage.setItem('1', '000')
    await storage1.setItem('b', 111)
    await storage2.setItem('c', 222)
    await storage3.setItem('d', 333)

    expect(await snapshot()).toEqual({
      // folder named "1"
      '/1/2/3/d': 333,
      '/1/2/c': 222,
      '/1/b': 111,

      // file named "1"
      '/1': '000',
      '/a': '000',
    })

    expect(await storage1.getAllKeys()).toEqual(['b'])

    await storage1.clear()

    expect(await snapshot()).toEqual({
      '/a': '000',
      '/1': '000',
    })
  })

  it('removeFolder', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})
    const storage1 = rootStorage.join('1/')
    const storage2 = storage1.join('2/')
    const storage3 = storage2.join('3/')

    await rootStorage.setItem('a', '000')
    await rootStorage.setItem('1', '000')
    await storage1.setItem('b', 111)
    await storage2.setItem('c', 222)
    await storage3.setItem('d', 333)

    expect(await snapshot()).toEqual({
      // folder named "1"
      '/1/2/3/d': 333,
      '/1/2/c': 222,
      '/1/b': 111,

      // root folder
      '/1': '000',
      '/a': '000',
    })

    await storage2.removeFolder('3/')

    expect(await snapshot()).toEqual({
      '/1/2/c': 222,
      '/1/b': 111,

      '/1': '000',
      '/a': '000',
    })

    await rootStorage.removeFolder('1/')

    expect(await snapshot()).toEqual({
      '/1': '000',
      '/a': '000',
    })
  })
})

describe('multi storage', () => {
  beforeEach(() => AsyncStorage.clear())

  it('saveMany, readAll, readMany, keys, and clear providing the serializers', async () => {
    const storage = mountAsyncMultiStorage(options)
    await storage.saveMany([item3, item4])

    const readItems = await storage.readAll()
    expect(readItems).toEqual([
      ['1', item3],
      ['2', item4],
    ])

    const readItem = await storage.readMany(['1'])
    expect(readItem).toEqual([['1', item3]])

    const keys = await storage.getAllKeys()
    expect(keys).toEqual(['1', '2'])

    await storage.clear()

    const emptyKeys = await storage.getAllKeys()
    expect(emptyKeys).toEqual([])
  })
  it('saveMany, readAll, readMany, keys, and clear default serializers / key as extractor', async () => {
    const rootStorage = mountAsyncStorage({path: '/'})
    const storage = mountAsyncMultiStorage({
      storage: rootStorage.join('multiStorage/'),
      dataFolder: 'dataFolder/',
      keyExtractor: 'id',
    })
    await storage.saveMany([item3, item4])

    const readItems = await storage.readAll()
    expect(readItems).toEqual([
      ['1', item3],
      ['2', item4],
    ])

    storage.removeMany(['2'])

    const keys = await storage.getAllKeys()
    expect(keys).toEqual(['1'])

    const readItem = await storage.readMany(['1'])
    expect(readItem).toEqual([['1', item3]])

    await storage.clear()

    const emptyKeys = await storage.getAllKeys()
    expect(emptyKeys).toEqual([])
  })
})

const options: App.MultiStorageOptions<any> = {
  storage: mountAsyncStorage({path: '/'}).join('multiStorage/'),
  dataFolder: 'dataFolder/',
  keyExtractor: (item: any) => item.id,
  serializer: JSON.stringify,
  deserializer: (data: any) => JSON.parse(data as string),
}

const item1 = {a: 123, b: 234}
const item2 = {c: 123, d: 234}
const item3 = {id: 1, a: 123, b: 234}
const item4 = {id: '2', c: 123, d: 234}

const snapshot = async () => {
  const keys = await AsyncStorage.getAllKeys()
  const entries = await AsyncStorage.multiGet(keys).then((entries) =>
    entries.map(([key, value]) => [key, parseSafe(value)]),
  )

  return Object.fromEntries(entries)
}
