package rpc

import (
	"context"
	"encoding/hex"
	"fmt"
	"testing"

	ics23 "github.com/confio/ics23/go"
	"github.com/cosmos/iavl"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	dbm "github.com/tendermint/tm-db"

	abci "github.com/tendermint/tendermint/abci/types"
	"github.com/tendermint/tendermint/crypto/merkle"
	"github.com/tendermint/tendermint/libs/bytes"
	lcmock "github.com/tendermint/tendermint/light/rpc/mocks"
	tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto"
	rpcmock "github.com/tendermint/tendermint/rpc/client/mocks"
	ctypes "github.com/tendermint/tendermint/rpc/core/types"
	"github.com/tendermint/tendermint/types"
)

// TestABCIQuery tests ABCIQuery requests and verifies proofs. HAPPY PATH 😀
func TestABCIQuery(t *testing.T) {
	tree, err := iavl.NewMutableTree(dbm.NewMemDB(), 100)
	require.NoError(t, err)

	var (
		key   = []byte("foo")
		value = []byte("bar")
	)
	tree.Set(key, value)

	commitmentProof, err := tree.GetMembershipProof(key)
	require.NoError(t, err)

	op := &testOp{
		Spec:  ics23.IavlSpec,
		Key:   key,
		Proof: commitmentProof,
	}

	next := &rpcmock.Client{}
	next.On(
		"ABCIQueryWithOptions",
		context.Background(),
		mock.AnythingOfType("string"),
		bytes.HexBytes(key),
		mock.AnythingOfType("client.ABCIQueryOptions"),
	).Return(&ctypes.ResultABCIQuery{
		Response: abci.ResponseQuery{
			Code:   0,
			Key:    key,
			Value:  value,
			Height: 1,
			ProofOps: &tmcrypto.ProofOps{
				Ops: []tmcrypto.ProofOp{op.ProofOp()},
			},
		},
	}, nil)

	lc := &lcmock.LightClient{}
	appHash, _ := hex.DecodeString("5EFD44055350B5CC34DBD26085347A9DBBE44EA192B9286A9FC107F40EA1FAC5")
	lc.On("VerifyLightBlockAtHeight", context.Background(), int64(2), mock.AnythingOfType("time.Time")).Return(
		&types.LightBlock{
			SignedHeader: &types.SignedHeader{
				Header: &types.Header{AppHash: appHash},
			},
		},
		nil,
	)

	c := NewClient(next, lc,
		KeyPathFn(func(_ string, key []byte) (merkle.KeyPath, error) {
			kp := merkle.KeyPath{}
			kp = kp.AppendKey(key, merkle.KeyEncodingURL)
			return kp, nil
		}))
	c.RegisterOpDecoder("ics23:iavl", testOpDecoder)
	res, err := c.ABCIQuery(context.Background(), "/store/accounts/key", key)
	require.NoError(t, err)

	assert.NotNil(t, res)
}

type testOp struct {
	Spec  *ics23.ProofSpec
	Key   []byte
	Proof *ics23.CommitmentProof
}

var _ merkle.ProofOperator = testOp{}

func (op testOp) GetKey() []byte {
	return op.Key
}

func (op testOp) ProofOp() tmcrypto.ProofOp {
	bz, err := op.Proof.Marshal()
	if err != nil {
		panic(err.Error())
	}
	return tmcrypto.ProofOp{
		Type: "ics23:iavl",
		Key:  op.Key,
		Data: bz,
	}
}

func (op testOp) Run(args [][]byte) ([][]byte, error) {
	// calculate root from proof
	root, err := op.Proof.Calculate()
	if err != nil {
		return nil, fmt.Errorf("could not calculate root for proof: %v", err)
	}
	// Only support an existence proof or nonexistence proof (batch proofs currently unsupported)
	switch len(args) {
	case 0:
		// Args are nil, so we verify the absence of the key.
		absent := ics23.VerifyNonMembership(op.Spec, root, op.Proof, op.Key)
		if !absent {
			return nil, fmt.Errorf("proof did not verify absence of key: %s", string(op.Key))
		}
	case 1:
		// Args is length 1, verify existence of key with value args[0]
		if !ics23.VerifyMembership(op.Spec, root, op.Proof, op.Key, args[0]) {
			return nil, fmt.Errorf("proof did not verify existence of key %s with given value %x", op.Key, args[0])
		}
	default:
		return nil, fmt.Errorf("args must be length 0 or 1, got: %d", len(args))
	}

	return [][]byte{root}, nil
}

func testOpDecoder(pop tmcrypto.ProofOp) (merkle.ProofOperator, error) {
	proof := &ics23.CommitmentProof{}
	err := proof.Unmarshal(pop.Data)
	if err != nil {
		return nil, err
	}

	op := testOp{
		Key:   pop.Key,
		Spec:  ics23.IavlSpec,
		Proof: proof,
	}
	return op, nil
}
