/*
Copyright 2018 - 2022 The Alephium Authors
This file is part of the alephium project.

The library is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import {
  web3,
  verifySignedMessage,
  publicKeyFromPrivateKey,
  TOTAL_NUMBER_OF_GROUPS,
  addressFromPublicKey,
  groupOfAddress,
  MessageHasher,
  KeyType,
  Account,
  isGroupedAccount
} from '@alephium/web3'
import {
  deriveHDWalletPrivateKey,
  deriveHDWalletPrivateKeyForGroup,
  deriveSecp256K1PrivateKey,
  deriveSecp256K1PrivateKeyForGroup,
  generateMnemonic,
  HDWallet
} from './hd-wallet'

describe('HD wallet', () => {
  const keyTypes: KeyType[] = ['default', 'bip340-schnorr', 'gl-secp256k1'] as KeyType[]

  beforeAll(() => {
    web3.setCurrentNodeProvider('http://127.0.0.1:22973')
  })

  const testMnemonic =
    'vault alarm sad mass witness property virus style good flower rice alpha viable evidence run glare pretty scout evil judge enroll refuse another lava'

  it('should gerate mnemonic', () => {
    expect(generateMnemonic().split(' ').length).toBe(24)
    expect(generateMnemonic(12).split(' ').length).toBe(12)
    expect(generateMnemonic(24).split(' ').length).toBe(24)
  })

  it('should derive private key based on index for secp256k1', () => {
    expect(deriveSecp256K1PrivateKey(testMnemonic, 0)).toEqual(
      'a642942e67258589cd2b1822c631506632db5a12aabcf413604e785300d762a5'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 1)).toEqual(
      'bd7dd0c4abd3cf8ba2d169c8320a2cc8bc8ab583b0db9a32d4352d6f5b15d037'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 2)).toEqual(
      '93ae1392f36a592aca154ea14e51b791c248beaea1b63117c57cc46d56e5f482'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 3)).toEqual(
      'ec8c4e863e4027d5217c382bfc67bd2638f21d6f956653505229f1d242123a9a'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 0, 'Alephium')).toEqual(
      '62814353f0fac259b448441898f294b17eff73ab1fd6a7fc4b8216f7e039bdce'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 1, 'Alephium')).toEqual(
      'ff8efb234d82f49ece8b34fa9a7250e16879a7a8a222463390159e2e51a1a117'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 2, 'Alephium')).toEqual(
      '9fce4cb835651c8d2aeed1daead8bd04ab314d586e9b8423ee0d7a264cb8608e'
    )
    expect(deriveSecp256K1PrivateKey(testMnemonic, 3, 'Alephium')).toEqual(
      'd8da830fe81dc6be8b2d0cc60dff79412f7b58445ab5463c63d1791b08ec1110'
    )
  })

  it('should derive private key for groups and secp256k1', () => {
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 0, 0, 'Alephium')[0]).toEqual(
      '62814353f0fac259b448441898f294b17eff73ab1fd6a7fc4b8216f7e039bdce'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 1, 0, 'Alephium')[0]).toEqual(
      'f62df0157aec61806d51480425e1f7a4950e13fa9a2de87988ae1d861e09d2ae'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 2, 0, 'Alephium')[0]).toEqual(
      'cae87098274ff447f785bc408a71b3416d6140bd824e623375959cbf43d2a2d5'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 3, 0, 'Alephium')[0]).toEqual(
      '9fce4cb835651c8d2aeed1daead8bd04ab314d586e9b8423ee0d7a264cb8608e'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 0, 100, 'Alephium')[0]).toEqual(
      'c047f716a03c4961d98427d3cf494fe3d5f3fd0b48fe3107699d90a453f2dce6'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 1, 100, 'Alephium')[0]).toEqual(
      '800fc78e24c87c04a76f23b8ab54179ef190f640d9346d3e89cbc3036ee69c9b'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 2, 100, 'Alephium')[0]).toEqual(
      '51df72c50e1b392805c3f530d2dece7e0c1842f827a579f7cd24907a6c4ff533'
    )
    expect(deriveSecp256K1PrivateKeyForGroup(testMnemonic, 3, 100, 'Alephium')[0]).toEqual(
      '04528b7736eab20cbde90f0d2f0cb3a99481dfe92664757646077ae7851e4314'
    )
  })

  function getRandomInt(max) {
    return Math.floor(Math.random() * max)
  }

  it('should derive private key for groups', () => {
    const startIndex = getRandomInt(1024)

    keyTypes.forEach((keyType) => {
      Array.from(Array(TOTAL_NUMBER_OF_GROUPS).keys()).forEach((group) => {
        const [privateKey, index] = deriveHDWalletPrivateKeyForGroup(testMnemonic, group, keyType, startIndex)
        expect(deriveHDWalletPrivateKey(testMnemonic, keyType, index)).toBe(privateKey)
        const address = addressFromPublicKey(publicKeyFromPrivateKey(privateKey, keyType), keyType)
        expect(groupOfAddress(address)).toBe(group)
      })
    })
  })

  it('should derive account', () => {
    function groupOf(account: Account): number {
      return isGroupedAccount(account) ? account.group : groupOfAddress(account.address)
    }

    keyTypes.forEach(async (keyType) => {
      const wallet = new HDWallet({ mnemonic: testMnemonic, passphrase: 'Alephium', keyType })
      const account0 = wallet.deriveAndAddNewAccount(0)
      const account1 = wallet.deriveAndAddNewAccount(1)
      const account2 = wallet.deriveAndAddNewAccount(2)
      const account3 = wallet.deriveAndAddNewAccount(3)
      expect(groupOf(account0)).toBe(0)
      expect(groupOf(account1)).toBe(1)
      expect(groupOf(account2)).toBe(2)
      expect(groupOf(account3)).toBe(3)
      expect(account0.publicKey).toEqual(
        publicKeyFromPrivateKey(deriveHDWalletPrivateKeyForGroup(testMnemonic, 0, keyType, 0, 'Alephium')[0], keyType)
      )
      expect(account1.publicKey).toEqual(
        publicKeyFromPrivateKey(deriveHDWalletPrivateKeyForGroup(testMnemonic, 1, keyType, 0, 'Alephium')[0], keyType)
      )
      expect(account2.publicKey).toEqual(
        publicKeyFromPrivateKey(deriveHDWalletPrivateKeyForGroup(testMnemonic, 2, keyType, 0, 'Alephium')[0], keyType)
      )
      expect(account3.publicKey).toEqual(
        publicKeyFromPrivateKey(deriveHDWalletPrivateKeyForGroup(testMnemonic, 3, keyType, 0, 'Alephium')[0], keyType)
      )
      expect(await wallet.getAccount(account0.address)).toStrictEqual(account0)
      expect(await wallet.getAccount(account1.address)).toStrictEqual(account1)
      expect(await wallet.getAccount(account2.address)).toStrictEqual(account2)
      expect(await wallet.getAccount(account3.address)).toStrictEqual(account3)

      const newAccount0 = wallet.deriveAndAddNewAccount(0)
      const newAccount1 = wallet.deriveAndAddNewAccount(1)
      const newAccount2 = wallet.deriveAndAddNewAccount(2)
      const newAccount3 = wallet.deriveAndAddNewAccount(3)
      expect(groupOf(newAccount0)).toBe(0)
      expect(groupOf(newAccount1)).toBe(1)
      expect(groupOf(newAccount2)).toBe(2)
      expect(groupOf(newAccount3)).toBe(3)
      expect(newAccount0.addressIndex).toBeGreaterThan(account0.addressIndex)
      expect(newAccount1.addressIndex).toBeGreaterThan(account1.addressIndex)
      expect(newAccount2.addressIndex).toBeGreaterThan(account2.addressIndex)
      expect(newAccount3.addressIndex).toBeGreaterThan(account3.addressIndex)
    })
  })

  const message = '09282cae7d2a81d8e6a5d436dc64bd2eaefeaaef1d086739bf6be4d37be0ad3a'
  const test = async (wallet: HDWallet, messageHasher: MessageHasher) => {
    const account = wallet.deriveAndAddNewAccount()
    const result = await wallet.signMessage({
      signerAddress: account.address,
      message: message,
      messageHasher
    })
    const signature = result.signature
    expect(verifySignedMessage(message, messageHasher, account.publicKey, signature, wallet.keyType)).toBe(true)
  }

  it('should sign with secp256k1', async () => {
    const wallet = new HDWallet({ mnemonic: testMnemonic })
    const account = wallet.deriveAndAddNewAccount()
    expect(account.keyType).toBe('default')

    await test(wallet, 'alephium')
    await test(wallet, 'blake2b')
    await test(wallet, 'sha256')
    await test(wallet, 'identity')
  })

  it('should sign with schnorr', async () => {
    const wallet = new HDWallet({ mnemonic: testMnemonic, keyType: 'bip340-schnorr' })
    const account = wallet.deriveAndAddNewAccount()
    expect(account.keyType).toBe('bip340-schnorr')

    await test(wallet, 'alephium')
    await test(wallet, 'blake2b')
    await test(wallet, 'sha256')
    await test(wallet, 'identity')
  })

  it('should check key type', () => {
    const invalidKeyTypes: KeyType[] = ['gl-ed25519', 'gl-secp256r1', 'gl-webauthn']
    invalidKeyTypes.forEach((keyType) => {
      expect(() => new HDWallet({ mnemonic: testMnemonic, keyType })).toThrow('Invalid key type')
    })
  })
})
