// Copyright (c) 2017-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package blockchain

import (
	"fmt"
	"testing"
	"time"

	"github.com/decred/dcrd/chaincfg/v3"
	"github.com/decred/dcrd/dcrutil/v4"
	"github.com/decred/dcrd/wire"
)

// mustLockTimeToSeq converts the passed relative lock time to a sequence number
// by using LockTimeToSequence.  It only differs in that it will panic if there
// is an error so errors in the source code can be detected.  It will only (and
// must only)  be called with hard-coded, and therefore known good, values.
func mustLockTimeToSeq(isSeconds bool, lockTime uint32) uint32 {
	sequence, err := LockTimeToSequence(isSeconds, lockTime)
	if err != nil {
		panic(fmt.Sprintf("invalid lock time in source file: "+
			"isSeconds: %v, lockTime: %d", isSeconds, lockTime))
	}
	return sequence
}

// TestCalcSequenceLock exercises several combinations of inputs to the
// CalcSequenceLock function in order to ensure the returned sequence locks are
// as expected.
func TestCalcSequenceLock(t *testing.T) {
	// Generate a synthetic simnet chain with enough nodes to properly test
	// the sequence lock functionality.
	numBlocks := uint32(20)
	params := chaincfg.RegNetParams()
	bc := newFakeChain(params)
	node := bc.bestChain.Tip()
	blockTime := time.Unix(node.timestamp, 0)
	for i := uint32(0); i < numBlocks; i++ {
		blockTime = blockTime.Add(time.Second)
		node = newFakeNode(node, 1, 1, 0, blockTime)
		bc.index.AddNode(node)
		bc.bestChain.SetTip(node)
	}

	// Create a utxo view with a fake utxo for the inputs used in the
	// transactions created below.  This utxo is added such that it has an
	// age of 4 blocks.
	targetTx := dcrutil.NewTx(&wire.MsgTx{
		TxOut: []*wire.TxOut{{
			Value:    10,
			Version:  0,
			PkScript: nil,
		}},
	})
	view := NewUtxoViewpoint(nil)
	view.AddTxOuts(targetTx, int64(numBlocks)-4, 0, noTreasury)
	view.SetBestHash(&node.hash)

	// Create a utxo that spends the fake utxo created above for use in the
	// transactions created in the tests.  It has an age of 4 blocks.  Note
	// that the sequence lock heights are always calculated from the same
	// point of view that they were originally calculated from for a given
	// utxo.  That is to say, the height prior to it.
	utxo := wire.OutPoint{
		Hash:  *targetTx.Hash(),
		Index: 0,
		Tree:  wire.TxTreeRegular,
	}
	prevUtxoHeight := int64(numBlocks) - 4

	// Obtain the median time past from the PoV of the input created above.
	// The median time for the input is the median time from the PoV of the
	// block *prior* to the one that included it.
	medianTime := node.RelativeAncestor(5).CalcPastMedianTime().Unix()

	// The median time calculated from the PoV of the best block in the
	// test chain.  For unconfirmed inputs, this value will be used since
	// the median time will be calculated from the PoV of the
	// yet-to-be-mined block.
	nextMedianTime := node.CalcPastMedianTime().Unix()
	nextBlockHeight := int64(numBlocks) + 1

	// Add an additional transaction which will serve as our unconfirmed
	// output.
	unConfTx := &wire.MsgTx{
		TxOut: []*wire.TxOut{{
			Value:    5,
			Version:  0,
			PkScript: nil,
		}},
	}
	unConfUtxo := wire.OutPoint{
		Hash:  unConfTx.TxHash(),
		Index: 0,
		Tree:  wire.TxTreeRegular,
	}

	// Adding a utxo with a height of 0x7fffffff indicates that the output
	// is currently unmined.
	view.AddTxOuts(dcrutil.NewTx(unConfTx), 0x7fffffff, wire.NullBlockIndex,
		noTreasury)

	tests := []struct {
		name      string
		txVersion uint16
		inputs    []*wire.TxIn
		isActive  bool
		want      SequenceLock
	}{
		{
			// A transaction of version one should disable sequence
			// locks as the new sequence number semantics only apply
			// to transactions version 2 or higher.
			name:      "v1 transaction",
			txVersion: 1,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 3),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   -1,
			},
		},
		{
			// A transaction with a single input with max sequence
			// number.  This sequence number has the high bit set,
			// so sequence locks should be disabled.
			name:      "max sequence number",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         wire.MaxTxInSequenceNum,
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   -1,
			},
		},
		{
			// A transaction that would result in a specific
			// sequence lock except set the agenda is not being
			// active yet, so sequence locks should be disabled.
			name:      "agenda not yet active",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 2),
			}},
			isActive: false,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   -1,
			},
		},
		{
			// A transaction with a single input whose locktime is
			// expressed in seconds.  However, the specified lock
			// time is below the required floor for time based lock
			// times since they have time granularity of 512
			// seconds.  As a result, the seconds locktime should be
			// just before the median time of the targeted block.
			name:      "seconds below granularity",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 2),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   medianTime - 1,
			},
		},
		{
			// A transaction with a single input whose locktime is
			// expressed in seconds.  The number of seconds should
			// be 1023 seconds after the median past time of the
			// input.
			name:      "1024 seconds",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 1024),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   medianTime + 1023,
			},
		},
		{
			// A transaction with multiple inputs.  The first input
			// has a locktime expressed in seconds.  The second
			// input has a sequence lock in blocks with a value of
			// 4.  The last input has a sequence number with a value
			// of 5, but has the disable bit set.  So the first lock
			// should be selected as it's the latest lock that isn't
			// disabled.
			name:      "multiple inputs, 1 disabled",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 2560),
			}, {
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 4),
			}, {
				PreviousOutPoint: utxo,
				Sequence: mustLockTimeToSeq(false, 5) |
					wire.SequenceLockTimeDisabled,
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: prevUtxoHeight + 3,
				MinTime:   medianTime + (5 << wire.SequenceLockTimeGranularity) - 1,
			},
		},
		{
			// A transaction with a single input.  The input's
			// sequence number encodes a relative locktime in blocks
			// (3 blocks).  The sequence lock should have a value
			// of -1 for seconds, but a height of 2 meaning it can
			// be included at height 3.
			name:      "3 blocks",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 3),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: prevUtxoHeight + 2,
				MinTime:   -1,
			},
		},
		{
			// A transaction with two inputs with locktimes
			// expressed in seconds.  The selected sequence lock
			// value for seconds should be the time further in the
			// future.
			name:      "2 inputs both in seconds",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 5120),
			}, {
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 2560),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   medianTime + (10 << wire.SequenceLockTimeGranularity) - 1,
			},
		},
		{
			// A transaction with two inputs with locktimes
			// expressed in blocks.  The selected sequence lock
			// value for blocks should be the height further in the
			// future, so a height of 10 indicating it can be
			// included at height 11.
			name:      "2 inputs both in blocks",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 1),
			}, {
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 11),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: prevUtxoHeight + 10,
				MinTime:   -1,
			},
		},
		{
			// A transaction with multiple inputs.  Two inputs are
			// seconds and the other two are blocks. The lock
			// further into the future for both inputs should be
			// chosen.
			name:      "4 inputs, 2 in seconds, 2 in blocks",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 2560),
			}, {
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(true, 6656),
			}, {
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 3),
			}, {
				PreviousOutPoint: utxo,
				Sequence:         mustLockTimeToSeq(false, 9),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: prevUtxoHeight + 8,
				MinTime:   medianTime + (13 << wire.SequenceLockTimeGranularity) - 1,
			},
		},
		{
			// A transaction with a single unconfirmed input.  Since
			// the input is unconfirmed, the height of the input
			// should be interpreted as the height of the *next*
			// block.  So, a 2 block relative lock means the
			// sequence lock should be for 1 block after the *next*
			// block height, indicating it can be included 2 blocks
			// after that.
			name:      "unconfirmed input in blocks",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: unConfUtxo,
				Sequence:         mustLockTimeToSeq(false, 2),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: nextBlockHeight + 1,
				MinTime:   -1,
			},
		},
		{
			// A transaction with a single unconfirmed input.  The
			// input has locktime in seconds, so the locktime should
			// be based off the median time of the *next* block.
			name:      "unconfirmed input in seconds",
			txVersion: 2,
			inputs: []*wire.TxIn{{
				PreviousOutPoint: unConfUtxo,
				Sequence:         mustLockTimeToSeq(true, 1024),
			}},
			isActive: true,
			want: SequenceLock{
				MinHeight: -1,
				MinTime:   nextMedianTime + 1023,
			},
		},
	}

	for i, test := range tests {
		// Create fake spending transaction per the test input data.
		tx := wire.MsgTx{
			SerType:  wire.TxSerializeFull,
			Version:  test.txVersion,
			LockTime: 0,
			Expiry:   0,
			TxOut:    nil,
		}
		for _, txIn := range test.inputs {
			tx.AddTxIn(txIn)
		}
		utilTx := dcrutil.NewTx(&tx)

		// Calculate the sequence lock for the test input data.  Since
		// the exported function always has the agenda active, use the
		// unexported function when simulating the agenda not being
		// active, and alternate between them to ensure both are
		// exercised.
		var seqLock *SequenceLock
		var err error
		if test.isActive && i%2 == 0 {
			seqLock, err = bc.CalcSequenceLock(utilTx, view)
		} else {
			bc.chainLock.Lock()
			seqLock, err = bc.calcSequenceLock(node, utilTx, view,
				test.isActive)
			bc.chainLock.Unlock()
		}
		if err != nil {
			t.Errorf("%s: unable to calc sequence lock: %v",
				test.name, err)
			continue
		}

		// Ensure both the returned sequence lock seconds and block
		// height match the expected values.
		if seqLock.MinTime != test.want.MinTime {
			t.Errorf("%s: mismatched seconds - got %v, want %v",
				test.name, seqLock.MinTime, test.want.MinTime)
			continue
		}
		if seqLock.MinHeight != test.want.MinHeight {
			t.Errorf("%s: mismatched height - got %v, want %v",
				test.name, seqLock.MinHeight,
				test.want.MinHeight)
		}
	}
}

// TestLockTimeToSequence ensure the convenience function to convert relative
// lock times to a sequence number works as expected.
func TestLockTimeToSequence(t *testing.T) {
	const (
		// The following constants are used over the package-level
		// definitions to ensure tests correctly detect any changes to
		// them.
		secondsGranularityBits = 9
		secondsBit             = 1 << 22
		maxValue               = 1<<16 - 1
		maxBlockHeight         = maxValue
		maxSeconds             = maxValue << secondsGranularityBits
	)

	tests := []struct {
		name      string
		locktime  uint32
		isSeconds bool
		expected  uint32
		invalid   bool
	}{
		{
			name:      "relative block height 0",
			locktime:  0,
			isSeconds: false,
			expected:  0,
		},
		{
			name:      "max relative block height",
			locktime:  maxBlockHeight,
			isSeconds: false,
			expected:  maxBlockHeight,
		},
		{
			name:      "max relative block height +1",
			locktime:  maxBlockHeight + 1,
			isSeconds: false,
			expected:  0,
			invalid:   true,
		},
		{
			name:      "relative seconds 0",
			locktime:  0,
			isSeconds: true,
			expected:  secondsBit,
		},
		{
			name:      "relative seconds granularity - 1",
			locktime:  (1 << secondsGranularityBits) - 1,
			isSeconds: true,
			expected:  secondsBit,
		},
		{
			name:      "relative seconds exact granularity",
			locktime:  1 << secondsGranularityBits,
			isSeconds: true,
			expected:  secondsBit + 1,
		},
		{
			name:      "relative seconds granularity + 1",
			locktime:  (1 << secondsGranularityBits) + 1,
			isSeconds: true,
			expected:  secondsBit + 1,
		},
		{
			name:      "relative seconds max - 1",
			locktime:  maxSeconds - 1,
			isSeconds: true,
			expected:  secondsBit + maxValue - 1,
		},
		{
			name:      "relative seconds max",
			locktime:  maxSeconds,
			isSeconds: true,
			expected:  secondsBit + maxValue,
		},
		{
			name:      "relative seconds max +1",
			locktime:  maxSeconds + 1,
			isSeconds: true,
			expected:  0,
			invalid:   true,
		},
	}

	for _, test := range tests {
		gotSequence, err := LockTimeToSequence(test.isSeconds,
			test.locktime)
		if err != nil && !test.invalid {
			t.Errorf("%s: unexpected error: %v", test.name, err)
			continue
		}
		if err == nil && test.invalid {
			t.Errorf("%s: did not receive expected error", test.name)
			continue
		}

		if gotSequence != test.expected {
			t.Errorf("%s: mismatched sequence - got %d, want %d",
				test.name, gotSequence, test.expected)
			continue
		}
	}
}
