package invoices

import (
	"testing"
	"time"

	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/record"
	"github.com/stretchr/testify/require"
)

type updateHTLCTest struct {
	name     string
	input    InvoiceHTLC
	invState ContractState
	setID    *[32]byte
	output   InvoiceHTLC
	expErr   error
}

// TestUpdateHTLC asserts the behavior of the updateHTLC method in various
// scenarios for MPP and AMP.
func TestUpdateHTLC(t *testing.T) {
	t.Parallel()

	testNow := time.Now()
	setID := [32]byte{0x01}
	ampRecord := record.NewAMP([32]byte{0x02}, setID, 3)
	preimage := lntypes.Preimage{0x04}
	hash := preimage.Hash()

	diffSetID := [32]byte{0x05}
	fakePreimage := lntypes.Preimage{0x06}
	testAlreadyNow := time.Now()

	tests := []updateHTLCTest{
		{
			name: "MPP accept",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP:           nil,
			},
			invState: ContractAccepted,
			setID:    nil,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP:           nil,
			},
			expErr: nil,
		},
		{
			name: "MPP settle",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP:           nil,
			},
			invState: ContractSettled,
			setID:    nil,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP:           nil,
			},
			expErr: nil,
		},
		{
			name: "MPP cancel",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP:           nil,
			},
			invState: ContractCanceled,
			setID:    nil,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP:           nil,
			},
			expErr: nil,
		},
		{
			name: "AMP accept missing preimage",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: nil,
				},
			},
			invState: ContractAccepted,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: nil,
				},
			},
			expErr: ErrHTLCPreimageMissing,
		},
		{
			name: "AMP accept invalid preimage",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &fakePreimage,
				},
			},
			invState: ContractAccepted,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &fakePreimage,
				},
			},
			expErr: ErrHTLCPreimageMismatch,
		},
		{
			name: "AMP accept valid preimage",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractAccepted,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "AMP accept valid preimage different htlc set",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractAccepted,
			setID:    &diffSetID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "AMP settle missing preimage",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: nil,
				},
			},
			invState: ContractSettled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: nil,
				},
			},
			expErr: ErrHTLCPreimageMissing,
		},
		{
			name: "AMP settle invalid preimage",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &fakePreimage,
				},
			},
			invState: ContractSettled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &fakePreimage,
				},
			},
			expErr: ErrHTLCPreimageMismatch,
		},
		{
			name: "AMP settle valid preimage",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractSettled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			// With the newer AMP logic, this is now valid, as we
			// want to be able to accept multiple settle attempts
			// to a given pay_addr. In this case, the HTLC should
			// remain in the accepted state.
			name: "AMP settle valid preimage different htlc set",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractSettled,
			setID:    &diffSetID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "accept invoice htlc already settled",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractAccepted,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: ErrHTLCAlreadySettled,
		},
		{
			name: "cancel invoice htlc already settled",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractCanceled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: ErrHTLCAlreadySettled,
		},
		{
			name: "settle invoice htlc already settled",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractSettled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateSettled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "cancel invoice",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   time.Time{},
				Expiry:        40,
				State:         HtlcStateAccepted,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractCanceled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "accept invoice htlc already canceled",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractAccepted,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "cancel invoice htlc already canceled",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractCanceled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
		{
			name: "settle invoice htlc already canceled",
			input: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			invState: ContractSettled,
			setID:    &setID,
			output: InvoiceHTLC{
				Amt:           5000,
				MppTotalAmt:   5000,
				AcceptHeight:  100,
				AcceptTime:    testNow,
				ResolveTime:   testAlreadyNow,
				Expiry:        40,
				State:         HtlcStateCanceled,
				CustomRecords: make(record.CustomSet),
				AMP: &InvoiceHtlcAMPData{
					Record:   *ampRecord,
					Hash:     hash,
					Preimage: &preimage,
				},
			},
			expErr: nil,
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			testUpdateHTLC(t, test, testNow)
		})
	}
}

func testUpdateHTLC(t *testing.T, test updateHTLCTest, now time.Time) {
	htlc := test.input.Copy()
	stateChanged, state, err := getUpdatedHtlcState(
		htlc, test.invState, test.setID,
	)
	if stateChanged {
		htlc.State = state
		htlc.ResolveTime = now
	}

	require.Equal(t, test.expErr, err)
	require.Equal(t, test.output, *htlc)
}
