package channeldb

import (
	"fmt"
	"testing"

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

// TestDecidePaymentStatus checks that given a set of HTLC and a failure
// reason, the payment's current status is returned as expected.
func TestDecidePaymentStatus(t *testing.T) {
	t.Parallel()

	// Create two attempts used for testing.
	inflight := HTLCAttempt{}
	settled := HTLCAttempt{
		Settle: &HTLCSettleInfo{Preimage: lntypes.Preimage{}},
	}
	failed := HTLCAttempt{
		Failure: &HTLCFailInfo{FailureSourceIndex: 1},
	}

	// Create a test failure reason and get the pointer.
	reason := FailureReasonNoRoute
	failure := &reason

	testCases := []struct {
		name           string
		htlcs          []HTLCAttempt
		reason         *FailureReason
		expectedStatus PaymentStatus
		expectedErr    error
	}{
		{
			// Test when inflight=true, settled=true, failed=true,
			// reason=yes.
			name: "state 1111",
			htlcs: []HTLCAttempt{
				inflight, settled, failed,
			},
			reason:         failure,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=true, failed=true,
			// reason=no.
			name: "state 1110",
			htlcs: []HTLCAttempt{
				inflight, settled, failed,
			},
			reason:         nil,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=true, failed=false,
			// reason=yes.
			name:           "state 1101",
			htlcs:          []HTLCAttempt{inflight, settled},
			reason:         failure,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=true, failed=false,
			// reason=no.
			name:           "state 1100",
			htlcs:          []HTLCAttempt{inflight, settled},
			reason:         nil,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=false, failed=true,
			// reason=yes.
			name:           "state 1011",
			htlcs:          []HTLCAttempt{inflight, failed},
			reason:         failure,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=false, failed=true,
			// reason=no.
			name:           "state 1010",
			htlcs:          []HTLCAttempt{inflight, failed},
			reason:         nil,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=false, failed=false,
			// reason=yes.
			name:           "state 1001",
			htlcs:          []HTLCAttempt{inflight},
			reason:         failure,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=true, settled=false, failed=false,
			// reason=no.
			name:           "state 1000",
			htlcs:          []HTLCAttempt{inflight},
			reason:         nil,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=false, settled=true, failed=true,
			// reason=yes.
			name:           "state 0111",
			htlcs:          []HTLCAttempt{settled, failed},
			reason:         failure,
			expectedStatus: StatusSucceeded,
		},
		{
			// Test when inflight=false, settled=true, failed=true,
			// reason=no.
			name:           "state 0110",
			htlcs:          []HTLCAttempt{settled, failed},
			reason:         nil,
			expectedStatus: StatusSucceeded,
		},
		{
			// Test when inflight=false, settled=true,
			// failed=false, reason=yes.
			name:           "state 0101",
			htlcs:          []HTLCAttempt{settled},
			reason:         failure,
			expectedStatus: StatusSucceeded,
		},
		{
			// Test when inflight=false, settled=true,
			// failed=false, reason=no.
			name:           "state 0100",
			htlcs:          []HTLCAttempt{settled},
			reason:         nil,
			expectedStatus: StatusSucceeded,
		},
		{
			// Test when inflight=false, settled=false,
			// failed=true, reason=yes.
			name:           "state 0011",
			htlcs:          []HTLCAttempt{failed},
			reason:         failure,
			expectedStatus: StatusFailed,
		},
		{
			// Test when inflight=false, settled=false,
			// failed=true, reason=no.
			name:           "state 0010",
			htlcs:          []HTLCAttempt{failed},
			reason:         nil,
			expectedStatus: StatusInFlight,
		},
		{
			// Test when inflight=false, settled=false,
			// failed=false, reason=yes.
			name:           "state 0001",
			htlcs:          []HTLCAttempt{},
			reason:         failure,
			expectedStatus: StatusFailed,
		},
		{
			// Test when inflight=false, settled=false,
			// failed=false, reason=no.
			name:           "state 0000",
			htlcs:          []HTLCAttempt{},
			reason:         nil,
			expectedStatus: StatusInitiated,
		},
	}

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

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

			status, err := decidePaymentStatus(tc.htlcs, tc.reason)
			require.Equalf(t, tc.expectedStatus, status,
				"got %s, want %s", status, tc.expectedStatus)
			require.ErrorIs(t, err, tc.expectedErr)
		})
	}
}

// TestPaymentStatusActions checks whether a list of actions can be applied
// against ALL possible payment statuses. Unlike normal unit tests where we
// check against a single function, all the actions including `removable`,
// `initable`, and `updatable` are tested together so this test can be used as
// a reference of state transition.
func TestPaymentStatusActions(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		status    PaymentStatus
		initErr   error
		updateErr error
		removeErr error
	}{
		{
			status:    StatusInitiated,
			initErr:   ErrPaymentExists,
			updateErr: nil,
			removeErr: nil,
		},
		{
			status:    StatusInFlight,
			initErr:   ErrPaymentInFlight,
			updateErr: nil,
			removeErr: ErrPaymentInFlight,
		},
		{
			status:    StatusSucceeded,
			initErr:   ErrAlreadyPaid,
			updateErr: ErrPaymentAlreadySucceeded,
			removeErr: nil,
		},
		{
			status:    StatusFailed,
			initErr:   nil,
			updateErr: ErrPaymentAlreadyFailed,
			removeErr: nil,
		},
		{
			status:    0,
			initErr:   ErrUnknownPaymentStatus,
			updateErr: ErrUnknownPaymentStatus,
			removeErr: ErrUnknownPaymentStatus,
		},
	}

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

		ps := tc.status
		name := fmt.Sprintf("test_%d_%s", i, ps.String())
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			require.ErrorIs(t, ps.initializable(), tc.initErr,
				"initable under state %v", tc.status)

			require.ErrorIs(t, ps.updatable(), tc.updateErr,
				"updatable under state %v", tc.status)

			require.ErrorIs(t, ps.removable(), tc.removeErr,
				"removable under state %v", tc.status)
		})
	}
}
