package native_test

import (
	"encoding/json"
	"math"
	"math/big"
	"path/filepath"
	"strings"
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/internal/contracts"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/noderoles"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

var pathToInternalContracts = filepath.Join("..", "..", "..", "..", "internal", "contracts")

func newOracleClient(t *testing.T) *tutustest.ContractInvoker {
	return newNativeClient(t, nativenames.Oracle)
}

func TestOracle_GetSetPrice(t *testing.T) {
	testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64)
}

func TestOracle_GetSetPriceCache(t *testing.T) {
	testGetSetCache(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice)
}

func putOracleRequest(t *testing.T, oracleInvoker *tutustest.ContractInvoker,
	url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) {
	var filtItem any
	if filter != nil {
		filtItem = *filter
	}
	if len(errStr) == 0 {
		oracleInvoker.Invoke(t, stackitem.Null{}, "requestURL", url, filtItem, cb, userData, gas)
		return
	}
	oracleInvoker.InvokeFail(t, errStr[0], "requestURL", url, filtItem, cb, userData, gas)
}

func TestOracle_Request(t *testing.T) {
	oracleCommitteeInvoker := newOracleClient(t)
	e := oracleCommitteeInvoker.Executor
	managementCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Management))
	designationCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation))
	lubCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Lub))

	cs := contracts.GetOracleContractState(t, pathToInternalContracts, e.Validator.ScriptHash(), 1)
	nBytes, err := cs.NEF.Bytes()
	require.NoError(t, err)
	mBytes, err := json.Marshal(cs.Manifest)
	require.NoError(t, err)
	expected, err := cs.ToStackItem()
	require.NoError(t, err)
	managementCommitteeInvoker.Invoke(t, expected, "deploy", nBytes, mBytes)
	helperValidatorInvoker := e.ValidatorInvoker(cs.Hash)

	gasForResponse := int64(2000_1234)
	var filter = "flt"
	userData := []byte("custom info")
	putOracleRequest(t, helperValidatorInvoker, "url", &filter, "handle", userData, gasForResponse)

	// Designate single Oracle node.
	oracleNode := e.NewAccount(t)
	designationCommitteeInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int(noderoles.Oracle), []any{oracleNode.(tutustest.SingleSigner).Account().PublicKey().Bytes()})
	err = oracleNode.(tutustest.SingleSigner).Account().ConvertMultisig(1, []*keys.PublicKey{oracleNode.(tutustest.SingleSigner).Account().PublicKey()})
	require.NoError(t, err)
	oracleNodeMulti := tutustest.NewMultiSigner(oracleNode.(tutustest.SingleSigner).Account())
	lubCommitteeInvoker.Invoke(t, true, "transfer", lubCommitteeInvoker.CommitteeHash, oracleNodeMulti.ScriptHash(), 100_0000_0000, nil)

	// Finish.
	prepareResponseTx := func(t *testing.T, requestID uint64) *transaction.Transaction {
		script := native.CreateOracleResponseScript(oracleCommitteeInvoker.Hash)

		tx := transaction.New(script, 1000_0000)
		tx.Nonce = tutustest.Nonce()
		tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
		tx.Attributes = []transaction.Attribute{{
			Type: transaction.OracleResponseT,
			Value: &transaction.OracleResponse{
				ID:     requestID,
				Code:   transaction.Success,
				Result: []byte{4, 8, 15, 16, 23, 42},
			},
		}}
		tx.Signers = []transaction.Signer{
			{
				Account: oracleNodeMulti.ScriptHash(),
				Scopes:  transaction.None,
			},
			{
				Account: oracleCommitteeInvoker.Hash,
				Scopes:  transaction.None,
			},
		}
		tx.NetworkFee = 1000_1234
		tx.Scripts = []transaction.Witness{
			{
				InvocationScript:   oracleNodeMulti.SignHashable(uint32(e.Chain.GetConfig().Magic), tx),
				VerificationScript: oracleNodeMulti.Script(),
			},
			{
				InvocationScript:   []byte{},
				VerificationScript: []byte{},
			},
		}
		return tx
	}
	tx := prepareResponseTx(t, 0)
	e.AddNewBlock(t, tx)
	e.CheckHalt(t, tx.Hash(), stackitem.Null{})

	// Ensure that callback was called.
	si := e.Chain.GetStorageItem(cs.ID, []byte("lastOracleResponse"))
	require.NotNil(t, si)
	actual, err := stackitem.Deserialize(si)
	require.NoError(t, err)
	require.Equal(t, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray([]byte("url")),
		stackitem.NewByteArray(userData),
		stackitem.NewBigInteger(big.NewInt(int64(tx.Attributes[0].Value.(*transaction.OracleResponse).Code))),
		stackitem.NewByteArray(tx.Attributes[0].Value.(*transaction.OracleResponse).Result),
	}), actual)

	// Check that the processed request is removed. We can't access GetRequestInternal directly,
	// but adding a response to this request should fail due to invalid request error.
	tx = prepareResponseTx(t, 0)
	err = e.Chain.VerifyTx(tx)
	require.Error(t, err)
	require.True(t, strings.Contains(err.Error(), "oracle tx points to invalid request"))

	t.Run("ErrorOnFinish", func(t *testing.T) {
		putOracleRequest(t, helperValidatorInvoker, "url", nil, "handle", []byte{1, 2}, gasForResponse)
		tx := prepareResponseTx(t, 1)
		e.AddNewBlock(t, tx)
		e.CheckFault(t, tx.Hash(), "ABORT")

		// Check that the processed request is cleaned up even if callback failed. We can't
		// access GetRequestInternal directly, but adding a response to this request
		// should fail due to invalid request error.
		tx = prepareResponseTx(t, 1)
		err = e.Chain.VerifyTx(tx)
		require.Error(t, err)
		require.True(t, strings.Contains(err.Error(), "oracle tx points to invalid request"))
	})
	t.Run("Reentrant", func(t *testing.T) {
		putOracleRequest(t, helperValidatorInvoker, "url", nil, "handleRecursive", []byte{}, gasForResponse)
		tx := prepareResponseTx(t, 2)
		e.AddNewBlock(t, tx)
		e.CheckFault(t, tx.Hash(), "'Oracle.finish' called from non-entry script")
		aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
		require.NoError(t, err)
		require.Equal(t, 2, len(aer[0].Events)) // OracleResponse + Invocation
	})
	t.Run("BadRequest", func(t *testing.T) {
		t.Run("non-UTF8 url", func(t *testing.T) {
			putOracleRequest(t, helperValidatorInvoker, "\xff", nil, "", []byte{1, 2}, gasForResponse, "invalid value: not UTF-8")
		})
		t.Run("non-UTF8 filter", func(t *testing.T) {
			var f = "\xff"
			putOracleRequest(t, helperValidatorInvoker, "url", &f, "", []byte{1, 2}, gasForResponse, "invalid value: not UTF-8")
		})
		t.Run("not enough gas", func(t *testing.T) {
			putOracleRequest(t, helperValidatorInvoker, "url", nil, "", nil, 1000, "not enough gas for response")
		})
		t.Run("disallowed callback", func(t *testing.T) {
			putOracleRequest(t, helperValidatorInvoker, "url", nil, "_deploy", nil, 1000_0000, "disallowed callback method (starts with '_')")
		})
	})
}
