package types

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"io/ioutil"
	"strings"
	"testing"

	"github.com/davecgh/go-spew/spew"

	"github.com/vapor/consensus"
	"github.com/vapor/errors"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/testutil"
)

func TestTransaction(t *testing.T) {
	cases := []struct {
		tx   *Tx
		hex  string
		hash bc.Hash
	}{
		{
			tx: NewTx(TxData{
				Version:        1,
				SerializedSize: uint64(5),
				Inputs:         nil,
				Outputs:        nil,
			}),
			hex: strings.Join([]string{
				"07", // serflags
				"01", // transaction version
				"00", // tx time range
				"00", // inputs count
				"00", // outputs count
			}, ""),
			hash: testutil.MustDecodeHash("8e88b9cb4615128c7209dff695f68b8de5b38648bf3d44d2d0e6a674848539c9"),
		},
		{
			tx: NewTx(TxData{
				Version:        1,
				SerializedSize: uint64(112),
				Inputs: []*TxInput{
					NewCoinbaseInput([]byte("arbitrary")),
				},
				Outputs: []*TxOutput{
					NewIntraChainOutput(*consensus.BTMAssetID, 254354, []byte("true")),
					NewIntraChainOutput(*consensus.BTMAssetID, 254354, []byte("false")),
				},
			}),
			hex: strings.Join([]string{
				"07",                 // serflags
				"01",                 // transaction version
				"00",                 // tx time range
				"01",                 // inputs count
				"01",                 // input 0: asset version
				"0b",                 // input 0: input commitment length
				"02",                 // input 0: coinbase type flag
				"09",                 // input 0: arbitrary length
				"617262697472617279", // input 0: arbitrary data
				"00",                 // input 0: witness length
				"02",                 // outputs count
				"01",                 // output 0: asset version
				"2b",                 // output 0: serialization length
				"00",                 // output 0: outType
				"29",                 // output 0: output commitment length
				"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // output 0: assetID
				"92c30f",   // output 0: amount
				"01",       // output 0: version
				"04",       // output 0: control program length
				"74727565", // output 0: control program
				"00",       // output 0: witness length
				"01",       // output 1: asset version
				"2c",       // output 1: serialization length
				"00",       // output 1: outType
				"2a",       // output 1: output commitment length
				"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // output 1: assetID
				"92c30f",     // output 1: amount
				"01",         // output 1: version
				"05",         // output 1: control program length
				"66616c7365", // output 1: control program
				"00",         // output 1: witness length
			}, ""),
			hash: testutil.MustDecodeHash("2591a2af0d3690107215c2a47ab60c4e8d7547f04154ecd5ccab1db0d31e66b4"),
		},
	}
	for i, test := range cases {
		got := testutil.Serialize(t, test.tx)
		want, err := hex.DecodeString(test.hex)
		if err != nil {
			t.Fatal(err)
		}

		if !bytes.Equal(got, want) {
			t.Errorf("test %d: bytes = %x want %x", i, got, want)
		}
		if test.tx.ID != test.hash {
			t.Errorf("test %d: hash = %x want %x", i, test.tx.ID.Bytes(), test.hash.Bytes())
		}

		txJSON, err := json.Marshal(test.tx)
		if err != nil {
			t.Errorf("test %d: error marshaling tx to json: %s", i, err)
		}
		txFromJSON := Tx{}
		if err := json.Unmarshal(txJSON, &txFromJSON); err != nil {
			t.Errorf("test %d: error unmarshaling tx from json: %s", i, err)
		}
		if !testutil.DeepEqual(test.tx.TxData, txFromJSON.TxData) {
			t.Errorf("test %d: types.TxData -> json -> types.TxData: got:\n%s\nwant:\n%s", i, spew.Sdump(txFromJSON.TxData), spew.Sdump(test.tx.TxData))
		}

		tx1 := new(TxData)
		if err := tx1.UnmarshalText([]byte(test.hex)); err != nil {
			t.Errorf("test %d: unexpected err %v", i, err)
		}
		if !testutil.DeepEqual(*tx1, test.tx.TxData) {
			t.Errorf("test %d: tx1 is:\n%swant:\n%s", i, spew.Sdump(*tx1), spew.Sdump(test.tx.TxData))
		}
	}
}

func BenchmarkTxWriteToTrue(b *testing.B) {
	tx := &Tx{}
	for i := 0; i < b.N; i++ {
		tx.writeTo(ioutil.Discard, 0)
	}
}

func BenchmarkTxWriteToFalse(b *testing.B) {
	tx := &Tx{}
	for i := 0; i < b.N; i++ {
		tx.writeTo(ioutil.Discard, serRequired)
	}
}

func BenchmarkTxWriteToTrue200(b *testing.B) {
	tx := &Tx{}
	for i := 0; i < 200; i++ {
		tx.Inputs = append(tx.Inputs, NewSpendInput(nil, bc.Hash{}, bc.AssetID{}, 0, 0, nil))
		tx.Outputs = append(tx.Outputs, NewIntraChainOutput(bc.AssetID{}, 0, nil))
	}
	for i := 0; i < b.N; i++ {
		tx.writeTo(ioutil.Discard, 0)
	}
}

func BenchmarkTxWriteToFalse200(b *testing.B) {
	tx := &Tx{}
	for i := 0; i < 200; i++ {
		tx.Inputs = append(tx.Inputs, NewSpendInput(nil, bc.Hash{}, bc.AssetID{}, 0, 0, nil))
		tx.Outputs = append(tx.Outputs, NewIntraChainOutput(bc.AssetID{}, 0, nil))
	}
	for i := 0; i < b.N; i++ {
		tx.writeTo(ioutil.Discard, serRequired)
	}
}

func BenchmarkTxInputWriteToTrue(b *testing.B) {
	input := NewSpendInput(nil, bc.Hash{}, bc.AssetID{}, 0, 0, nil)
	ew := errors.NewWriter(ioutil.Discard)
	for i := 0; i < b.N; i++ {
		input.writeTo(ew)
	}
}

func BenchmarkTxInputWriteToFalse(b *testing.B) {
	input := NewSpendInput(nil, bc.Hash{}, bc.AssetID{}, 0, 0, nil)
	ew := errors.NewWriter(ioutil.Discard)
	for i := 0; i < b.N; i++ {
		input.writeTo(ew)
	}
}

func BenchmarkTxOutputWriteToTrue(b *testing.B) {
	output := NewIntraChainOutput(bc.AssetID{}, 0, nil)
	ew := errors.NewWriter(ioutil.Discard)
	for i := 0; i < b.N; i++ {
		output.writeTo(ew)
	}
}

func BenchmarkTxOutputWriteToFalse(b *testing.B) {
	output := NewIntraChainOutput(bc.AssetID{}, 0, nil)
	ew := errors.NewWriter(ioutil.Discard)
	for i := 0; i < b.N; i++ {
		output.writeTo(ew)
	}
}

func BenchmarkAssetAmountWriteTo(b *testing.B) {
	aa := bc.AssetAmount{}
	for i := 0; i < b.N; i++ {
		aa.WriteTo(ioutil.Discard)
	}
}
