// +build integration

package sync

import (
	"blockbook/bchain"
	"blockbook/common"
	"blockbook/db"
	"encoding/json"
	"errors"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"
)

var testMap = map[string]func(t *testing.T, th *TestHandler){
	"ConnectBlocks":         testConnectBlocks,
	"ConnectBlocksParallel": testConnectBlocksParallel,
	"HandleFork":            testHandleFork,
}

type TestHandler struct {
	Coin     string
	Chain    bchain.BlockChain
	TestData *TestData
}

type Range struct {
	Lower uint32 `json:"lower"`
	Upper uint32 `json:"upper"`
}

type TestData struct {
	ConnectBlocks struct {
		SyncRanges []Range              `json:"syncRanges"`
		Blocks     map[uint32]BlockInfo `json:"blocks"`
	} `json:"connectBlocks"`
	HandleFork struct {
		SyncRanges []Range            `json:"syncRanges"`
		FakeBlocks map[uint32]BlockID `json:"fakeBlocks"`
		RealBlocks map[uint32]BlockID `json:"realBlocks"`
	} `json:"handleFork"`
}

type BlockID struct {
	Height uint32 `json:"height"`
	Hash   string `json:"hash"`
}

type BlockInfo struct {
	BlockID
	NoTxs     uint32       `json:"noTxs"`
	TxDetails []*bchain.Tx `json:"txDetails"`
}

func IntegrationTest(t *testing.T, coin string, chain bchain.BlockChain, testConfig json.RawMessage) {
	tests, err := getTests(testConfig)
	if err != nil {
		t.Fatalf("Failed loading of test list: %s", err)
	}

	parser := chain.GetChainParser()
	td, err := loadTestData(coin, parser)
	if err != nil {
		t.Fatalf("Failed loading of test data: %s", err)
	}

	for _, test := range tests {
		if f, found := testMap[test]; found {
			h := TestHandler{Coin: coin, Chain: chain, TestData: td}
			t.Run(test, func(t *testing.T) { f(t, &h) })
		} else {
			t.Errorf("%s: test not found", test)
			continue
		}
	}
}

func getTests(cfg json.RawMessage) ([]string, error) {
	var v []string
	err := json.Unmarshal(cfg, &v)
	if err != nil {
		return nil, err
	}
	if len(v) == 0 {
		return nil, errors.New("No tests declared")
	}
	return v, nil
}

func loadTestData(coin string, parser bchain.BlockChainParser) (*TestData, error) {
	path := filepath.Join("sync/testdata", coin+".json")
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var v TestData
	err = json.Unmarshal(b, &v)
	if err != nil {
		return nil, err
	}

	for _, b := range v.ConnectBlocks.Blocks {
		for _, tx := range b.TxDetails {
			// convert amounts in test json to bit.Int and clear the temporary JsonValue
			for i := range tx.Vout {
				vout := &tx.Vout[i]
				vout.ValueSat, err = parser.AmountToBigInt(vout.JsonValue)
				if err != nil {
					return nil, err
				}
				vout.JsonValue = ""
			}

			// get addresses parsed
			err := setTxAddresses(parser, tx)
			if err != nil {
				return nil, err
			}
		}
	}

	return &v, nil
}

func setTxAddresses(parser bchain.BlockChainParser, tx *bchain.Tx) error {
	for i := range tx.Vout {
		ad, err := parser.GetAddrDescFromVout(&tx.Vout[i])
		if err != nil {
			return err
		}
		a, s, err := parser.GetAddressesFromAddrDesc(ad)
		if err == nil && s {
			tx.Vout[i].ScriptPubKey.Addresses = a
		}
	}
	return nil
}

func makeRocksDB(parser bchain.BlockChainParser, m *common.Metrics, is *common.InternalState) (*db.RocksDB, func(), error) {
	p, err := ioutil.TempDir("", "sync_test")
	if err != nil {
		return nil, nil, err
	}

	d, err := db.NewRocksDB(p, 1<<17, 1<<14, parser, m)
	if err != nil {
		return nil, nil, err
	}

	d.SetInternalState(is)

	closer := func() {
		d.Close()
		os.RemoveAll(p)
	}

	return d, closer, nil
}

var metricsRegistry = map[string]*common.Metrics{}

func getMetrics(name string) (*common.Metrics, error) {
	if m, found := metricsRegistry[name]; found {
		return m, nil
	} else {
		m, err := common.GetMetrics(name)
		if err != nil {
			return nil, err
		}
		metricsRegistry[name] = m
		return m, nil
	}
}

func withRocksDBAndSyncWorker(t *testing.T, h *TestHandler, startHeight uint32, fn func(*db.RocksDB, *db.SyncWorker, chan os.Signal)) {
	m, err := getMetrics(h.Coin)
	if err != nil {
		t.Fatal(err)
	}
	is := &common.InternalState{}

	d, closer, err := makeRocksDB(h.Chain.GetChainParser(), m, is)
	if err != nil {
		t.Fatal(err)
	}
	defer closer()

	ch := make(chan os.Signal)

	sw, err := db.NewSyncWorker(d, h.Chain, 8, 0, int(startHeight), false, ch, m, is)
	if err != nil {
		t.Fatal(err)
	}

	fn(d, sw, ch)
}
