package claimtrie

import (
	"testing"

	"github.com/btcsuite/btcd/claimtrie/change"
	"github.com/btcsuite/btcd/claimtrie/config"
	"github.com/btcsuite/btcd/claimtrie/merkletrie"
	"github.com/btcsuite/btcd/claimtrie/param"

	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"

	"github.com/stretchr/testify/require"
)

var cfg = config.DefaultConfig

func setup(t *testing.T) {
	param.SetNetwork(wire.TestNet)
	cfg.DataDir = t.TempDir()
}

func b(s string) []byte {
	return []byte(s)
}

func buildTx(hash chainhash.Hash) *wire.MsgTx {
	tx := wire.NewMsgTx(1)
	txIn := wire.NewTxIn(wire.NewOutPoint(&hash, 0), nil, nil)
	tx.AddTxIn(txIn)
	tx.AddTxOut(wire.NewTxOut(0, nil))
	return tx
}

func TestFixedHashes(t *testing.T) {

	r := require.New(t)

	setup(t)
	ct, err := New(cfg)
	r.NoError(err)
	defer func() {
		err = ct.Close()
		r.NoError(err)
	}()

	r.Equal(merkletrie.EmptyTrieHash[:], ct.MerkleHash()[:])

	tx1 := buildTx(*merkletrie.EmptyTrieHash)
	tx2 := buildTx(tx1.TxHash())
	tx3 := buildTx(tx2.TxHash())
	tx4 := buildTx(tx3.TxHash())

	err = ct.AddClaim(b("test"), tx1.TxIn[0].PreviousOutPoint, change.NewClaimID(tx1.TxIn[0].PreviousOutPoint), 50)
	r.NoError(err)

	err = ct.AddClaim(b("test2"), tx2.TxIn[0].PreviousOutPoint, change.NewClaimID(tx2.TxIn[0].PreviousOutPoint), 50)
	r.NoError(err)

	err = ct.AddClaim(b("test"), tx3.TxIn[0].PreviousOutPoint, change.NewClaimID(tx3.TxIn[0].PreviousOutPoint), 50)
	r.NoError(err)

	err = ct.AddClaim(b("tes"), tx4.TxIn[0].PreviousOutPoint, change.NewClaimID(tx4.TxIn[0].PreviousOutPoint), 50)
	r.NoError(err)

	err = ct.AppendBlock()
	r.NoError(err)

	expected, err := chainhash.NewHashFromStr("938fb93364bf8184e0b649c799ae27274e8db5221f1723c99fb2acd3386cfb00")
	r.NoError(err)
	r.Equal(expected[:], ct.MerkleHash()[:])
}

func TestNormalizationFork(t *testing.T) {

	r := require.New(t)

	setup(t)
	param.NormalizedNameForkHeight = 2
	ct, err := New(cfg)
	r.NoError(err)
	r.NotNil(ct)
	defer func() {
		err = ct.Close()
		r.NoError(err)
	}()

	hash := chainhash.HashH([]byte{1, 2, 3})

	o1 := wire.OutPoint{Hash: hash, Index: 1}
	err = ct.AddClaim([]byte("AÑEJO"), o1, change.NewClaimID(o1), 10)
	r.NoError(err)

	o2 := wire.OutPoint{Hash: hash, Index: 2}
	err = ct.AddClaim([]byte("AÑejo"), o2, change.NewClaimID(o2), 5)
	r.NoError(err)

	o3 := wire.OutPoint{Hash: hash, Index: 3}
	err = ct.AddClaim([]byte("あてはまる"), o3, change.NewClaimID(o3), 5)
	r.NoError(err)

	o4 := wire.OutPoint{Hash: hash, Index: 4}
	err = ct.AddClaim([]byte("Aḿlie"), o4, change.NewClaimID(o4), 5)
	r.NoError(err)

	o5 := wire.OutPoint{Hash: hash, Index: 5}
	err = ct.AddClaim([]byte("TEST"), o5, change.NewClaimID(o5), 5)
	r.NoError(err)

	o6 := wire.OutPoint{Hash: hash, Index: 6}
	err = ct.AddClaim([]byte("test"), o6, change.NewClaimID(o6), 7)
	r.NoError(err)

	err = ct.AppendBlock()
	r.NoError(err)
	r.NotEqual(merkletrie.EmptyTrieHash[:], ct.MerkleHash()[:])

	n, err := ct.nodeManager.Node([]byte("AÑEJO"))
	r.NoError(err)
	r.NotNil(n.BestClaim)
	r.Equal(int32(1), n.TakenOverAt)

	o7 := wire.OutPoint{Hash: hash, Index: 7}
	err = ct.AddClaim([]byte("aÑEJO"), o7, change.NewClaimID(o7), 8)
	r.NoError(err)

	err = ct.AppendBlock()
	r.NoError(err)
	r.NotEqual(merkletrie.EmptyTrieHash[:], ct.MerkleHash()[:])

	n, err = ct.nodeManager.Node([]byte("añejo"))
	r.NoError(err)
	r.Equal(3, len(n.Claims))
	r.Equal(uint32(1), n.BestClaim.OutPoint.Index)
	r.Equal(int32(2), n.TakenOverAt)
}

func TestActivationsOnNormalizationFork(t *testing.T) {

	r := require.New(t)

	setup(t)
	param.NormalizedNameForkHeight = 4
	ct, err := New(cfg)
	r.NoError(err)
	r.NotNil(ct)
	defer func() {
		err = ct.Close()
		r.NoError(err)
	}()

	hash := chainhash.HashH([]byte{1, 2, 3})

	o7 := wire.OutPoint{Hash: hash, Index: 7}
	err = ct.AddClaim([]byte("A"), o7, change.NewClaimID(o7), 1)
	r.NoError(err)
	err = ct.AppendBlock()
	r.NoError(err)
	err = ct.AppendBlock()
	r.NoError(err)
	err = ct.AppendBlock()
	r.NoError(err)
	verifyBestIndex(t, ct, "A", 7, 1)

	o8 := wire.OutPoint{Hash: hash, Index: 8}
	err = ct.AddClaim([]byte("A"), o8, change.NewClaimID(o8), 2)
	r.NoError(err)
	err = ct.AppendBlock()
	r.NoError(err)
	verifyBestIndex(t, ct, "a", 8, 2)

	err = ct.AppendBlock()
	r.NoError(err)
	err = ct.AppendBlock()
	r.NoError(err)
	verifyBestIndex(t, ct, "a", 8, 2)

	err = ct.ResetHeight(3)
	r.NoError(err)
	verifyBestIndex(t, ct, "A", 7, 1)
}

func TestNormalizationSortOrder(t *testing.T) {

	r := require.New(t)
	// this was an unfortunate bug; the normalization fork should not have activated anything
	// alas, it's now part of our history; we hereby test it to keep it that way
	setup(t)
	param.NormalizedNameForkHeight = 2
	ct, err := New(cfg)
	r.NoError(err)
	r.NotNil(ct)
	defer func() {
		err := ct.Close()
		r.NoError(err)
	}()

	hash := chainhash.HashH([]byte{1, 2, 3})

	o1 := wire.OutPoint{Hash: hash, Index: 1}
	err = ct.AddClaim([]byte("A"), o1, change.NewClaimID(o1), 1)
	r.NoError(err)

	o2 := wire.OutPoint{Hash: hash, Index: 2}
	err = ct.AddClaim([]byte("A"), o2, change.NewClaimID(o2), 2)
	r.NoError(err)

	o3 := wire.OutPoint{Hash: hash, Index: 3}
	err = ct.AddClaim([]byte("a"), o3, change.NewClaimID(o3), 3)
	r.NoError(err)

	err = ct.AppendBlock()
	r.NoError(err)
	verifyBestIndex(t, ct, "A", 2, 2)
	verifyBestIndex(t, ct, "a", 3, 1)

	err = ct.AppendBlock()
	r.NoError(err)
	verifyBestIndex(t, ct, "a", 3, 3)
}

func verifyBestIndex(t *testing.T, ct *ClaimTrie, name string, idx uint32, claims int) {

	r := require.New(t)

	n, err := ct.nodeManager.Node([]byte(name))
	r.NoError(err)
	r.Equal(claims, len(n.Claims))
	if claims > 0 {
		r.Equal(idx, n.BestClaim.OutPoint.Index)
	}
}

func TestRebuild(t *testing.T) {
	r := require.New(t)
	setup(t)
	ct, err := New(cfg)
	r.NoError(err)
	r.NotNil(ct)
	defer func() {
		err := ct.Close()
		r.NoError(err)
	}()

	hash := chainhash.HashH([]byte{1, 2, 3})

	o1 := wire.OutPoint{Hash: hash, Index: 1}
	err = ct.AddClaim([]byte("test1"), o1, change.NewClaimID(o1), 1)
	r.NoError(err)

	o2 := wire.OutPoint{Hash: hash, Index: 2}
	err = ct.AddClaim([]byte("test2"), o2, change.NewClaimID(o2), 2)
	r.NoError(err)

	err = ct.AppendBlock()
	r.NoError(err)

	m := ct.MerkleHash()
	r.NotNil(m)
	r.NotEqual(*merkletrie.EmptyTrieHash, *m)

	ct.merkleTrie = merkletrie.NewRamTrie(ct.nodeManager)
	ct.merkleTrie.SetRoot(m, nil)

	m2 := ct.MerkleHash()
	r.NotNil(m2)
	r.Equal(*m, *m2)
}