package notary

import (
	"context"
	"errors"
	"testing"

	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/network/payload"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/stretchr/testify/require"
)

type RPCClient struct {
	err     error
	invRes  *result.Invoke
	netFee  int64
	bCount  uint32
	version *result.Version
	hash    util.Uint256
	nhash   util.Uint256
	mirror  bool
	applog  *result.ApplicationLog
}

func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
	return r.invRes, r.err
}
func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
	return r.invRes, r.err
}
func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
	return r.invRes, r.err
}
func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
	return r.netFee, r.err
}
func (r *RPCClient) GetBlockCount() (uint32, error) {
	return r.bCount, r.err
}
func (r *RPCClient) GetVersion() (*result.Version, error) {
	verCopy := *r.version
	return &verCopy, r.err
}
func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
	return r.hash, r.err
}
func (r *RPCClient) SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint256, error) {
	if r.mirror {
		return req.FallbackTransaction.Hash(), nil
	}
	return r.nhash, r.err
}
func (r *RPCClient) TerminateSession(sessionID uuid.UUID) (bool, error) {
	return false, nil // Just a stub, unused by actor.
}
func (r *RPCClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
	return nil, nil // Just a stub, unused by actor.
}
func (r *RPCClient) Context() context.Context {
	return context.Background()
}
func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*result.ApplicationLog, error) {
	return r.applog, nil
}

var _ = waiter.RPCPollingBased(&RPCClient{})

func TestNewActor(t *testing.T) {
	rc := &RPCClient{
		version: &result.Version{
			Protocol: result.Protocol{
				Network:              netmode.UnitTestNet,
				MillisecondsPerBlock: 1000,
				ValidatorsCount:      7,
			},
		},
	}

	_, err := NewActor(rc, nil, nil)
	require.Error(t, err)

	var (
		keyz  [4]*keys.PrivateKey
		accs  [4]*wallet.Account
		faccs [4]*wallet.Account
		pkeys [4]*keys.PublicKey
	)
	for i := range accs {
		keyz[i], err = keys.NewPrivateKey()
		require.NoError(t, err)
		accs[i] = wallet.NewAccountFromPrivateKey(keyz[i])
		pkeys[i] = keyz[i].PublicKey()
		faccs[i] = FakeSimpleAccount(pkeys[i])
	}
	var multiAccs [4]*wallet.Account
	for i := range accs {
		multiAccs[i] = &wallet.Account{}
		*multiAccs[i] = *accs[i]
		require.NoError(t, multiAccs[i].ConvertMultisig(smartcontract.GetDefaultHonestNodeCount(len(pkeys)), pkeys[:]))
	}

	// nil Contract
	badMultiAcc0 := &wallet.Account{}
	*badMultiAcc0 = *multiAccs[0]
	badMultiAcc0.Contract = nil
	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: multiAccs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: badMultiAcc0,
	}}, accs[0])
	require.Error(t, err)

	// Non-standard script.
	badMultiAcc0.Contract = &wallet.Contract{}
	*badMultiAcc0.Contract = *multiAccs[0].Contract
	badMultiAcc0.Contract.Script = append(badMultiAcc0.Contract.Script, byte(opcode.NOP))
	badMultiAcc0.Address = address.Uint160ToString(badMultiAcc0.Contract.ScriptHash())
	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: badMultiAcc0.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: badMultiAcc0,
	}}, accs[0])
	require.Error(t, err)

	// Too many keys
	var (
		manyKeys  [256]*keys.PrivateKey
		manyPkeys [256]*keys.PublicKey
	)
	for i := range manyKeys {
		manyKeys[i], err = keys.NewPrivateKey()
		require.NoError(t, err)
		manyPkeys[i] = manyKeys[i].PublicKey()
	}
	bigMultiAcc := &wallet.Account{}
	*bigMultiAcc = *wallet.NewAccountFromPrivateKey(manyKeys[0])
	require.NoError(t, bigMultiAcc.ConvertMultisig(129, manyPkeys[:]))

	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: bigMultiAcc.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: bigMultiAcc,
	}}, wallet.NewAccountFromPrivateKey(manyKeys[0]))
	require.Error(t, err)

	// No contract in the simple account.
	badSimple0 := &wallet.Account{}
	*badSimple0 = *accs[0]
	badSimple0.Contract = nil
	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: multiAccs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: multiAccs[0],
	}}, badSimple0)
	require.Error(t, err)

	// Simple account that can't sign.
	badSimple0 = FakeSimpleAccount(pkeys[0])
	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: multiAccs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: multiAccs[0],
	}}, badSimple0)
	require.Error(t, err)

	// Multisig account instead of simple one.
	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: multiAccs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: multiAccs[0],
	}}, multiAccs[0])
	require.Error(t, err)

	// Main actor freaking out on hash mismatch.
	_, err = NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: accs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: multiAccs[0],
	}}, accs[0])
	require.Error(t, err)

	// FB actor freaking out on hash mismatch.
	opts := NewDefaultActorOptions(NewReader(invoker.New(rc, nil)), accs[0])
	opts.FbSigner.Signer.Account = multiAccs[0].Contract.ScriptHash()
	_, err = NewTunedActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: multiAccs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: multiAccs[0],
	}}, opts)
	require.Error(t, err)

	// Good, one multisig.
	multi0, err := NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: multiAccs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: multiAccs[0],
	}}, accs[0])
	require.NoError(t, err)

	script := []byte{byte(opcode.RET)}
	rc.invRes = &result.Invoke{
		State:       "HALT",
		GasConsumed: 3,
		Script:      script,
		Stack:       []stackitem.Item{stackitem.Make(42)},
	}
	tx, err := multi0.MakeRun(script)
	require.NoError(t, err)
	require.Equal(t, 1, len(tx.Attributes))
	require.Equal(t, transaction.NotaryAssistedT, tx.Attributes[0].Type)
	require.Equal(t, &transaction.NotaryAssisted{NKeys: 4}, tx.Attributes[0].Value)

	// Good, 4 single sigs with one that can sign and one contract.
	single4, err := NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: accs[0].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: accs[0],
	}, {
		Signer: transaction.Signer{
			Account: faccs[1].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: faccs[1],
	}, {
		Signer: transaction.Signer{
			Account: faccs[2].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: faccs[2],
	}, {
		Signer: transaction.Signer{
			Account: accs[3].Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: faccs[3],
	}, {
		Signer: transaction.Signer{
			Account: util.Uint160{1, 2, 3},
			Scopes:  transaction.CalledByEntry,
		},
		Account: FakeContractAccount(util.Uint160{1, 2, 3}),
	}}, accs[0])
	require.NoError(t, err)

	tx, err = single4.MakeRun(script)
	require.NoError(t, err)
	require.Equal(t, 1, len(tx.Attributes))
	require.Equal(t, transaction.NotaryAssistedT, tx.Attributes[0].Type)
	require.Equal(t, &transaction.NotaryAssisted{NKeys: 4}, tx.Attributes[0].Value) // One account can sign, three need to collect additional sigs.
}

func TestSendRequestExactly(t *testing.T) {
	rc := &RPCClient{
		version: &result.Version{
			Protocol: result.Protocol{
				Network:              netmode.UnitTestNet,
				MillisecondsPerBlock: 1000,
				ValidatorsCount:      7,
			},
		},
	}

	key0, err := keys.NewPrivateKey()
	require.NoError(t, err)
	key1, err := keys.NewPrivateKey()
	require.NoError(t, err)

	acc0 := wallet.NewAccountFromPrivateKey(key0)
	facc1 := FakeSimpleAccount(key1.PublicKey())

	act, err := NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: acc0.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc0,
	}, {
		Signer: transaction.Signer{
			Account: facc1.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: facc1,
	}}, acc0)
	require.NoError(t, err)

	script := []byte{byte(opcode.RET)}
	mainTx := transaction.New(script, 1)
	fbTx := transaction.New(script, 1)

	// Hashes mismatch
	_, _, _, err = act.SendRequestExactly(mainTx, fbTx)
	require.Error(t, err)

	// Error returned
	rc.err = errors.New("")
	_, _, _, err = act.SendRequestExactly(mainTx, fbTx)
	require.Error(t, err)

	// OK returned
	rc.err = nil
	rc.nhash = fbTx.Hash()
	mHash, fbHash, vub, err := act.SendRequestExactly(mainTx, fbTx)
	require.NoError(t, err)
	require.Equal(t, mainTx.Hash(), mHash)
	require.Equal(t, fbTx.Hash(), fbHash)
	require.Equal(t, mainTx.ValidUntilBlock, vub)
}

func TestSendRequest(t *testing.T) {
	rc := &RPCClient{
		version: &result.Version{
			Protocol: result.Protocol{
				Network:              netmode.UnitTestNet,
				MillisecondsPerBlock: 1000,
				ValidatorsCount:      7,
			},
		},
		bCount: 42,
	}

	key0, err := keys.NewPrivateKey()
	require.NoError(t, err)
	key1, err := keys.NewPrivateKey()
	require.NoError(t, err)

	acc0 := wallet.NewAccountFromPrivateKey(key0)
	facc0 := FakeSimpleAccount(key0.PublicKey())
	facc1 := FakeSimpleAccount(key1.PublicKey())

	act, err := NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: acc0.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc0,
	}, {
		Signer: transaction.Signer{
			Account: facc1.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: facc1,
	}}, acc0)
	require.NoError(t, err)

	script := []byte{byte(opcode.RET)}
	rc.invRes = &result.Invoke{
		State:       "HALT",
		GasConsumed: 3,
		Script:      script,
		Stack:       []stackitem.Item{stackitem.Make(42)},
	}

	mainTx, err := act.MakeRun(script)
	require.NoError(t, err)

	// No attributes.
	fbTx, err := act.FbActor.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	fbTx.Attributes = nil
	_, _, _, err = act.SendRequest(mainTx, fbTx)
	require.Error(t, err)

	// Bad NVB.
	fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	fbTx.Attributes[1].Type = transaction.HighPriority
	fbTx.Attributes[1].Value = nil
	_, _, _, err = act.SendRequest(mainTx, fbTx)
	require.Error(t, err)

	// Bad Conflicts.
	fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	fbTx.Attributes[2].Type = transaction.HighPriority
	fbTx.Attributes[2].Value = nil
	_, _, _, err = act.SendRequest(mainTx, fbTx)
	require.Error(t, err)

	// GetBlockCount error.
	fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	rc.err = errors.New("")
	_, _, _, err = act.SendRequest(mainTx, fbTx)
	require.Error(t, err)

	// Can't sign suddenly.
	rc.err = nil
	acc0Backup := &wallet.Account{}
	*acc0Backup = *acc0
	*acc0 = *facc0
	fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	_, _, _, err = act.SendRequest(mainTx, fbTx)
	require.Error(t, err)

	// Good.
	*acc0 = *acc0Backup
	fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	_, _, _, err = act.SendRequest(mainTx, fbTx)
	require.Error(t, err)
}

func TestNotarize(t *testing.T) {
	rc := &RPCClient{
		version: &result.Version{
			Protocol: result.Protocol{
				Network:              netmode.UnitTestNet,
				MillisecondsPerBlock: 1000,
				ValidatorsCount:      7,
			},
		},
		bCount: 42,
	}

	key0, err := keys.NewPrivateKey()
	require.NoError(t, err)
	key1, err := keys.NewPrivateKey()
	require.NoError(t, err)

	acc0 := wallet.NewAccountFromPrivateKey(key0)
	facc1 := FakeSimpleAccount(key1.PublicKey())

	act, err := NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: acc0.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc0,
	}, {
		Signer: transaction.Signer{
			Account: facc1.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: facc1,
	}}, acc0)
	require.NoError(t, err)

	script := []byte{byte(opcode.RET)}

	// Immediate error from MakeRun.
	rc.invRes = &result.Invoke{
		State:       "FAULT",
		GasConsumed: 3,
		Script:      script,
		Stack:       []stackitem.Item{stackitem.Make(42)},
	}
	_, _, _, err = act.Notarize(act.MakeRun(script))
	require.Error(t, err)

	// Explicitly good transaction. but failure to create a fallback.
	rc.invRes.State = "HALT"
	tx, err := act.MakeRun(script)
	require.NoError(t, err)

	rc.invRes.State = "FAULT"
	_, _, _, err = act.Notarize(tx, nil)
	require.Error(t, err)

	// FB hash mismatch from SendRequestExactly.
	rc.invRes.State = "HALT"
	_, _, _, err = act.Notarize(act.MakeRun(script))
	require.Error(t, err)

	// Good.
	rc.mirror = true
	mHash, fbHash, vub, err := act.Notarize(act.MakeRun(script))
	require.NoError(t, err)
	require.NotEqual(t, util.Uint256{}, mHash)
	require.NotEqual(t, util.Uint256{}, fbHash)
	require.Equal(t, uint32(92), vub)
}

func TestDefaultActorOptions(t *testing.T) {
	rc := &RPCClient{
		version: &result.Version{
			Protocol: result.Protocol{
				Network:              netmode.UnitTestNet,
				MillisecondsPerBlock: 1000,
				ValidatorsCount:      7,
			},
		},
	}
	acc, err := wallet.NewAccount()
	require.NoError(t, err)
	opts := NewDefaultActorOptions(NewReader(invoker.New(rc, nil)), acc)
	rc.invRes = &result.Invoke{
		State:       "HALT",
		GasConsumed: 3,
		Script:      opts.FbScript,
		Stack:       []stackitem.Item{stackitem.Make(42)},
	}
	tx := transaction.New(opts.FbScript, 1)
	require.Error(t, opts.MainCheckerModifier(&result.Invoke{State: "FAULT"}, tx))
	rc.invRes.State = "FAULT"
	require.Error(t, opts.MainCheckerModifier(&result.Invoke{State: "HALT"}, tx))
	rc.invRes.State = "HALT"
	require.NoError(t, opts.MainCheckerModifier(&result.Invoke{State: "HALT"}, tx))
	require.Equal(t, uint32(42), tx.ValidUntilBlock)
}

func TestWait(t *testing.T) {
	rc := &RPCClient{version: &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}}

	key0, err := keys.NewPrivateKey()
	require.NoError(t, err)
	key1, err := keys.NewPrivateKey()
	require.NoError(t, err)

	acc0 := wallet.NewAccountFromPrivateKey(key0)
	facc1 := FakeSimpleAccount(key1.PublicKey())

	act, err := NewActor(rc, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: acc0.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc0,
	}, {
		Signer: transaction.Signer{
			Account: facc1.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: facc1,
	}}, acc0)
	require.NoError(t, err)

	someErr := errors.New("someErr")
	_, err = act.Wait(util.Uint256{}, util.Uint256{}, 0, someErr)
	require.ErrorIs(t, err, someErr)

	cont := util.Uint256{1, 2, 3}
	ex := state.Execution{
		Trigger:     trigger.Application,
		VMState:     vmstate.Halt,
		GasConsumed: 123,
		Stack:       []stackitem.Item{stackitem.Null{}},
	}
	applog := &result.ApplicationLog{
		Container:     cont,
		IsTransaction: true,
		Executions:    []state.Execution{ex},
	}
	rc.applog = applog
	res, err := act.Wait(util.Uint256{}, util.Uint256{}, 0, nil)
	require.NoError(t, err)
	require.Equal(t, &state.AppExecResult{
		Container: cont,
		Execution: ex,
	}, res)
}