const { describe, it } = require('mocha')
const assert = require('assert')
const bitcoin = require('../../')
const regtestUtils = require('./_regtest')
const regtest = regtestUtils.network

// See bottom of file for some helper functions used to make the payment objects needed.

describe('bitcoinjs-lib (transactions with psbt)', () => {
  it('can create (and broadcast via 3PBP) a typical Transaction', async () => {
    // these are { payment: Payment; keys: ECPair[] }
    const alice1 = createPayment('p2pkh')
    const alice2 = createPayment('p2pkh')

    // give Alice 2 unspent outputs
    const inputData1 = await getInputData(5e4, alice1.payment, false, 'noredeem')
    const inputData2 = await getInputData(7e4, alice2.payment, false, 'noredeem')
    {
      const {
        hash, // string of txid or Buffer of tx hash. (txid and hash are reverse order)
        index, // the output index of the txo you are spending
        nonWitnessUtxo, // the full previous transaction as a Buffer
      } = inputData1
      assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1)
    }

    // network is only needed if you pass an address to addOutput
    // using script (Buffer of scriptPubkey) instead will avoid needed network.
    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData1) // alice1 unspent
      .addInput(inputData2) // alice2 unspent
      .addOutput({
        address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf',
        value: 8e4
      }) // the actual "spend"
      .addOutput({
        address: alice2.payment.address, // OR script, which is a Buffer.
        value: 1e4
      }) // Alice's change
    // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee

    // Let's show a new feature with PSBT.
    // We can have multiple signers sign in parrallel and combine them.
    // (this is not necessary, but a nice feature)

    // encode to send out to the signers
    const psbtBaseText = psbt.toBase64()

    // each signer imports
    const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText)
    const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText)

    // Alice signs each input with the respective private keys
    signer1.signInput(0, alice1.keys[0])
    signer2.signInput(1, alice2.keys[0])

    // encode to send back to combiner (signer 1 and 2 are not near each other)
    const s1text = signer1.toBase64()
    const s2text = signer2.toBase64()

    const final1 = bitcoin.Psbt.fromBase64(s1text)
    const final2 = bitcoin.Psbt.fromBase64(s2text)

    // final1.combine(final2) would give the exact same result
    psbt.combine(final1, final2)

    // This step it new. Since we separate the signing operation and
    // the creation of the scriptSig and witness stack, we are able to
    psbt.finalizeAllInputs()
    // it returns an array of the success of each input, also a result attribute
    // which is true if all array items are true.

    // build and broadcast our RegTest network
    await regtestUtils.broadcast(psbt.extractTransaction().toHex())
    // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839
  })

  it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => {
    const alice1 = createPayment('p2pkh')
    const inputData1 = await getInputData(2e5, alice1.payment, false, 'noredeem')

    const data = Buffer.from('bitcoinjs-lib', 'utf8')
    const embed = bitcoin.payments.embed({ data: [data] })

    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData1)
      .addOutput({
        script: embed.output,
        value: 1000
      })
      .addOutput({
        address: regtestUtils.RANDOM_ADDRESS,
        value: 1e5
      })
      .signInput(0, alice1.keys[0])

    psbt.finalizeAllInputs()

    // build and broadcast to the RegTest network
    await regtestUtils.broadcast(psbt.extractTransaction().toHex())
  })

  it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => {
    const multisig = createPayment('p2sh-p2ms(2 of 4)')
    const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh')
    {
      const {
        hash,
        index,
        nonWitnessUtxo,
        redeemScript, // NEW: P2SH needs to give redeemScript when adding an input.
      } = inputData1
      assert.deepStrictEqual({ hash, index, nonWitnessUtxo, redeemScript }, inputData1)
    }

    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData1)
      .addOutput({
        address: regtestUtils.RANDOM_ADDRESS,
        value: 1e4
      })
      .signInput(0, multisig.keys[0])
      .signInput(0, multisig.keys[2])

    psbt.finalizeAllInputs()

    const tx = psbt.extractTransaction()

    // build and broadcast to the Bitcoin RegTest network
    await regtestUtils.broadcast(tx.toHex())

    await regtestUtils.verify({
      txId: tx.getId(),
      address: regtestUtils.RANDOM_ADDRESS,
      vout: 0,
      value: 1e4
    })
  })

  it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => {
    const p2sh = createPayment('p2sh-p2wpkh')
    const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh')
    {
      const {
        hash,
        index,
        witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; }
        redeemScript,
      } = inputData
      assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript }, inputData)
    }

    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData)
      .addOutput({
        address: regtestUtils.RANDOM_ADDRESS,
        value: 2e4
      })
      .signInput(0, p2sh.keys[0])

    psbt.finalizeAllInputs()

    const tx = psbt.extractTransaction()

    // build and broadcast to the Bitcoin RegTest network
    await regtestUtils.broadcast(tx.toHex())

    await regtestUtils.verify({
      txId: tx.getId(),
      address: regtestUtils.RANDOM_ADDRESS,
      vout: 0,
      value: 2e4
    })
  })

  it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => {

    // the only thing that changes is you don't give a redeemscript for input data

    const p2wpkh = createPayment('p2wpkh')
    const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem')
    {
      const {
        hash,
        index,
        witnessUtxo,
      } = inputData
      assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData)
    }

    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData)
      .addOutput({
        address: regtestUtils.RANDOM_ADDRESS,
        value: 2e4
      })
      .signInput(0, p2wpkh.keys[0])

    psbt.finalizeAllInputs()

    const tx = psbt.extractTransaction()

    // build and broadcast to the Bitcoin RegTest network
    await regtestUtils.broadcast(tx.toHex())

    await regtestUtils.verify({
      txId: tx.getId(),
      address: regtestUtils.RANDOM_ADDRESS,
      vout: 0,
      value: 2e4
    })
  })

  it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => {
    const p2wsh = createPayment('p2wsh-p2pk')
    const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh')
    {
      const {
        hash,
        index,
        witnessUtxo,
        witnessScript, // NEW: A Buffer of the witnessScript
      } = inputData
      assert.deepStrictEqual({ hash, index, witnessUtxo, witnessScript }, inputData)
    }

    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData)
      .addOutput({
        address: regtestUtils.RANDOM_ADDRESS,
        value: 2e4
      })
      .signInput(0, p2wsh.keys[0])

    psbt.finalizeAllInputs()

    const tx = psbt.extractTransaction()

    // build and broadcast to the Bitcoin RegTest network
    await regtestUtils.broadcast(tx.toHex())

    await regtestUtils.verify({
      txId: tx.getId(),
      address: regtestUtils.RANDOM_ADDRESS,
      vout: 0,
      value: 2e4
    })
  })

  it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => {
    const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)')
    const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh')
    {
      const {
        hash,
        index,
        witnessUtxo,
        redeemScript,
        witnessScript,
      } = inputData
      assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript, witnessScript }, inputData)
    }

    const psbt = new bitcoin.Psbt({ network: regtest })
      .addInput(inputData)
      .addOutput({
        address: regtestUtils.RANDOM_ADDRESS,
        value: 2e4
      })
      .signInput(0, p2sh.keys[0])
      .signInput(0, p2sh.keys[2])
      .signInput(0, p2sh.keys[3])

    psbt.finalizeAllInputs()

    const tx = psbt.extractTransaction()

    // build and broadcast to the Bitcoin RegTest network
    await regtestUtils.broadcast(tx.toHex())

    await regtestUtils.verify({
      txId: tx.getId(),
      address: regtestUtils.RANDOM_ADDRESS,
      vout: 0,
      value: 2e4
    })
  })
})

function createPayment(_type) {
  const splitType = _type.split('-').reverse();
  const isMultisig = splitType[0].slice(0, 4) === 'p2ms';
  const keys = [];
  let m;
  if (isMultisig) {
    const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/)
    m = parseInt(match[1])
    let n = parseInt(match[2])
    while (n > 1) {
      keys.push(bitcoin.ECPair.makeRandom({ network: regtest }));
      n--
    }
  }
  keys.push(bitcoin.ECPair.makeRandom({ network: regtest }));

  let payment;
  splitType.forEach(type => {
    if (type.slice(0, 4) === 'p2ms') {
      payment = bitcoin.payments.p2ms({
        m,
        pubkeys: keys.map(key => key.publicKey).sort(),
        network: regtest,
      });
    } else if (['p2sh', 'p2wsh'].indexOf(type) > -1) {
      payment = bitcoin.payments[type]({
        redeem: payment,
        network: regtest,
      });
    } else {
      payment = bitcoin.payments[type]({
        pubkey: keys[0].publicKey,
        network: regtest,
      });
    }
  });

  return {
    payment,
    keys,
  };
}

function getWitnessUtxo(out) {
  delete out.address;
  out.script = Buffer.from(out.script, 'hex');
  return out;
}

async function getInputData(amount, payment, isSegwit, redeemType) {
  const unspent = await regtestUtils.faucetComplex(payment.output, amount);
  const utx = await regtestUtils.fetch(unspent.txId);
  // for non segwit inputs, you must pass the full transaction buffer
  const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex');
  // for segwit inputs, you only need the output script and value as an object.
  const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]);
  const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo };
  const mixin2 = {};
  switch (redeemType) {
    case 'p2sh':
      mixin2.redeemScript = payment.redeem.output;
      break;
    case 'p2wsh':
      mixin2.witnessScript = payment.redeem.output;
      break;
    case 'p2sh-p2wsh':
      mixin2.witnessScript = payment.redeem.redeem.output;
      mixin2.redeemScript = payment.redeem.output;
      break;
  }
  return {
    hash: unspent.txId,
    index: unspent.vout,
    ...mixin,
    ...mixin2,
  };
}