// +build integration

package sync

import (
	"blockbook/bchain"
	"blockbook/db"
	"math/big"
	"os"
	"reflect"
	"strings"
	"testing"
)

func testConnectBlocks(t *testing.T, h *TestHandler) {
	for _, rng := range h.TestData.ConnectBlocks.SyncRanges {
		withRocksDBAndSyncWorker(t, h, rng.Lower, func(d *db.RocksDB, sw *db.SyncWorker, ch chan os.Signal) {
			upperHash, err := h.Chain.GetBlockHash(rng.Upper)
			if err != nil {
				t.Fatal(err)
			}

			err = db.ConnectBlocks(sw, func(hash string, height uint32) {
				if hash == upperHash {
					close(ch)
				}
			}, true)
			if err != nil {
				if !strings.HasPrefix(err.Error(), "connectBlocks interrupted at height") {
					t.Fatal(err)
				}
			}

			height, _, err := d.GetBestBlock()
			if err != nil {
				t.Fatal(err)
			}
			if height < rng.Upper {
				t.Fatalf("Best block height mismatch: %d < %d", height, rng.Upper)
			}

			t.Run("verifyBlockInfo", func(t *testing.T) { verifyBlockInfo(t, d, h, rng) })
			t.Run("verifyTransactions", func(t *testing.T) { verifyTransactions(t, d, h, rng) })
			t.Run("verifyAddresses", func(t *testing.T) { verifyAddresses(t, d, h, rng) })
		})
	}
}

func testConnectBlocksParallel(t *testing.T, h *TestHandler) {
	for _, rng := range h.TestData.ConnectBlocks.SyncRanges {
		withRocksDBAndSyncWorker(t, h, rng.Lower, func(d *db.RocksDB, sw *db.SyncWorker, ch chan os.Signal) {
			upperHash, err := h.Chain.GetBlockHash(rng.Upper)
			if err != nil {
				t.Fatal(err)
			}

			err = sw.ConnectBlocksParallel(rng.Lower, rng.Upper)
			if err != nil {
				t.Fatal(err)
			}

			height, hash, err := d.GetBestBlock()
			if err != nil {
				t.Fatal(err)
			}
			if height != rng.Upper {
				t.Fatalf("Best block height mismatch: %d != %d", height, rng.Upper)
			}
			if hash != upperHash {
				t.Fatalf("Best block hash mismatch: %s != %s", hash, upperHash)
			}

			t.Run("verifyBlockInfo", func(t *testing.T) { verifyBlockInfo(t, d, h, rng) })
			t.Run("verifyTransactions", func(t *testing.T) { verifyTransactions(t, d, h, rng) })
			t.Run("verifyAddresses", func(t *testing.T) { verifyAddresses(t, d, h, rng) })
		})
	}
}

func verifyBlockInfo(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range) {
	for height := rng.Lower; height <= rng.Upper; height++ {
		block, found := h.TestData.ConnectBlocks.Blocks[height]
		if !found {
			continue
		}

		bi, err := d.GetBlockInfo(height)
		if err != nil {
			t.Errorf("GetBlockInfo(%d) error: %s", height, err)
			continue
		}
		if bi == nil {
			t.Errorf("GetBlockInfo(%d) returned nil", height)
			continue
		}

		if bi.Hash != block.Hash {
			t.Errorf("Block hash mismatch: %s != %s", bi.Hash, block.Hash)
		}

		if bi.Txs != block.NoTxs {
			t.Errorf("Number of transactions in block %s mismatch: %d != %d", bi.Hash, bi.Txs, block.NoTxs)
		}
	}
}

func verifyTransactions(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range) {
	type txInfo struct {
		txid  string
		index int32
	}
	addr2txs := make(map[string][]txInfo)
	checkMap := make(map[string][]bool)

	for height := rng.Lower; height <= rng.Upper; height++ {
		block, found := h.TestData.ConnectBlocks.Blocks[height]
		if !found {
			continue
		}

		for _, tx := range block.TxDetails {
			for _, vin := range tx.Vin {
				for _, a := range vin.Addresses {
					addr2txs[a] = append(addr2txs[a], txInfo{tx.Txid, ^int32(vin.Vout)})
					checkMap[a] = append(checkMap[a], false)
				}
			}
			for _, vout := range tx.Vout {
				for _, a := range vout.ScriptPubKey.Addresses {
					addr2txs[a] = append(addr2txs[a], txInfo{tx.Txid, int32(vout.N)})
					checkMap[a] = append(checkMap[a], false)
				}
			}
		}
	}

	for addr, txs := range addr2txs {
		err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, height uint32, indexes []int32) error {
			for i, tx := range txs {
				for _, index := range indexes {
					if txid == tx.txid && index == tx.index {
						checkMap[addr][i] = true
					}
				}
			}
			return nil
		})
		if err != nil {
			t.Fatal(err)
		}
	}

	for addr, txs := range addr2txs {
		for i, tx := range txs {
			if !checkMap[addr][i] {
				t.Errorf("%s: transaction not found %+v", addr, tx)
			}
		}
	}
}

func verifyAddresses(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range) {
	parser := h.Chain.GetChainParser()

	for height := rng.Lower; height <= rng.Upper; height++ {
		block, found := h.TestData.ConnectBlocks.Blocks[height]
		if !found {
			continue
		}

		for _, tx := range block.TxDetails {
			ta, err := d.GetTxAddresses(tx.Txid)
			if err != nil {
				t.Fatal(err)
			}
			if ta == nil {
				t.Errorf("Tx %s: not found in TxAddresses", tx.Txid)
				continue
			}

			txInfo := getTxInfo(tx)
			taInfo, err := getTaInfo(parser, ta)
			if err != nil {
				t.Fatal(err)
			}

			if ta.Height != height {
				t.Errorf("Tx %s: block height mismatch: %d != %d", tx.Txid, ta.Height, height)
				continue
			}

			if len(txInfo.inputs) > 0 && !reflect.DeepEqual(taInfo.inputs, txInfo.inputs) {
				t.Errorf("Tx %s: inputs mismatch: got %q, want %q", tx.Txid, taInfo.inputs, txInfo.inputs)
			}

			if !reflect.DeepEqual(taInfo.outputs, txInfo.outputs) {
				t.Errorf("Tx %s: outputs mismatch: got %q, want %q", tx.Txid, taInfo.outputs, txInfo.outputs)
			}

			if taInfo.valOutSat.Cmp(&txInfo.valOutSat) != 0 {
				t.Errorf("Tx %s: total output amount mismatch: got %s, want %s",
					tx.Txid, taInfo.valOutSat.String(), txInfo.valOutSat.String())
			}

			if len(txInfo.inputs) > 0 {
				treshold := "0.0001"
				fee := new(big.Int).Sub(&taInfo.valInSat, &taInfo.valOutSat)
				if strings.Compare(parser.AmountToDecimalString(fee), treshold) > 0 {
					t.Errorf("Tx %s: suspicious amounts: input ∑ [%s] - output ∑ [%s] > %s",
						tx.Txid, taInfo.valInSat.String(), taInfo.valOutSat.String(), treshold)
				}
			}
		}
	}
}

type txInfo struct {
	inputs    []string
	outputs   []string
	valInSat  big.Int
	valOutSat big.Int
}

func getTxInfo(tx *bchain.Tx) *txInfo {
	info := &txInfo{inputs: []string{}, outputs: []string{}}

	for _, vin := range tx.Vin {
		for _, a := range vin.Addresses {
			info.inputs = append(info.inputs, a)
		}
	}
	for _, vout := range tx.Vout {
		for _, a := range vout.ScriptPubKey.Addresses {
			info.outputs = append(info.outputs, a)
		}
		info.valOutSat.Add(&info.valOutSat, &vout.ValueSat)
	}

	return info
}

func getTaInfo(parser bchain.BlockChainParser, ta *db.TxAddresses) (*txInfo, error) {
	info := &txInfo{inputs: []string{}, outputs: []string{}}

	for i := range ta.Inputs {
		info.valInSat.Add(&info.valInSat, &ta.Inputs[i].ValueSat)
		addrs, s, err := ta.Inputs[i].Addresses(parser)
		if err == nil && s {
			for _, a := range addrs {
				info.inputs = append(info.inputs, a)
			}
		}
	}

	for i := range ta.Outputs {
		info.valOutSat.Add(&info.valOutSat, &ta.Outputs[i].ValueSat)
		addrs, s, err := ta.Outputs[i].Addresses(parser)
		if err == nil && s {
			for _, a := range addrs {
				info.outputs = append(info.outputs, a)
			}
		}
	}

	return info, nil
}
