package actor

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"

	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"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/stackitem"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/stretchr/testify/require"
)

type RPCClient struct {
	err     error
	invRes  *result.Invoke
	netFee  int64
	bCount  atomic.Uint32
	version *result.Version
	hash    util.Uint256
	appLog  *result.ApplicationLog
	context context.Context
}

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.Load(), 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) 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 {
	if r.context == nil {
		return context.Background()
	}
	return r.context
}

func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*result.ApplicationLog, error) {
	if r.appLog != nil {
		return r.appLog, nil
	}
	return nil, errors.New("not found")
}
func testRPCAndAccount(t *testing.T) (*RPCClient, *wallet.Account) {
	client := &RPCClient{
		version: &result.Version{
			Protocol: result.Protocol{
				Network:              netmode.UnitTestNet,
				MillisecondsPerBlock: 1000,
				ValidatorsCount:      7,
			},
		},
	}
	acc, err := wallet.NewAccount()
	require.NoError(t, err)
	return client, acc
}

func TestNew(t *testing.T) {
	client, acc := testRPCAndAccount(t)

	// No signers.
	_, err := New(client, nil)
	require.Error(t, err)

	_, err = New(client, []SignerAccount{})
	require.Error(t, err)

	_, err = NewTuned(client, []SignerAccount{}, NewDefaultOptions())
	require.Error(t, err)

	// Good simple.
	a, err := NewSimple(client, acc)
	require.NoError(t, err)
	require.Equal(t, 1, len(a.signers))
	require.Equal(t, 1, len(a.txSigners))
	require.Equal(t, transaction.CalledByEntry, a.signers[0].Signer.Scopes)
	require.Equal(t, transaction.CalledByEntry, a.txSigners[0].Scopes)

	// Contractless account.
	badAcc, err := wallet.NewAccount()
	require.NoError(t, err)
	badAccHash := badAcc.Contract.ScriptHash()
	badAcc.Contract = nil

	signers := []SignerAccount{{
		Signer: transaction.Signer{
			Account: acc.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc,
	}, {
		Signer: transaction.Signer{
			Account: badAccHash,
			Scopes:  transaction.CalledByEntry,
		},
		Account: badAcc,
	}}

	_, err = New(client, signers)
	require.Error(t, err)

	// GetVersion returning error.
	client.err = errors.New("bad")
	_, err = NewSimple(client, acc)
	require.Error(t, err)
	client.err = nil

	// Account mismatch.
	acc2, err := wallet.NewAccount()
	require.NoError(t, err)
	signers = []SignerAccount{{
		Signer: transaction.Signer{
			Account: acc2.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc,
	}, {
		Signer: transaction.Signer{
			Account: acc2.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: acc2,
	}}
	_, err = New(client, signers)
	require.Error(t, err)

	// Good multiaccount.
	signers[0].Signer.Account = acc.Contract.ScriptHash()
	a, err = New(client, signers)
	require.NoError(t, err)
	require.Equal(t, 2, len(a.signers))
	require.Equal(t, 2, len(a.txSigners))

	// Good tuned
	opts := Options{
		Attributes: []transaction.Attribute{{Type: transaction.HighPriority}},
	}
	a, err = NewTuned(client, signers, opts)
	require.NoError(t, err)
	require.Equal(t, 1, len(a.opts.Attributes))
}

func TestSimpleWrappers(t *testing.T) {
	client, acc := testRPCAndAccount(t)
	origVer := *client.version

	a, err := NewSimple(client, acc)
	require.NoError(t, err)

	client.netFee = 42
	nf, err := a.CalculateNetworkFee(new(transaction.Transaction))
	require.NoError(t, err)
	require.Equal(t, int64(42), nf)

	client.bCount.Store(100500)
	bc, err := a.GetBlockCount()
	require.NoError(t, err)
	require.Equal(t, uint32(100500), bc)

	require.Equal(t, netmode.UnitTestNet, a.GetNetwork())
	client.version.Protocol.Network = netmode.TestNet
	require.Equal(t, netmode.UnitTestNet, a.GetNetwork())
	require.Equal(t, origVer, a.GetVersion())

	a, err = NewSimple(client, acc)
	require.NoError(t, err)
	require.Equal(t, netmode.TestNet, a.GetNetwork())
	require.Equal(t, *client.version, a.GetVersion())

	client.hash = util.Uint256{1, 2, 3}
	h, vub, err := a.Send(&transaction.Transaction{ValidUntilBlock: 123})
	require.NoError(t, err)
	require.Equal(t, client.hash, h)
	require.Equal(t, uint32(123), vub)
}

func TestSign(t *testing.T) {
	client, acc := testRPCAndAccount(t)
	acc2, err := wallet.NewAccount()
	require.NoError(t, err)

	a, err := New(client, []SignerAccount{{
		Signer: transaction.Signer{
			Account: acc.Contract.ScriptHash(),
			Scopes:  transaction.None,
		},
		Account: acc,
	}, {
		Signer: transaction.Signer{
			Account: acc2.Contract.ScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
		Account: &wallet.Account{ // Looks like acc2, but has no private key.
			Address:      acc2.Address,
			EncryptedWIF: acc2.EncryptedWIF,
			Contract:     acc2.Contract,
		},
	}})
	require.NoError(t, err)

	script := []byte{1, 2, 3}
	client.hash = util.Uint256{2, 5, 6}
	client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}

	tx, err := a.MakeUnsignedRun(script, nil)
	require.NoError(t, err)
	require.Error(t, a.Sign(tx))
	_, _, err = a.SignAndSend(tx)
	require.Error(t, err)
}

func TestSenders(t *testing.T) {
	client, acc := testRPCAndAccount(t)
	a, err := NewSimple(client, acc)
	require.NoError(t, err)
	script := []byte{1, 2, 3}

	// Bad.
	client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script}
	_, _, err = a.SendCall(util.Uint160{1}, "method", 42)
	require.Error(t, err)
	_, _, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42)
	require.Error(t, err)
	_, _, err = a.SendRun(script)
	require.Error(t, err)
	_, _, err = a.SendTunedRun(script, nil, nil)
	require.Error(t, err)
	_, _, err = a.SendUncheckedRun(script, 1, nil, func(t *transaction.Transaction) error {
		return errors.New("bad")
	})
	require.Error(t, err)

	// Good.
	client.hash = util.Uint256{2, 5, 6}
	client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
	h, vub, err := a.SendCall(util.Uint160{1}, "method", 42)
	require.NoError(t, err)
	require.Equal(t, client.hash, h)
	require.Equal(t, uint32(8), vub)

	h, vub, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42)
	require.NoError(t, err)
	require.Equal(t, client.hash, h)
	require.Equal(t, uint32(8), vub)

	h, vub, err = a.SendRun(script)
	require.NoError(t, err)
	require.Equal(t, client.hash, h)
	require.Equal(t, uint32(8), vub)

	h, vub, err = a.SendTunedRun(script, nil, nil)
	require.NoError(t, err)
	require.Equal(t, client.hash, h)
	require.Equal(t, uint32(8), vub)

	h, vub, err = a.SendUncheckedRun(script, 1, nil, nil)
	require.NoError(t, err)
	require.Equal(t, client.hash, h)
	require.Equal(t, uint32(8), vub)
}

func TestSender(t *testing.T) {
	client, acc := testRPCAndAccount(t)
	a, err := NewSimple(client, acc)
	require.NoError(t, err)
	require.Equal(t, acc.ScriptHash(), a.Sender())
}