import { equal, match, notEqual, ok, throws } from 'node:assert'
import { after, before, describe, test } from 'node:test'

import * as browser from '../index.browser.js'
import * as node from '../index.js'

for (let type of ['node', 'browser']) {
  let { customAlphabet, customRandom, nanoid, random, urlAlphabet } =
    type === 'node' ? node : browser

  describe(type, () => {
    if (type === 'browser') {
      before(() => {
        Object.defineProperty(global, 'crypto', {
          configurable: true,
          value: {
            getRandomValues(array) {
              for (let i = 0; i < array.length; i++) {
                array[i] = Math.floor(Math.random() * 256)
              }
              return array
            }
          }
        })
      })

      after(() => {
        Object.defineProperty(global, 'crypto', { value: undefined })
      })
    }

    test('is ready for 0 size', () => {
      equal(nanoid(0), '')
    })

    test(`generates URL-friendly IDs`, () => {
      for (let i = 0; i < 100; i++) {
        let id = nanoid()
        equal(id.length, 21)
        equal(typeof id, 'string')
        for (let char of id) {
          match(urlAlphabet, new RegExp(char, 'g'))
        }
      }
    })

    test(`changes ID length`, () => {
      equal(nanoid(10).length, 10)
    })

    test(`generates large IDs`, () => {
      let id = nanoid(1000)
      equal(id.length, 1000)
      for (let char of id) {
        match(urlAlphabet, new RegExp(char, 'g'))
      }
    })

    test(`generates IDs bigger than pool size`, () => {
      let id = nanoid(70000)
      equal(id.length, 70000)
      for (let char of id) {
        match(urlAlphabet, new RegExp(char, 'g'))
      }
    })

    test(`accepts string`, () => {
      equal(nanoid('10').length, 10)
    })

    test(`has no collisions`, () => {
      let used = {}
      for (let i = 0; i < 50 * 1000; i++) {
        let id = nanoid()
        equal(used[id], undefined)
        used[id] = true
      }
    })

    test(`avoids pool pollution, infinite loop`, () => {
      nanoid(2.1)
      let second = nanoid()
      let third = nanoid()
      notEqual(second, third)
    })

    test('throws on negative or too big ID length', () => {
      throws(() => {
        nanoid(2147483648)
      }, /Wrong ID size|Invalid typed array length/)
      throws(() => {
        nanoid(-10)
      }, /Wrong ID size|Invalid typed array length/)
      let a = nanoid()
      let b = nanoid()
      notEqual(a, b)
    })

    test(`has flat distribution`, () => {
      let COUNT = 100 * 1000
      let LENGTH = nanoid().length

      let chars = {}
      for (let i = 0; i < COUNT; i++) {
        let id = nanoid()
        for (let char of id) {
          if (!chars[char]) chars[char] = 0
          chars[char] += 1
        }
      }

      equal(Object.keys(chars).length, urlAlphabet.length)

      let max = 0
      let min = Number.MAX_SAFE_INTEGER
      for (let k in chars) {
        let distribution = (chars[k] * urlAlphabet.length) / (COUNT * LENGTH)
        if (distribution > max) max = distribution
        if (distribution < min) min = distribution
      }
      ok(max - min <= 0.05)
    })

    if (type === 'node') {
      test(`prevent collision with proxy number`, () => {
        let makeProxyNumberToReproducePreviousID = () => {
          let step = 0
          return {
            valueOf() {
              // "if (!pool || pool.length < bytes) {"
              if (step === 0) {
                step++
                return 0
              }
              // "} else if (poolOffset + bytes > pool.length) {"
              if (step === 1) {
                step++
                return -Infinity
              }
              // "poolOffset += bytes"
              if (step === 2) {
                step++
                return 0
              }

              return 21
            }
          }
        }

        let id1 = nanoid()
        let id2 = nanoid(makeProxyNumberToReproducePreviousID())
        notEqual(id1, id2)
      })

      test(`recovers from crypto errors`, () => {
        nanoid(1)
        let original = crypto.getRandomValues
        crypto.getRandomValues = () => {
          throw new Error('Entropy failure')
        }
        try {
          throws(() => {
            nanoid(65536)
          }, /Entropy failure/)
          throws(() => {
            random(131072)
          }, /Entropy failure/)
        } finally {
          crypto.getRandomValues = original
        }
        equal(nanoid().length, 21)
        equal(random(10).length, 10)
      })
    }

    describe('customAlphabet', () => {
      test(`has options`, () => {
        let nanoidA = customAlphabet('a', 5)
        equal(nanoidA(), 'aaaaa')
      })

      test(`has flat distribution`, () => {
        let COUNT = 50 * 1000
        let LENGTH = 30
        let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
        let nanoid2 = customAlphabet(ALPHABET, LENGTH)

        let chars = {}
        for (let i = 0; i < COUNT; i++) {
          let id = nanoid2()
          for (let char of id) {
            if (!chars[char]) chars[char] = 0
            chars[char] += 1
          }
        }

        equal(Object.keys(chars).length, ALPHABET.length)

        let max = 0
        let min = Number.MAX_SAFE_INTEGER
        for (let k in chars) {
          let distribution = (chars[k] * ALPHABET.length) / (COUNT * LENGTH)
          if (distribution > max) max = distribution
          if (distribution < min) min = distribution
        }
        ok(max - min <= 0.05)
      })

      test(`changes size`, () => {
        let nanoidA = customAlphabet('a')
        equal(nanoidA(10), 'aaaaaaaaaa')
      })

      test(`is ready for 0 size`, () => {
        equal(customAlphabet('abc')(0), '')
        equal(customAlphabet('abc', 0)(0), '')
        equal(customAlphabet('')(0), '')
        equal(customAlphabet('', 0)(0), '')
      })

      test(`avoids pool pollution, infinite loop`, () => {
        let ALPHABET = 'abcdefghijklmnopqrstuvwxyz'
        let nanoid2 = customAlphabet(ALPHABET)
        nanoid2(2.1)
        let second = nanoid2()
        let third = nanoid2()
        notEqual(second, third)
      })

      test(`supports multi-byte characters`, () => {
        let ALPHABET = 'абвгд'
        let nanoid2 = customAlphabet(ALPHABET, 10)
        for (let i = 0; i < 100; i++) {
          let id = nanoid2()
          equal(id.length, 10)
          for (let char of id) {
            ok(ALPHABET.includes(char))
          }
        }
      })

      test(`supports alphabets bigger than 256 symbols`, () => {
        equal(customAlphabet('a'.repeat(300))(0), '')
      })

      test(`supports non-string alphabets`, () => {
        let ALPHABET = ['a', 'b', 'c', 'd']
        let nanoid2 = customAlphabet(ALPHABET, 10)
        let id = nanoid2()
        equal(id.length, 10)
        for (let char of id) {
          ok(ALPHABET.includes(char))
        }
      })

      if (type === 'node') {
        test(`does not fall in infinite loop`, () => {
          equal(customAlphabet('abc')(0), '')
          equal(customAlphabet('abc', 0)(0), '')
        })
      }
    })

    describe('customRandom', () => {
      test(`supports generator`, () => {
        let sequence = [2, 255, 3, 7, 7, 7, 7, 7, 0, 1]
        function fakeRandom(size) {
          let bytes = []
          for (let i = 0; i < size; i += sequence.length) {
            bytes = bytes.concat(sequence.slice(0, size - i))
          }
          return bytes
        }
        let nanoid4 = customRandom('abcde', 4, fakeRandom)
        let nanoid18 = customRandom('abcde', 18, fakeRandom)
        equal(nanoid4(), 'cccc')
        equal(nanoid18(), 'acccccdcbacccccdcb')
      })

      test(`is ready for 0 size`, () => {
        let nanoid0 = customRandom('abc', 5, size => new Uint8Array(size))
        equal(nanoid0(0), '')
        equal(customRandom('', 5, size => new Uint8Array(size))(0), '')
      })

      test(`supports power-of-two alphabets`, () => {
        let sequence = [2, 255, 3, 7]
        function fakeRandom(size) {
          let bytes = []
          for (let i = 0; i < size; i += sequence.length) {
            bytes = bytes.concat(sequence.slice(0, size - i))
          }
          return bytes
        }
        let nanoid4 = customRandom('abcd', 4, fakeRandom)
        equal(nanoid4(), 'dddc')
        equal(nanoid4(0), '')
      })
    })

    describe('urlAlphabet', () => {
      test(`is string`, () => {
        equal(typeof urlAlphabet, 'string')
      })

      test(`has 64 symbols`, () => {
        equal(urlAlphabet.length, 64)
      })

      test(`has no duplicates`, () => {
        for (let i = 0; i < urlAlphabet.length; i++) {
          equal(urlAlphabet.lastIndexOf(urlAlphabet[i]), i)
        }
      })
    })

    describe('random', () => {
      test(`throws on negative size`, () => {
        throws(() => {
          random(-1)
        }, /Wrong ID size|Invalid typed array length/)
      })

      test(`generates small random buffers`, () => {
        for (let i = 0; i < urlAlphabet.length; i++) {
          equal(random(10).length, 10)
        }
      })

      test(`generates random buffers`, () => {
        let numbers = {}
        let bytes = random(1000)
        equal(bytes.length, 1000)
        for (let byte of bytes) {
          if (!numbers[byte]) numbers[byte] = 0
          numbers[byte] += 1
          equal(typeof byte, 'number')
          ok(byte <= 255)
          ok(byte >= 0)
        }
      })
    })
  })
}
