package notary

import (
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/internal/fakechain"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config/netmode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/mempool"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativehashes"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
)

func TestWallet(t *testing.T) {
	bc := fakechain.NewFakeChain()
	mainCfg := config.P2PNotary{Enabled: true}
	cfg := Config{
		MainCfg: mainCfg,
		Chain:   bc,
		Log:     zaptest.NewLogger(t),
	}
	t.Run("unexisting wallet", func(t *testing.T) {
		cfg.MainCfg.UnlockWallet.Path = "./testdata/does_not_exists.json"
		_, err := NewNotary(cfg, netmode.UnitTestNet, mempool.New(1, 1, true, nil), nil)
		require.Error(t, err)
	})

	t.Run("bad password", func(t *testing.T) {
		cfg.MainCfg.UnlockWallet.Path = "./testdata/notary1.json"
		cfg.MainCfg.UnlockWallet.Password = "invalid"
		_, err := NewNotary(cfg, netmode.UnitTestNet, mempool.New(1, 1, true, nil), nil)
		require.Error(t, err)
	})

	t.Run("good", func(t *testing.T) {
		cfg.MainCfg.UnlockWallet.Path = "./testdata/notary1.json"
		cfg.MainCfg.UnlockWallet.Password = "one"
		_, err := NewNotary(cfg, netmode.UnitTestNet, mempool.New(1, 1, true, nil), nil)
		require.NoError(t, err)
	})
}

func TestVerifyIncompleteRequest(t *testing.T) {
	bc := fakechain.NewFakeChain()
	notaryContractHash := nativehashes.Notary
	_, ntr, _ := getTestNotary(t, bc, "./testdata/notary1.json", "one")
	sig := append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...) // we're not interested in signature correctness
	acc1, _ := keys.NewPrivateKey()
	acc2, _ := keys.NewPrivateKey()
	acc3, _ := keys.NewPrivateKey()
	sigScript1 := acc1.PublicKey().GetVerificationScript()
	sigScript2 := acc2.PublicKey().GetVerificationScript()
	sigScript3 := acc3.PublicKey().GetVerificationScript()
	multisigScript1, err := smartcontract.CreateMultiSigRedeemScript(1, keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()})
	require.NoError(t, err)
	multisigScriptHash1 := hash.Hash160(multisigScript1)
	multisigScript2, err := smartcontract.CreateMultiSigRedeemScript(2, keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()})
	require.NoError(t, err)
	multisigScriptHash2 := hash.Hash160(multisigScript2)

	checkErr := func(t *testing.T, tx *transaction.Transaction, nKeys uint8) {
		witnessInfo, err := ntr.verifyIncompleteWitnesses(tx, nKeys)
		require.Error(t, err)
		require.Nil(t, witnessInfo)
	}

	errCases := map[string]struct {
		tx    *transaction.Transaction
		nKeys uint8
	}{
		"not enough signers": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: notaryContractHash}},
				Scripts: []transaction.Witness{{}},
			},
		},
		"missing Notary witness": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: acc2.GetScriptHash()}},
				Scripts: []transaction.Witness{{}, {}},
			},
		},
		"bad verification script": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   []byte{},
						VerificationScript: []byte{1, 2, 3},
					},
					{},
				},
			},
		},
		"sig: bad nKeys": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: acc2.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: sigScript2,
					},
					{},
				},
			},
			nKeys: 3,
		},
		"multisig: bad witnesses count": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
				},
			},
			nKeys: 2,
		},
		"multisig: bad nKeys": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{},
				},
			},
			nKeys: 2,
		},
	}

	for name, errCase := range errCases {
		t.Run(name, func(t *testing.T) {
			checkErr(t, errCase.tx, errCase.nKeys)
		})
	}

	testCases := map[string]struct {
		tx           *transaction.Transaction
		nKeys        uint8
		expectedInfo []witnessInfo
	}{
		"single sig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{},
				},
			},
			nKeys: 1,
			expectedInfo: []witnessInfo{
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Contract},
			},
		},
		"multiple sig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: acc2.GetScriptHash()}, {Account: acc3.GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   []byte{},
						VerificationScript: sigScript2,
					},
					{
						InvocationScript:   sig,
						VerificationScript: sigScript3,
					},
					{},
				},
			},
			nKeys: 3,
			expectedInfo: []witnessInfo{
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc2.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"single multisig 1 out of 3": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{},
				},
			},
			nKeys: 3,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"single multisig 2 out of 3": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash2}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript2,
					},
					{},
				},
			},
			nKeys: 3,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 2, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"empty sig + single multisig 1 out of 3": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   []byte{},
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{},
				},
			},
			nKeys: 1 + 3,
			expectedInfo: []witnessInfo{
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"single multisig 1 out of 3 + empty single sig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{
						InvocationScript:   []byte{},
						VerificationScript: sigScript1,
					},
					{},
				},
			},
			nKeys: 3 + 1,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Contract},
			},
		},
		"several multisig witnesses": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: multisigScriptHash2}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript2,
					},
					{},
				},
			},
			nKeys: 3 + 3,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: MultiSignature, nSigsLeft: 2, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"multisig + sig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{},
				},
			},
			nKeys: 3 + 1,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Contract},
			},
		},
		"sig + multisig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{},
				},
			},
			nKeys: 1 + 3,
			expectedInfo: []witnessInfo{
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"empty multisig + sig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   []byte{},
						VerificationScript: multisigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{},
				},
			},
			nKeys: 3 + 1,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Contract},
			},
		},
		"sig + empty multisig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   []byte{},
						VerificationScript: multisigScript1,
					},
					{},
				},
			},
			nKeys: 1 + 3,
			expectedInfo: []witnessInfo{
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"multisig + empty sig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{
						InvocationScript:   []byte{},
						VerificationScript: sigScript1,
					},
					{},
				},
			},
			nKeys: 3 + 1,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Contract},
			},
		},
		"empty sig + multisig": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   []byte{},
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{},
				},
			},
			nKeys: 1 + 3,
			expectedInfo: []witnessInfo{
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
		"multiple sigs + multiple multisigs": {
			tx: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: multisigScriptHash1},
					{Account: acc1.PublicKey().GetScriptHash()},
					{Account: acc2.PublicKey().GetScriptHash()},
					{Account: acc3.PublicKey().GetScriptHash()},
					{Account: multisigScriptHash2},
					{Account: notaryContractHash}},
				Scripts: []transaction.Witness{
					{
						InvocationScript:   sig,
						VerificationScript: multisigScript1,
					},
					{
						InvocationScript:   sig,
						VerificationScript: sigScript1,
					},
					{
						InvocationScript:   []byte{},
						VerificationScript: sigScript2,
					},
					{
						InvocationScript:   sig,
						VerificationScript: sigScript3,
					},
					{
						InvocationScript:   []byte{},
						VerificationScript: multisigScript2,
					},
					{},
				},
			},
			nKeys: 3 + 1 + 1 + 1 + 3,
			expectedInfo: []witnessInfo{
				{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc2.PublicKey()}},
				{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc3.PublicKey()}},
				{typ: MultiSignature, nSigsLeft: 2, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
				{typ: Contract},
			},
		},
	}

	for name, testCase := range testCases {
		t.Run(name, func(t *testing.T) {
			actualInfo, err := ntr.verifyIncompleteWitnesses(testCase.tx, testCase.nKeys)
			require.NoError(t, err)
			require.Equal(t, len(testCase.expectedInfo), len(actualInfo))
			for i, expected := range testCase.expectedInfo {
				actual := actualInfo[i]
				require.Equal(t, expected.typ, actual.typ)
				require.Equal(t, expected.nSigsLeft, actual.nSigsLeft)
				require.ElementsMatch(t, expected.pubs, actual.pubs)
				require.Nil(t, actual.sigs)
			}
		})
	}
}
