// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

//go:build test

package p

import (
	"math/rand"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/luxfi/constants"
	"github.com/luxfi/crypto/bls"
	"github.com/luxfi/crypto/bls/signer/localsigner"
	"github.com/luxfi/crypto/secp256k1"
	"github.com/luxfi/ids"
	"github.com/luxfi/math/set"
	"github.com/luxfi/node/vms/components/gas"
	lux "github.com/luxfi/utxo"
	"github.com/luxfi/node/vms/platformvm/reward"
	"github.com/luxfi/node/vms/platformvm/stakeable"
	"github.com/luxfi/node/vms/platformvm/txs"
	"github.com/luxfi/node/vms/platformvm/txs/fee"
	"github.com/luxfi/node/vms/platformvm/warp"
	"github.com/luxfi/node/vms/platformvm/warp/message"
	"github.com/luxfi/node/vms/platformvm/warp/payload"
	"github.com/luxfi/node/wallet/chain/p/builder"
	"github.com/luxfi/node/wallet/chain/p/wallet"
	"github.com/luxfi/node/wallet/network/primary/common"
	"github.com/luxfi/node/wallet/network/primary/common/utxotest"
	"github.com/luxfi/utils"
	"github.com/luxfi/node/vms/platformvm/fx"
	"github.com/luxfi/node/vms/platformvm/signer"
	"github.com/luxfi/utxo/secp256k1fx"
	"github.com/luxfi/vm/types"
)

var (
	chainID      = ids.GenerateTestID()
	nodeID       = ids.GenerateTestNodeID()
	validationID = ids.GenerateTestID()

	testKeys      = secp256k1.TestKeys()
	chainAuthKey  = testKeys[0]
	chainAuthAddr = chainAuthKey.Address()
	chainOwner    = &secp256k1fx.OutputOwners{
		Threshold: 1,
		Addrs:     []ids.ShortID{chainAuthAddr},
	}
	importKey   = testKeys[0]
	importAddr  = importKey.Address()
	importOwner = &secp256k1fx.OutputOwners{
		Threshold: 1,
		Addrs:     []ids.ShortID{importAddr},
	}
	rewardKey    = testKeys[0]
	rewardAddr   = rewardKey.Address()
	rewardsOwner = &secp256k1fx.OutputOwners{
		Threshold: 1,
		Addrs:     []ids.ShortID{rewardAddr},
	}
	utxoKey   = testKeys[1]
	utxoAddr  = utxoKey.Address()
	utxoOwner = secp256k1fx.OutputOwners{
		Threshold: 1,
		Addrs:     []ids.ShortID{utxoAddr},
	}
	validationAuthKey  = testKeys[2]
	validationAuthAddr = validationAuthKey.Address()
	validationOwner    = &secp256k1fx.OutputOwners{
		Threshold: 1,
		Addrs:     []ids.ShortID{validationAuthAddr},
	}

	// We hard-code [utxoAssetID] and [chainAssetID] to make ordering of UTXOs
	// generated by [makeTestUTXOs] reproducible.
	utxoAssetID   = ids.Empty.Prefix(1789)
	chainAssetID = ids.Empty.Prefix(2025)
	utxos        = makeTestUTXOs(utxoKey)

	luxOutput = &lux.TransferableOutput{
		Asset: lux.Asset{ID: utxoAssetID},
		Out: &secp256k1fx.TransferOutput{
			Amt:          7 * constants.Lux,
			OutputOwners: utxoOwner,
		},
	}

	chainOwners = map[ids.ID]fx.Owner{
		chainID: chainOwner,
	}
	validationOwners = map[ids.ID]fx.Owner{
		validationID: validationOwner,
	}

	primaryNetworkPermissionlessStaker = &txs.ChainValidator{
		Validator: txs.Validator{
			NodeID: nodeID,
			End:    uint64(time.Now().Add(time.Hour).Unix()),
			Wght:   2 * constants.Lux,
		},
		Chain: constants.PrimaryNetworkID,
	}

	testContext = &builder.Context{
		NetworkID: constants.UnitTestID,
		UTXOAssetID:  utxoAssetID,

		ComplexityWeights: gas.Dimensions{
			gas.Bandwidth: 1,
			gas.DBRead:    10,
			gas.DBWrite:   100,
			gas.Compute:   1000,
		},
		GasPrice: 1,
	}
	dynamicFeeCalculator = fee.NewDynamicCalculator(
		testContext.ComplexityWeights,
		testContext.GasPrice,
	)

	testEnvironment = []environment{
		{
			name:          "default",
			context:       testContext,
			feeCalculator: dynamicFeeCalculator,
		},
		{
			name:          "default with memo",
			context:       testContext,
			feeCalculator: dynamicFeeCalculator,
			memo:          []byte("memo"),
		},
	}
)

type environment struct {
	name          string
	context       *builder.Context
	feeCalculator fee.Calculator
	memo          []byte
}

// These tests create a tx, then verify that utxos included in the tx are
// exactly necessary to pay fees for it.

func TestBaseTx(t *testing.T) {
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr), e.context, backend)
			)

			utx, err := builder.NewBaseTx(
				[]*lux.TransferableOutput{luxOutput},
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Contains(utx.Outs, luxOutput)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func TestAddChainValidatorTx(t *testing.T) {
	chainValidator := &txs.ChainValidator{
		Validator: txs.Validator{
			NodeID: nodeID,
			End:    uint64(time.Now().Add(time.Hour).Unix()),
		},
		Chain: chainID,
	}

	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, chainOwners)
				builder = builder.New(set.Of(utxoAddr, chainAuthAddr), e.context, backend)
			)

			utx, err := builder.NewAddChainValidatorTx(
				chainValidator,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(*chainValidator, utx.ChainValidator)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func TestRemoveChainValidatorTx(t *testing.T) {
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, chainOwners)
				builder = builder.New(set.Of(utxoAddr, chainAuthAddr), e.context, backend)
			)

			utx, err := builder.NewRemoveChainValidatorTx(
				nodeID,
				chainID,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(nodeID, utx.NodeID)
			require.Equal(chainID, utx.Chain)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func TestCreateChainTx(t *testing.T) {
	var (
		genesisBytes = []byte{'a', 'b', 'c'}
		vmID         = ids.GenerateTestID()
		fxIDs        = []ids.ID{ids.GenerateTestID()}
		chainName    = "dummyChain"
	)

	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, chainOwners)
				builder = builder.New(set.Of(utxoAddr, chainAuthAddr), e.context, backend)
			)

			utx, err := builder.NewCreateChainTx(
				chainID,
				genesisBytes,
				vmID,
				fxIDs,
				chainName,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(chainID, utx.ChainID)
			require.Equal(genesisBytes, utx.GenesisData)
			require.Equal(vmID, utx.VMID)
			require.ElementsMatch(fxIDs, utx.FxIDs)
			require.Equal(chainName, utx.BlockchainName)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func TestTransferChainOwnershipTx(t *testing.T) {
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, chainOwners)
				builder = builder.New(set.Of(utxoAddr, chainAuthAddr), e.context, backend)
			)

			utx, err := builder.NewTransferChainOwnershipTx(
				chainID,
				chainOwner,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(chainID, utx.Chain)
			require.Equal(chainOwner, utx.Owner)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func TestImportTx(t *testing.T) {
	var (
		sourceChainID = ids.GenerateTestID()
		importedUTXOs = utxos[4:] // Use the last UTXO (9 Lux) which is sufficient for fees
	)

	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
					sourceChainID:             importedUTXOs,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr), e.context, backend)
			)

			utx, err := builder.NewImportTx(
				sourceChainID,
				importOwner,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(sourceChainID, utx.SourceChain)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			require.Empty(utx.Ins)                              // The imported input should be sufficient for fees
			require.Len(utx.ImportedInputs, len(importedUTXOs)) // All utxos should be imported
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				utx.ImportedInputs,
				nil,
				nil,
			)
		})
	}
}

func TestExportTx(t *testing.T) {
	exportedOutputs := []*lux.TransferableOutput{luxOutput}

	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr), e.context, backend)
			)

			utx, err := builder.NewExportTx(
				chainID,
				exportedOutputs,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(chainID, utx.DestinationChain)
			require.ElementsMatch(exportedOutputs, utx.ExportedOutputs)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				utx.ExportedOutputs,
				nil,
			)
		})
	}
}

func TestAddPermissionlessValidatorTx(t *testing.T) {
	var utxosOffset uint64 = 2025
	makeUTXO := func(amount uint64) *lux.UTXO {
		utxosOffset++
		return &lux.UTXO{
			UTXOID: lux.UTXOID{
				TxID:        ids.Empty.Prefix(utxosOffset),
				OutputIndex: uint32(utxosOffset),
			},
			Asset: lux.Asset{ID: utxoAssetID},
			Out: &secp256k1fx.TransferOutput{
				Amt:          amount,
				OutputOwners: utxoOwner,
			},
		}
	}

	var (
		utxos = []*lux.UTXO{
			makeUTXO(1 * constants.NanoLux), // small UTXO
			makeUTXO(9 * constants.Lux),     // large UTXO
		}

		validationRewardsOwner        = rewardsOwner
		delegationRewardsOwner        = rewardsOwner
		delegationShares       uint32 = reward.PercentDenominator
	)

	sk, err := localsigner.New()
	require.NoError(t, err)

	pop, err := signer.NewProofOfPossession(sk)
	require.NoError(t, err)

	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr, rewardAddr), e.context, backend)
			)

			utx, err := builder.NewAddPermissionlessValidatorTx(
				primaryNetworkPermissionlessStaker,
				pop,
				utxoAssetID,
				validationRewardsOwner,
				delegationRewardsOwner,
				delegationShares,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(primaryNetworkPermissionlessStaker.Validator, utx.Validator)
			require.Equal(primaryNetworkPermissionlessStaker.Chain, utx.Chain)
			require.Equal(pop, utx.Signer)
			// Outputs should be merged if possible. For example, if there are two
			// unlocked inputs consumed for staking, this should only produce one staked
			// output.
			require.Len(utx.StakeOuts, 1)
			// check stake amount
			require.Equal(
				map[ids.ID]uint64{
					utxoAssetID: primaryNetworkPermissionlessStaker.Wght,
				},
				addOutputAmounts(utx.StakeOuts),
			)
			require.Equal(validationRewardsOwner, utx.ValidatorRewardsOwner)
			require.Equal(delegationRewardsOwner, utx.DelegatorRewardsOwner)
			require.Equal(delegationShares, utx.DelegationShares)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				utx.StakeOuts,
				nil,
			)
		})
	}
}

func TestAddPermissionlessDelegatorTx(t *testing.T) {
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr, rewardAddr), e.context, backend)
			)

			utx, err := builder.NewAddPermissionlessDelegatorTx(
				primaryNetworkPermissionlessStaker,
				utxoAssetID,
				rewardsOwner,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(primaryNetworkPermissionlessStaker.Validator, utx.Validator)
			require.Equal(primaryNetworkPermissionlessStaker.Chain, utx.Chain)
			// check stake amount
			require.Equal(
				map[ids.ID]uint64{
					utxoAssetID: primaryNetworkPermissionlessStaker.Wght,
				},
				addOutputAmounts(utx.StakeOuts),
			)
			require.Equal(rewardsOwner, utx.DelegationRewardsOwner)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				utx.StakeOuts,
				nil,
			)
		})
	}
}

func TestConvertNetworkToL1Tx(t *testing.T) {
	sk0, err := localsigner.New()
	require.NoError(t, err)
	pop0, err := signer.NewProofOfPossession(sk0)
	require.NoError(t, err)
	sk1, err := localsigner.New()
	require.NoError(t, err)
	pop1, err := signer.NewProofOfPossession(sk1)
	require.NoError(t, err)

	var (
		chainID    = ids.GenerateTestID()
		address    = utils.RandomBytes(32)
		validators = []*txs.ConvertNetworkToL1Validator{
			{
				NodeID:  utils.RandomBytes(ids.NodeIDLen),
				Weight:  rand.Uint64(), //#nosec G404
				Balance: constants.Lux,
				Signer:  *pop0,
				RemainingBalanceOwner: message.PChainOwner{
					Threshold: 1,
					Addresses: []ids.ShortID{
						ids.GenerateTestShortID(),
					},
				},
				DeactivationOwner: message.PChainOwner{
					Threshold: 1,
					Addresses: []ids.ShortID{
						ids.GenerateTestShortID(),
					},
				},
			},
			{
				NodeID:                utils.RandomBytes(ids.NodeIDLen),
				Weight:                rand.Uint64(), //#nosec G404
				Balance:               2 * constants.Lux,
				Signer:                *pop1,
				RemainingBalanceOwner: message.PChainOwner{},
				DeactivationOwner:     message.PChainOwner{},
			},
		}
	)
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				localChainOwners = map[ids.ID]fx.Owner{
					chainID: chainOwner,
				}
				backend = wallet.NewBackend(chainUTXOs, localChainOwners)
				builder = builder.New(set.Of(utxoAddr, chainAuthAddr), e.context, backend)
			)

			utx, err := builder.NewConvertNetworkToL1Tx(
				chainID,
				chainID,
				address,
				validators,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(chainID, utx.Chain)
			require.Equal(chainID, utx.ManagerChainID)
			require.Equal(types.JSONByteSlice(address), utx.Address)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			require.True(utils.IsSortedAndUnique(utx.Validators))
			require.Equal(validators, utx.Validators)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				map[ids.ID]uint64{
					e.context.UTXOAssetID: 3 * constants.Lux, // Balance of the validators
				},
			)
		})
	}
}

func TestRegisterL1ValidatorTx(t *testing.T) {
	const (
		expiry = 1731005097
		weight = 7905001371

		balance = constants.Lux
	)

	sk, err := localsigner.New()
	require.NoError(t, err)
	pop, err := signer.NewProofOfPossession(sk)
	require.NoError(t, err)

	addressedCallPayload, err := message.NewRegisterL1Validator(
		chainID,
		nodeID,
		pop.PublicKey,
		expiry,
		message.PChainOwner{
			Threshold: 1,
			Addresses: []ids.ShortID{
				ids.GenerateTestShortID(),
			},
		},
		message.PChainOwner{
			Threshold: 1,
			Addresses: []ids.ShortID{
				ids.GenerateTestShortID(),
			},
		},
		weight,
	)
	require.NoError(t, err)

	addressedCall, err := payload.NewAddressedCall(
		utils.RandomBytes(20),
		addressedCallPayload.Bytes(),
	)
	require.NoError(t, err)

	unsignedWarp, err := warp.NewUnsignedMessage(
		constants.UnitTestID,
		ids.GenerateTestID(),
		addressedCall.Bytes(),
	)
	require.NoError(t, err)

	signers := set.NewBits(0)

	unsignedBytes := unsignedWarp.Bytes()
	sig, err := sk.Sign(unsignedBytes)
	require.NoError(t, err)

	sigBytes := [bls.SignatureLen]byte{}
	copy(sigBytes[:], bls.SignatureToBytes(sig))

	warp, err := warp.NewMessage(
		unsignedWarp,
		&warp.BitSetSignature{
			Signers:   signers.Bytes(),
			Signature: sigBytes,
		},
	)
	require.NoError(t, err)
	warpMessageBytes := warp.Bytes()

	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr), e.context, backend)
			)

			utx, err := builder.NewRegisterL1ValidatorTx(
				balance,
				pop.ProofOfPossession,
				warpMessageBytes,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(balance, utx.Balance)
			require.Equal(pop.ProofOfPossession, utx.ProofOfPossession)
			require.Equal(types.JSONByteSlice(warpMessageBytes), utx.Message)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				map[ids.ID]uint64{
					e.context.UTXOAssetID: balance, // Balance of the validator
				},
			)
		})
	}
}

func TestSetL1ValidatorWeightTx(t *testing.T) {
	const (
		nonce  = 1
		weight = 7905001371
	)
	var (
		validationID = ids.GenerateTestID()
		chainID      = ids.GenerateTestID()
		address      = utils.RandomBytes(20)
	)

	addressedCallPayload, err := message.NewL1ValidatorWeight(
		validationID,
		nonce,
		weight,
	)
	require.NoError(t, err)

	addressedCall, err := payload.NewAddressedCall(
		address,
		addressedCallPayload.Bytes(),
	)
	require.NoError(t, err)

	unsignedWarp, err := warp.NewUnsignedMessage(
		constants.UnitTestID,
		chainID,
		addressedCall.Bytes(),
	)
	require.NoError(t, err)

	sk, err := localsigner.New()
	require.NoError(t, err)
	sig, err := sk.Sign(unsignedWarp.Bytes())
	require.NoError(t, err)

	warp, err := warp.NewMessage(
		unsignedWarp,
		&warp.BitSetSignature{
			Signers: set.NewBits(0).Bytes(),
			Signature: ([bls.SignatureLen]byte)(
				bls.SignatureToBytes(sig),
			),
		},
	)
	require.NoError(t, err)

	warpMessageBytes := warp.Bytes()
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr), e.context, backend)
			)

			utx, err := builder.NewSetL1ValidatorWeightTx(
				warpMessageBytes,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(types.JSONByteSlice(warpMessageBytes), utx.Message)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func TestIncreaseL1ValidatorBalanceTx(t *testing.T) {
	const balance = constants.Lux
	validationID := ids.GenerateTestID()
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, nil)
				builder = builder.New(set.Of(utxoAddr), e.context, backend)
			)

			utx, err := builder.NewIncreaseL1ValidatorBalanceTx(
				validationID,
				balance,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(validationID, utx.ValidationID)
			require.Equal(balance, utx.Balance)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				map[ids.ID]uint64{
					e.context.UTXOAssetID: balance, // Balance increase
				},
			)
		})
	}
}

func TestDisableL1ValidatorTx(t *testing.T) {
	for _, e := range testEnvironment {
		t.Run(e.name, func(t *testing.T) {
			var (
				require    = require.New(t)
				chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*lux.UTXO{
					constants.PlatformChainID: utxos,
				})
				backend = wallet.NewBackend(chainUTXOs, validationOwners)
				builder = builder.New(set.Of(utxoAddr, validationAuthAddr), e.context, backend)
			)

			utx, err := builder.NewDisableL1ValidatorTx(
				validationID,
				common.WithMemo(e.memo),
			)
			require.NoError(err)
			require.Equal(validationID, utx.ValidationID)
			require.Equal(types.JSONByteSlice(e.memo), utx.Memo)
			requireFeeIsCorrect(
				require,
				e.feeCalculator,
				utx,
				&utx.BaseTx.BaseTx,
				nil,
				nil,
				nil,
			)
		})
	}
}

func makeTestUTXOs(utxosKey *secp256k1.PrivateKey) []*lux.UTXO {
	// Note: we avoid ids.GenerateTestNodeID here to make sure that UTXO IDs
	// won't change run by run. This simplifies checking what utxos are included
	// in the built txs.
	const utxosOffset uint64 = 2025

	utxosAddr := utxosKey.Address()
	return []*lux.UTXO{
		{ // a small UTXO first, which should not be enough to pay fees
			UTXOID: lux.UTXOID{
				TxID:        ids.Empty.Prefix(utxosOffset),
				OutputIndex: uint32(utxosOffset),
			},
			Asset: lux.Asset{ID: utxoAssetID},
			Out: &secp256k1fx.TransferOutput{
				Amt: 2 * constants.MilliLux,
				OutputOwners: secp256k1fx.OutputOwners{
					Locktime:  0,
					Addrs:     []ids.ShortID{utxosAddr},
					Threshold: 1,
				},
			},
		},
		{ // a locked, small UTXO
			UTXOID: lux.UTXOID{
				TxID:        ids.Empty.Prefix(utxosOffset + 1),
				OutputIndex: uint32(utxosOffset + 1),
			},
			Asset: lux.Asset{ID: utxoAssetID},
			Out: &stakeable.LockOut{
				Locktime: uint64(time.Now().Add(time.Hour).Unix()),
				TransferableOut: &secp256k1fx.TransferOutput{
					Amt: 3 * constants.MilliLux,
					OutputOwners: secp256k1fx.OutputOwners{
						Threshold: 1,
						Addrs:     []ids.ShortID{utxosAddr},
					},
				},
			},
		},
		{ // a chainAssetID denominated UTXO
			UTXOID: lux.UTXOID{
				TxID:        ids.Empty.Prefix(utxosOffset + 2),
				OutputIndex: uint32(utxosOffset + 2),
			},
			Asset: lux.Asset{ID: chainAssetID},
			Out: &secp256k1fx.TransferOutput{
				Amt: 99 * constants.MegaLux,
				OutputOwners: secp256k1fx.OutputOwners{
					Locktime:  0,
					Addrs:     []ids.ShortID{utxosAddr},
					Threshold: 1,
				},
			},
		},
		{ // a locked, large UTXO
			UTXOID: lux.UTXOID{
				TxID:        ids.Empty.Prefix(utxosOffset + 3),
				OutputIndex: uint32(utxosOffset + 3),
			},
			Asset: lux.Asset{ID: utxoAssetID},
			Out: &stakeable.LockOut{
				Locktime: uint64(time.Now().Add(time.Hour).Unix()),
				TransferableOut: &secp256k1fx.TransferOutput{
					Amt: 88 * constants.Lux,
					OutputOwners: secp256k1fx.OutputOwners{
						Threshold: 1,
						Addrs:     []ids.ShortID{utxosAddr},
					},
				},
			},
		},
		{ // a large UTXO last, which should be enough to pay any fee by itself
			UTXOID: lux.UTXOID{
				TxID:        ids.Empty.Prefix(utxosOffset + 4),
				OutputIndex: uint32(utxosOffset + 4),
			},
			Asset: lux.Asset{ID: utxoAssetID},
			Out: &secp256k1fx.TransferOutput{
				Amt: 9 * constants.Lux,
				OutputOwners: secp256k1fx.OutputOwners{
					Locktime:  0,
					Addrs:     []ids.ShortID{utxosAddr},
					Threshold: 1,
				},
			},
		},
	}
}

// requireFeeIsCorrect calculates the required fee for the unsigned transaction
// and verifies that the burned amount is exactly the required fee.
func requireFeeIsCorrect(
	require *require.Assertions,
	feeCalculator fee.Calculator,
	utx txs.UnsignedTx,
	baseTx *lux.BaseTx,
	additionalIns []*lux.TransferableInput,
	additionalOuts []*lux.TransferableOutput,
	additionalFee map[ids.ID]uint64,
) {
	amountConsumed := addInputAmounts(baseTx.Ins, additionalIns)
	amountProduced := addOutputAmounts(baseTx.Outs, additionalOuts)

	expectedFee, err := feeCalculator.CalculateFee(utx)
	require.NoError(err)
	expectedAmountBurned := addAmounts(
		map[ids.ID]uint64{
			utxoAssetID: expectedFee,
		},
		additionalFee,
	)
	expectedAmountConsumed := addAmounts(amountProduced, expectedAmountBurned)
	require.Equal(expectedAmountConsumed, amountConsumed)
}

func addAmounts(allAmounts ...map[ids.ID]uint64) map[ids.ID]uint64 {
	amounts := make(map[ids.ID]uint64)
	for _, amountsToAdd := range allAmounts {
		for assetID, amount := range amountsToAdd {
			amounts[assetID] += amount
		}
	}
	return amounts
}

func addInputAmounts(inputSlices ...[]*lux.TransferableInput) map[ids.ID]uint64 {
	consumed := make(map[ids.ID]uint64)
	for _, inputs := range inputSlices {
		for _, in := range inputs {
			consumed[in.AssetID()] += in.In.Amount()
		}
	}
	return consumed
}

func addOutputAmounts(outputSlices ...[]*lux.TransferableOutput) map[ids.ID]uint64 {
	produced := make(map[ids.ID]uint64)
	for _, outputs := range outputSlices {
		for _, out := range outputs {
			produced[out.AssetID()] += out.Out.Amount()
		}
	}
	return produced
}
