package sweep

import (
	"testing"

	"github.com/lightningnetwork/lnd/fn"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/stretchr/testify/require"
)

// TestLinearFeeFunctionNewMaxFeeRateUsed tests when the conf target is <= 1,
// the max fee rate is used.
func TestLinearFeeFunctionNewMaxFeeRateUsed(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a mock fee estimator.
	estimator := &chainfee.MockEstimator{}
	defer estimator.AssertExpectations(t)

	// Create testing params.
	maxFeeRate := chainfee.SatPerKWeight(10000)
	noStartFeeRate := fn.None[chainfee.SatPerKWeight]()

	// Assert init fee function with zero conf value will end up using the
	// max fee rate.
	f, err := NewLinearFeeFunction(maxFeeRate, 0, estimator, noStartFeeRate)
	rt.NoError(err)
	rt.NotNil(f)

	// Assert the internal state.
	rt.Equal(maxFeeRate, f.startingFeeRate)
	rt.Equal(maxFeeRate, f.endingFeeRate)
	rt.Equal(maxFeeRate, f.currentFeeRate)

	// Assert init fee function with conf of one will end up using the max
	// fee rate.
	f, err = NewLinearFeeFunction(maxFeeRate, 1, estimator, noStartFeeRate)
	rt.NoError(err)
	rt.NotNil(f)

	// Assert the internal state.
	rt.Equal(maxFeeRate, f.startingFeeRate)
	rt.Equal(maxFeeRate, f.endingFeeRate)
	rt.Equal(maxFeeRate, f.currentFeeRate)
}

// TestLinearFeeFunctionNewZeroFeeRateDelta tests when the fee rate delta is
// zero, it will return an error except when the width is one.
func TestLinearFeeFunctionNewZeroFeeRateDelta(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a mock fee estimator.
	estimator := &chainfee.MockEstimator{}
	defer estimator.AssertExpectations(t)

	// Create testing params.
	maxFeeRate := chainfee.SatPerKWeight(10000)
	estimatedFeeRate := chainfee.SatPerKWeight(500)
	confTarget := uint32(6)
	noStartFeeRate := fn.None[chainfee.SatPerKWeight]()

	// When the calculated fee rate delta is 0, an error should be returned
	// when the width is not one.
	//
	// Mock the fee estimator to return the fee rate.
	estimator.On("EstimateFeePerKW", confTarget).Return(
		// The starting fee rate is the max fee rate.
		maxFeeRate, nil).Once()
	estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()

	f, err := NewLinearFeeFunction(
		maxFeeRate, confTarget, estimator, noStartFeeRate,
	)
	rt.ErrorContains(err, "fee rate delta is zero")
	rt.Nil(f)

	// When the calculated fee rate delta is 0, an error should NOT be
	// returned when the width is one, and the starting feerate is capped
	// at the max fee rate.
	//
	// Mock the fee estimator to return the fee rate.
	smallConf := uint32(2)
	estimator.On("EstimateFeePerKW", smallConf).Return(
		// The fee rate is greater than the max fee rate.
		maxFeeRate+1, nil).Once()
	estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()

	f, err = NewLinearFeeFunction(
		maxFeeRate, smallConf, estimator, noStartFeeRate,
	)
	rt.NoError(err)
	rt.NotNil(f)

	// Assert the internal state.
	rt.Equal(maxFeeRate, f.startingFeeRate)
	rt.Equal(maxFeeRate, f.endingFeeRate)
	rt.Equal(maxFeeRate, f.currentFeeRate)
	rt.Zero(f.deltaFeeRate)
	rt.Equal(smallConf-1, f.width)
}

// TestLinearFeeFunctionNewEsimator tests the NewLinearFeeFunction function
// properly reacts to the response or error returned from the fee estimator.
func TestLinearFeeFunctionNewEsimator(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a mock fee estimator.
	estimator := &chainfee.MockEstimator{}
	defer estimator.AssertExpectations(t)

	// Create testing params.
	maxFeeRate := chainfee.SatPerKWeight(10000)
	minRelayFeeRate := chainfee.SatPerKWeight(100)
	confTarget := uint32(6)
	noStartFeeRate := fn.None[chainfee.SatPerKWeight]()

	// When the fee estimator returns an error, it's returned.
	//
	// Mock the fee estimator to return an error.
	estimator.On("EstimateFeePerKW", confTarget).Return(
		chainfee.SatPerKWeight(0), errDummy).Once()

	f, err := NewLinearFeeFunction(
		maxFeeRate, confTarget, estimator, noStartFeeRate,
	)
	rt.ErrorIs(err, errDummy)
	rt.Nil(f)

	// When the conf target is >= 1008, the min relay fee should be used.
	//
	// Mock the fee estimator to reutrn the fee rate.
	estimator.On("RelayFeePerKW").Return(minRelayFeeRate).Once()

	largeConf := uint32(1008)
	f, err = NewLinearFeeFunction(
		maxFeeRate, largeConf, estimator, noStartFeeRate,
	)
	rt.NoError(err)
	rt.NotNil(f)

	// Assert the internal state.
	rt.Equal(minRelayFeeRate, f.startingFeeRate)
	rt.Equal(maxFeeRate, f.endingFeeRate)
	rt.Equal(minRelayFeeRate, f.currentFeeRate)
	rt.NotZero(f.deltaFeeRate)
	rt.Equal(largeConf-1, f.width)
}

// TestLinearFeeFunctionNewSuccess tests we can create the fee function
// successfully.
func TestLinearFeeFunctionNewSuccess(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a mock fee estimator.
	estimator := &chainfee.MockEstimator{}
	defer estimator.AssertExpectations(t)

	// Create testing params.
	maxFeeRate := chainfee.SatPerKWeight(10000)
	estimatedFeeRate := chainfee.SatPerKWeight(500)
	minRelayFeeRate := chainfee.SatPerKWeight(100)
	confTarget := uint32(6)
	noStartFeeRate := fn.None[chainfee.SatPerKWeight]()
	startFeeRate := chainfee.SatPerKWeight(1000)

	// Check a successfully created fee function.
	//
	// Mock the fee estimator to return the fee rate.
	estimator.On("EstimateFeePerKW", confTarget).Return(
		estimatedFeeRate, nil).Once()
	estimator.On("RelayFeePerKW").Return(minRelayFeeRate).Once()

	f, err := NewLinearFeeFunction(
		maxFeeRate, confTarget, estimator, noStartFeeRate,
	)
	rt.NoError(err)
	rt.NotNil(f)

	// Assert the internal state.
	rt.Equal(estimatedFeeRate, f.startingFeeRate)
	rt.Equal(maxFeeRate, f.endingFeeRate)
	rt.Equal(estimatedFeeRate, f.currentFeeRate)
	rt.NotZero(f.deltaFeeRate)
	rt.Equal(confTarget-1, f.width)

	// Check a successfully created fee function using the specified
	// starting fee rate.
	//
	// NOTE: by NOT mocking the fee estimator, we assert the
	// estimateFeeRate is NOT called.
	f, err = NewLinearFeeFunction(
		maxFeeRate, confTarget, estimator, fn.Some(startFeeRate),
	)

	rt.NoError(err)
	rt.NotNil(f)

	// Assert the customized starting fee rate is used.
	rt.Equal(startFeeRate, f.startingFeeRate)
	rt.Equal(maxFeeRate, f.endingFeeRate)
	rt.Equal(startFeeRate, f.currentFeeRate)
	rt.NotZero(f.deltaFeeRate)
	rt.Equal(confTarget-1, f.width)
}

// TestLinearFeeFunctionFeeRateAtPosition checks the expected feerate is
// calculated and returned.
func TestLinearFeeFunctionFeeRateAtPosition(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a fee func which has three positions:
	// - position 0: 1000
	// - position 1: 2000
	// - position 2: 3000
	f := &LinearFeeFunction{
		startingFeeRate: 1000,
		endingFeeRate:   3000,
		position:        0,
		deltaFeeRate:    1_000_000,
		width:           3,
	}

	testCases := []struct {
		name            string
		pos             uint32
		expectedFeerate chainfee.SatPerKWeight
	}{
		{
			name:            "position 0",
			pos:             0,
			expectedFeerate: 1000,
		},
		{
			name:            "position 1",
			pos:             1,
			expectedFeerate: 2000,
		},
		{
			name:            "position 2",
			pos:             2,
			expectedFeerate: 3000,
		},
		{
			name:            "position 3",
			pos:             3,
			expectedFeerate: 3000,
		},
	}

	for _, tc := range testCases {
		tc := tc

		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			result := f.feeRateAtPosition(tc.pos)
			rt.Equal(tc.expectedFeerate, result)
		})
	}
}

// TestLinearFeeFunctionIncrement checks the internal state is updated
// correctly when the fee rate is incremented.
func TestLinearFeeFunctionIncrement(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a mock fee estimator.
	estimator := &chainfee.MockEstimator{}
	defer estimator.AssertExpectations(t)

	// Create testing params. These params are chosen so the delta value is
	// 100.
	maxFeeRate := chainfee.SatPerKWeight(900)
	estimatedFeeRate := chainfee.SatPerKWeight(100)
	confTarget := uint32(9) // This means the width is 8.

	// Mock the fee estimator to return the fee rate.
	estimator.On("EstimateFeePerKW", confTarget).Return(
		estimatedFeeRate, nil).Once()
	estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()

	f, err := NewLinearFeeFunction(
		maxFeeRate, confTarget, estimator,
		fn.None[chainfee.SatPerKWeight](),
	)
	rt.NoError(err)

	// We now increase the position from 1 to 8.
	for i := uint32(1); i <= confTarget-1; i++ {
		// Increase the fee rate.
		increased, err := f.Increment()
		rt.NoError(err)
		rt.True(increased)

		// Assert the internal state.
		rt.Equal(i, f.position)

		delta := chainfee.SatPerKWeight(i * 100)
		rt.Equal(estimatedFeeRate+delta, f.currentFeeRate)

		// Check public method returns the expected fee rate.
		rt.Equal(estimatedFeeRate+delta, f.FeeRate())
	}

	// Now the position is at 8th, increase it again should give us an
	// error.
	increased, err := f.Increment()
	rt.ErrorIs(err, ErrMaxPosition)
	rt.False(increased)
}

// TestLinearFeeFunctionIncreaseFeeRate checks the internal state is updated
// correctly when the fee rate is increased using conf targets.
func TestLinearFeeFunctionIncreaseFeeRate(t *testing.T) {
	t.Parallel()

	rt := require.New(t)

	// Create a mock fee estimator.
	estimator := &chainfee.MockEstimator{}
	defer estimator.AssertExpectations(t)

	// Create testing params. These params are chosen so the delta value is
	// 100.
	maxFeeRate := chainfee.SatPerKWeight(900)
	estimatedFeeRate := chainfee.SatPerKWeight(100)
	confTarget := uint32(9) // This means the width is 8.

	// Mock the fee estimator to return the fee rate.
	estimator.On("EstimateFeePerKW", confTarget).Return(
		estimatedFeeRate, nil).Once()
	estimator.On("RelayFeePerKW").Return(estimatedFeeRate).Once()

	f, err := NewLinearFeeFunction(
		maxFeeRate, confTarget, estimator,
		fn.None[chainfee.SatPerKWeight](),
	)
	rt.NoError(err)

	// If we are increasing the fee rate using the initial conf target, we
	// should get a nil error and false.
	increased, err := f.IncreaseFeeRate(confTarget)
	rt.NoError(err)
	rt.False(increased)

	// Test that we are allowed to use a larger conf target.
	increased, err = f.IncreaseFeeRate(confTarget + 1)
	rt.NoError(err)
	rt.False(increased)

	// We now increase the fee rate from conf target 8 to 2 and assert we
	// get no error and true.
	for i := uint32(1); i < confTarget-1; i++ {
		// Increase the fee rate.
		increased, err := f.IncreaseFeeRate(confTarget - i)
		rt.NoError(err)
		rt.True(increased)

		// Assert the internal state.
		rt.Equal(i, f.position)

		delta := chainfee.SatPerKWeight(i * 100)
		rt.Equal(estimatedFeeRate+delta, f.currentFeeRate)

		// Check public method returns the expected fee rate.
		rt.Equal(estimatedFeeRate+delta, f.FeeRate())
	}

	// Test that when we use a conf target of 1, we get the ending fee
	// rate.
	increased, err = f.IncreaseFeeRate(1)
	rt.NoError(err)
	rt.True(increased)
	rt.Equal(confTarget-1, f.position)
	rt.Equal(maxFeeRate, f.currentFeeRate)

	// Test that when we use a conf target of 0, ErrMaxPosition is
	// returned.
	increased, err = f.IncreaseFeeRate(0)
	rt.ErrorIs(err, ErrMaxPosition)
	rt.False(increased)
}
