require_relative 'spec_helper'
describe BTC::TransactionBuilder do

  class SignerByKey
    include BTC::TransactionBuilder::Signer
    def initialize(&block)
      @block = block
    end
    def signing_key_for_output(output: nil, address: nil)
      @block.call(output, address)
    end
  end

  class SignerBySignatureScript
    include BTC::TransactionBuilder::Signer
    def initialize(&block)
      @block = block
    end
    def signature_script_for_input(input: nil, output: nil)
      @block.call(input, output)
    end
  end

  describe "TransactionBuilder with no outputs" do
    before do
      @builder = BTC::TransactionBuilder.new
      @all_utxos = self.mock_utxos

      @builder.input_addresses = self.mock_addresses
      @builder.provider = BTC::TransactionBuilder::Provider.new do |txb|
        addrs = txb.public_addresses
        addrs.must_equal self.mock_addresses
        scripts = addrs.map{|a| a.script }.uniq
        @all_utxos.find_all{|utxo| scripts.include?(utxo.script) }
      end
      @builder.change_address = BTC::Address.parse("1CBtcGivXmHQ8ZqdPgeMfcpQNJrqTrSAcG")
    end

    it "should fill unspent_outputs using Provider" do
      @builder.unspent_outputs.must_equal @all_utxos
    end

    it "should have a default fee rate" do
      @builder.fee_rate.must_equal BTC::Transaction::DEFAULT_FEE_RATE
    end

    it "should return a result" do
      result = @builder.build
      result.class.must_equal BTC::TransactionBuilder::Result
    end

    it "should compose a fully-spending transaction when no outputs are given" do
      result = @builder.build
      tx = result.transaction
      tx.inputs.size.must_equal mock_utxos.size
      tx.outputs.size.must_equal 1 # only one change address
    end

    it "should have a list of unsigned indexes" do
      result = @builder.build
      result.unsigned_input_indexes.must_equal (0...(mock_utxos.size)).to_a
    end

    it "should have a reasonable fee" do
      result = @builder.build
      size = result.transaction.data.bytesize # size of unsigned transaction
      result.fee.must_be :>=, (size / 1000)*BTC::Transaction::DEFAULT_FEE_RATE
      result.fee.must_be :<, 0.01 * BTC::COIN
    end

    it "should be able to set mining fee" do
      @builder.fee =50690
      result = @builder.build()
      result.fee.must_equal 50690
    end
    
    it "should have valid amounts" do
      result = @builder.build
      result.inputs_amount.must_equal mock_utxos.inject(0){|sum, out| sum + out.value}
      result.outputs_amount.must_be :<=, result.inputs_amount
      # Balance should be correct
      result.inputs_amount.must_equal (result.outputs_amount + result.fee)
      result.transaction.inputs.each do |txin|
        # Unsigned txins contain respective output scripts.
        # Signed txins contain data-only signature script (for P2PKH it is signature + pubkey)
        txin.signature_script.data_only?.must_equal false
        txin.signature_script.p2pkh?.must_equal true
      end
    end

    it "should sign if WIFs are provided" do
      @builder.input_addresses = mock_wifs
      result = @builder.build
      result.unsigned_input_indexes.must_equal []
      result.transaction.inputs.each do |txin|
        # Unsigned txins contain respective output scripts.
        # Signed txins contain data-only signature script (for P2PKH it is signature + pubkey)
        txin.signature_script.data_only?.must_equal true
        txin.signature_script.p2pkh?.must_equal false
      end
    end

    it "should sign if signer provides a key" do
      @builder.signer = SignerByKey.new do |txout, addr|
        wif = mock_wifs.find{|wif| wif.public_address == addr}
        wif.key
      end
      result = @builder.build
      result.unsigned_input_indexes.must_equal []
      result.transaction.inputs.each do |txin|
        # Unsigned txins contain respective output scripts.
        # Signed txins contain data-only signature script (for P2PKH it is signature + pubkey)
        txin.signature_script.data_only?.must_equal true
        txin.signature_script.p2pkh?.must_equal false
      end
    end

    it "should sign if signer provides a signature script" do
      @builder.signer = SignerBySignatureScript.new do |txin, txout|
        BTC::Script.new << "signature" << "pubkey"
      end
      result = @builder.build
      result.unsigned_input_indexes.must_equal []
      result.transaction.inputs.each do |txin|
        # Unsigned txins contain respective output scripts.
        # Signed txins contain data-only signature script (for P2PKH it is signature + pubkey)
        txin.signature_script.data_only?.must_equal true
        txin.signature_script.p2pkh?.must_equal false
      end
    end

    it "should include all prepended unspents AND all normal unspents" do
      @builder.prepended_unspent_outputs = [ BTC::TransactionOutput.new(
        value: 6_666_666,
        script: BTC::Address.parse("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX").script,
        index: 0,
        transaction_hash: "some mock tx".hash256
      ) ]
      result = @builder.build
      result.inputs_amount.must_equal mock_utxos.inject(0){|sum, out| sum + out.value} + 6_666_666
      result.transaction.inputs.size.must_equal mock_utxos.size + 1
    end
  end




  describe "TransactionBuilder with some outputs" do
    before do
      @builder = BTC::TransactionBuilder.new
      @all_utxos = self.mock_utxos
      @builder.input_addresses = self.mock_addresses
      @builder.provider = BTC::TransactionBuilder::Provider.new do |txb|
        addrs = txb.public_addresses
        addrs.must_equal self.mock_addresses
        scripts = addrs.map{|a| a.script }.uniq
        @all_utxos.find_all{|utxo| scripts.include?(utxo.script) }
      end
      @builder.change_address = BTC::Address.parse("1CBtcGivXmHQ8ZqdPgeMfcpQNJrqTrSAcG")
      @builder.outputs = [ BTC::TransactionOutput.new(value: 1000_500,
                            script: BTC::Address.parse("1TipsuQ7CSqfQsjA9KU5jarSB1AnrVLLo").script) ]
    end

    it "should fill unspent_outputs using provider" do
      @builder.unspent_outputs.must_equal @all_utxos
    end

    it "should have a default fee rate" do
      @builder.fee_rate.must_equal BTC::Transaction::DEFAULT_FEE_RATE
    end

    it "should compose a minimal transaction to pay necessary amount" do
      result = @builder.build
      tx = result.transaction
      tx.inputs.size.must_equal 11
      tx.outputs.size.must_equal 2 # one change address and one output address

      # Payment address
      tx.outputs.first.value.must_equal 1000_500
      tx.outputs.first.script.standard_address.to_s.must_equal "1TipsuQ7CSqfQsjA9KU5jarSB1AnrVLLo"

      # Change address
      tx.outputs.last.value.must_equal (1000_00*11 - 1000_500 - result.fee)
      tx.outputs.last.script.standard_address.to_s.must_equal "1CBtcGivXmHQ8ZqdPgeMfcpQNJrqTrSAcG"
    end

    it "should have a list of unsigned indexes" do
      result = @builder.build
      result.unsigned_input_indexes.must_equal (0...(result.transaction.inputs.size)).to_a
    end

    it "should have a reasonable fee" do
      result = @builder.build
      size = result.transaction.data.bytesize # size of unsigned transaction
      result.fee.must_be :>=, (size / 1000)*BTC::Transaction::DEFAULT_FEE_RATE
      result.fee.must_be :<, 0.01 * BTC::COIN
    end

    it "should be able to set mining fee" do
      @builder.fee =50690
      result = @builder.build()
      result.fee.must_equal 50690
    end

    it "should have valid amounts" do
      result = @builder.build
      result.inputs_amount.must_equal 11*1000_00
      result.outputs_amount.must_be :<=, result.inputs_amount
      # Balance should be correct
      result.inputs_amount.must_equal (result.outputs_amount + result.fee)
      result.transaction.inputs.each do |txin|
        # Unsigned txins contain respective output scripts.
        # Signed txins contain data-only signature script (for P2PKH it is signature + pubkey)
        txin.signature_script.data_only?.must_equal false
        txin.signature_script.p2pkh?.must_equal true
      end
    end

    it "should sign if WIFs are provided" do
      @builder.input_addresses = mock_wifs
      result = @builder.build
      result.unsigned_input_indexes.must_equal []
      result.transaction.inputs.each do |txin|
        # Unsigned txins contain respective output scripts.
        # Signed txins contain data-only signature script (for P2PKH it is signature + pubkey)
        txin.signature_script.data_only?.must_equal true
        txin.signature_script.p2pkh?.must_equal false
      end
    end

    it "should include all prepended unspents and none of normal unspents if amount is covered" do
      @builder.prepended_unspent_outputs = [ BTC::TransactionOutput.new(
        value: 6_666_666,
        script: BTC::Address.parse("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX").script,
        index: 0,
        transaction_hash: "some mock tx".hash256
      ) ]
      result = @builder.build
      result.inputs_amount.must_equal 6_666_666
      result.transaction.inputs.size.must_equal 1
    end

    it "should include all prepended unspents and just enough of normal unspents" do
      @builder.prepended_unspent_outputs = [ BTC::TransactionOutput.new(
        value: @builder.outputs.first.value - (self.mock_utxos.first.value / 2),
        script: BTC::Address.parse("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX").script,
        index: 0,
        transaction_hash: "some mock tx".hash256
      ) ]
      result = @builder.build
      result.inputs_amount.must_equal(@builder.outputs.first.value + (self.mock_utxos.first.value / 2))
      result.transaction.inputs.size.must_equal 2
    end
  end

  describe "TransactionBuilder edge cases" do
    before do
      @builder = BTC::TransactionBuilder.new
      @all_utxos = self.mock_utxos
      @builder.input_addresses = self.mock_addresses
      @builder.provider = BTC::TransactionBuilder::Provider.new do |txb|
        addrs = txb.public_addresses
        addrs.must_equal self.mock_addresses
        scripts = addrs.map{|a| a.script }.uniq
        @all_utxos.find_all{|utxo| scripts.include?(utxo.script) }
      end
      @builder.change_address = BTC::Address.parse("1CBtcGivXmHQ8ZqdPgeMfcpQNJrqTrSAcG")
    end

    it "should detect missing change address" do
      @builder.change_address = nil
      lambda do
        result = @builder.build
      end.must_raise BTC::TransactionBuilder::MissingChangeAddressError
    end

    it "should detect missing unspents" do
      @builder.provider = nil
      lambda do
        result = @builder.build
      end.must_raise BTC::TransactionBuilder::MissingUnspentOutputsError
    end

    it "should detect missing unspents" do
      @builder.unspent_outputs = [ ]
      lambda do
        result = @builder.build
      end.must_raise BTC::TransactionBuilder::MissingUnspentOutputsError
    end

    it "should detect not enough unspents" do
      @builder.outputs = [ BTC::TransactionOutput.new(value: 100*BTC::COIN, script: @builder.change_address.script) ]
      lambda do
        result = @builder.build
      end.must_raise BTC::TransactionBuilder::InsufficientFundsError
    end

    it "should detect not enough unspents because of change constraints" do
      @builder.dust_change = 0 # no coins are allowed to be lost
      @builder.minimum_change = 10000
      @builder.unspent_outputs = mock_utxos[0, 1]
      @builder.outputs = [ BTC::TransactionOutput.new(value: 1000_00 - @builder.fee_rate - 10, script: @builder.change_address.script) ]
      lambda do
        result = @builder.build
      end.must_raise BTC::TransactionBuilder::InsufficientFundsError
    end

    it "should forgo change if it's below dust level" do
      @builder.dust_change = 42
      @builder.minimum_change = 1000
      @builder.unspent_outputs = mock_utxos[0, 1]
      assumed_fee = 2590 #@builder.fee_rate # we assume we'll have sub-1K transaction
      amount = 1000_00 - assumed_fee - @builder.dust_change
      @builder.outputs = [ BTC::TransactionOutput.new(value: amount, script: @builder.change_address.script) ]

      result = @builder.build
      result.fee.must_equal assumed_fee
      result.transaction.wont_equal nil
      result.transaction.outputs.size.must_equal 1
      result.transaction.outputs.first.value.must_equal amount
    end

  end

  def mock_keys
    @mock_keys ||= [
      BTC::Key.new(private_key: "Wallet1".sha256),
      BTC::Key.new(private_key: "Wallet2".sha256)
    ]
  end

  def mock_addresses
    mock_keys.map{|k| k.address }
  end

  def mock_wifs
    mock_keys.map{|k| k.to_wif_object }
  end

  def mock_utxos
    scripts = mock_addresses.map{|a| a.script }
    (0...32).map do |i|
      BTC::TransactionOutput.new(value:  100_000,
                           script: scripts[i % scripts.size],
                 transaction_hash: ((16+i).to_s(16)*32).from_hex,
                            index: i)
    end
  end

end
