package loopin

import (
	"context"
	"testing"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/loop/fsm"
	"github.com/lightninglabs/loop/staticaddr/address"
	"github.com/lightninglabs/loop/staticaddr/deposit"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/stretchr/testify/require"
)

type testCase struct {
	name        string
	deposits    []*deposit.Deposit
	targetValue btcutil.Amount
	csvExpiry   uint32
	blockHeight uint32
	expected    []*deposit.Deposit
	expectedErr string
}

// TestSelectDeposits tests the selectDeposits function, which selects
// deposits that can cover a target value while respecting the dust limit.
func TestSelectDeposits(t *testing.T) {
	d1, d2, d3, d4 := &deposit.Deposit{
		Value:              1_000_000,
		ConfirmationHeight: 5_000,
	}, &deposit.Deposit{
		Value:              2_000_000,
		ConfirmationHeight: 5_001,
	}, &deposit.Deposit{
		Value:              3_000_000,
		ConfirmationHeight: 5_002,
	}, &deposit.Deposit{
		Value:              3_000_000,
		ConfirmationHeight: 5_003,
	}
	d1.Hash = chainhash.Hash{1}
	d1.Index = 0
	d2.Hash = chainhash.Hash{2}
	d2.Index = 0
	d3.Hash = chainhash.Hash{3}
	d3.Index = 0
	d4.Hash = chainhash.Hash{4}
	d4.Index = 0

	testCases := []testCase{
		{
			name:        "single deposit exact target",
			deposits:    []*deposit.Deposit{d1},
			targetValue: 1_000_000,
			expected:    []*deposit.Deposit{d1},
			expectedErr: "",
		},
		{
			name:        "prefer larger deposit when both cover",
			deposits:    []*deposit.Deposit{d1, d2},
			targetValue: 1_000_000,
			expected:    []*deposit.Deposit{d2},
			expectedErr: "",
		},
		{
			name:        "prefer largest among three when one is enough",
			deposits:    []*deposit.Deposit{d1, d2, d3},
			targetValue: 1_000_000,
			expected:    []*deposit.Deposit{d3},
			expectedErr: "",
		},
		{
			name:        "single deposit insufficient by 1",
			deposits:    []*deposit.Deposit{d1},
			targetValue: 1_000_001,
			expected:    []*deposit.Deposit{},
			expectedErr: "not enough deposits to cover",
		},
		{
			name:        "target leaves exact dust limit change",
			deposits:    []*deposit.Deposit{d1},
			targetValue: 1_000_000 - dustLimit,
			expected:    []*deposit.Deposit{d1},
			expectedErr: "",
		},
		{
			name:        "target leaves dust change (just over)",
			deposits:    []*deposit.Deposit{d1},
			targetValue: 1_000_000 - dustLimit + 1,
			expected:    []*deposit.Deposit{},
			expectedErr: "not enough deposits to cover",
		},
		{
			name:        "all deposits exactly match target",
			deposits:    []*deposit.Deposit{d1, d2, d3},
			targetValue: d1.Value + d2.Value + d3.Value,
			expected:    []*deposit.Deposit{d1, d2, d3},
			expectedErr: "",
		},
		{
			name:        "sum minus dust limit is allowed (change == dust)",
			deposits:    []*deposit.Deposit{d1, d2, d3},
			targetValue: d1.Value + d2.Value + d3.Value - dustLimit,
			expected:    []*deposit.Deposit{d1, d2, d3},
			expectedErr: "",
		},
		{
			name:        "sum minus dust limit plus 1 is not allowed (dust change)",
			deposits:    []*deposit.Deposit{d1, d2, d3},
			targetValue: d1.Value + d2.Value + d3.Value - dustLimit + 1,
			expected:    []*deposit.Deposit{},
			expectedErr: "not enough deposits to cover",
		},
		{
			name:        "tie by value, prefer earlier expiry",
			deposits:    []*deposit.Deposit{d3, d4},
			targetValue: d4.Value - dustLimit, // d3/d4 have the
			// same value but different expiration.
			expected:    []*deposit.Deposit{d3},
			expectedErr: "",
		},
		{
			name: "prefilter filters deposits close to expiry",
			deposits: func() []*deposit.Deposit {
				// dClose expires before
				// htlcExpiry+DepositHtlcDelta and must be
				// filtered out. dOK expires exactly at the
				// threshold and must be eligible.
				dClose := &deposit.Deposit{
					Value:              3_000_000,
					ConfirmationHeight: 3000,
				}
				dClose.Hash = chainhash.Hash{5}
				dClose.Index = 0
				dOK := &deposit.Deposit{
					Value:              2_000_000,
					ConfirmationHeight: 3050,
				}
				dOK.Hash = chainhash.Hash{6}
				dOK.Index = 0
				return []*deposit.Deposit{dClose, dOK}
			}(),
			targetValue: 1_000_000,
			csvExpiry:   1000,
			blockHeight: 3000,
			expected: func() []*deposit.Deposit {
				// Only dOK should be considered.
				// dClose is filtered.
				dOK := &deposit.Deposit{
					Value:              2_000_000,
					ConfirmationHeight: 3050,
				}
				dOK.Hash = chainhash.Hash{6}
				dOK.Index = 0
				return []*deposit.Deposit{dOK}
			}(),
			expectedErr: "",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			selectedDeposits, err := SelectDeposits(
				tc.targetValue, tc.deposits, tc.csvExpiry,
				tc.blockHeight,
			)
			if tc.expectedErr == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, err, tc.expectedErr)
			}
			require.ElementsMatch(t, tc.expected, selectedDeposits)
		})
	}
}

// mockDepositManager implements DepositManager for tests.
type mockDepositManager struct {
	byOutpoint map[string]*deposit.Deposit
}

func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
	[]*deposit.Deposit, error) {

	return nil, nil
}

func (m *mockDepositManager) AllStringOutpointsActiveDeposits(_ []string,
	_ fsm.StateType) ([]*deposit.Deposit, bool) {

	return nil, false
}

func (m *mockDepositManager) TransitionDeposits(_ context.Context,
	_ []*deposit.Deposit, _ fsm.EventType, _ fsm.StateType) error {

	return nil
}

func (m *mockDepositManager) DepositsForOutpoints(_ context.Context,
	outpoints []string, ignoreUnknown bool) ([]*deposit.Deposit, error) {

	res := make([]*deposit.Deposit, 0, len(outpoints))
	for _, op := range outpoints {
		if d, ok := m.byOutpoint[op]; ok {
			res = append(res, d)
		}
	}
	return res, nil
}

func (m *mockDepositManager) GetActiveDepositsInState(_ fsm.StateType) (
	[]*deposit.Deposit, error) {

	return nil, nil
}

// mockStore implements StaticAddressLoopInStore for tests.
type mockStore struct {
	loopIns map[lntypes.Hash]*StaticAddressLoopIn
	mapIDs  map[lntypes.Hash][]deposit.ID
}

func (s *mockStore) CreateLoopIn(_ context.Context,
	_ *StaticAddressLoopIn) error {

	return nil
}

func (s *mockStore) UpdateLoopIn(_ context.Context,
	_ *StaticAddressLoopIn) error {

	return nil
}

func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context,
	_ []fsm.StateType) ([]*StaticAddressLoopIn, error) {

	return nil, nil
}
func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
	return false, nil
}

func (s *mockStore) GetLoopInByHash(_ context.Context,
	swapHash lntypes.Hash) (*StaticAddressLoopIn, error) {

	li, ok := s.loopIns[swapHash]
	if !ok {
		return nil, nil
	}
	return li, nil
}
func (s *mockStore) SwapHashesForDepositIDs(_ context.Context,
	depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error) {

	// Filter the prepared mapping to only include hashes that reference
	// any of the provided deposit IDs.
	idSet := make(map[deposit.ID]struct{}, len(depositIDs))
	for _, id := range depositIDs {
		idSet[id] = struct{}{}
	}
	res := make(map[lntypes.Hash][]deposit.ID)
	for h, ids := range s.mapIDs {
		for _, id := range ids {
			if _, ok := idSet[id]; ok {
				res[h] = ids
				break
			}
		}
	}

	return res, nil
}

// helper to create a deposit with specific outpoint and value.
func makeDeposit(h byte, index uint32, value btcutil.Amount) *deposit.Deposit {
	d := &deposit.Deposit{Value: value}
	d.Hash = chainhash.Hash{h}
	d.Index = index
	var id deposit.ID
	id[0] = h
	d.ID = id

	return d
}

// helper to outpoint string as used by txin.PreviousOutPoint.String().
func outpointString(d *deposit.Deposit) string {
	return wire.OutPoint{Hash: d.Hash, Index: d.Index}.String()
}

// build a sweep tx with given inputs and outputs.
func makeSweepTx(inputs []wire.OutPoint, outputs []*wire.TxOut) *wire.MsgTx {
	tx := wire.NewMsgTx(2)
	for _, in := range inputs {
		tx.AddTxIn(&wire.TxIn{PreviousOutPoint: in})
	}
	for _, out := range outputs {
		tx.AddTxOut(out)
	}

	return tx
}

// TestCheckChange exercises all relevant scenarios for checkChange.
func TestCheckChange(t *testing.T) {
	ctx := context.Background()

	// Prepare a common change address and an alternate address.
	changeAddr := &address.Parameters{PkScript: []byte{0xaa, 0xbb}}
	otherAddr := &address.Parameters{PkScript: []byte{0xcc, 0xdd}}
	serverAddr := &address.Parameters{PkScript: []byte{0xee, 0xff}}

	// Prepare swaps (loop-ins) with varying deposit totals and selections.
	// Helper to make a swap with deposits and selected amount.
	makeSwap := func(h byte, deposits []*deposit.Deposit,
		selected btcutil.Amount) (lntypes.Hash, *StaticAddressLoopIn) {

		var hash lntypes.Hash
		hash[0] = h
		li := &StaticAddressLoopIn{
			Deposits:       deposits,
			SelectedAmount: selected,
			AddressParams:  changeAddr,
		}
		return hash, li
	}

	// Deposits belonging to different swaps.
	s1d1 := makeDeposit(1, 0, 1000)
	s1d2 := makeDeposit(1, 1, 2000)
	s2d1 := makeDeposit(2, 0, 1500)
	s3d1 := makeDeposit(3, 0, 800)
	s4d1 := makeDeposit(4, 0, 900)

	// Swaps:
	// A: total 3000, selected 3000 => no change.
	hA, liA := makeSwap(10, []*deposit.Deposit{s1d1, s1d2}, 3000)
	// B: total 1500, selected 1000 => change 500.
	hB, liB := makeSwap(11, []*deposit.Deposit{s2d1}, 1000)
	// C: total 800, selected 400 => change 400.
	hC, liC := makeSwap(12, []*deposit.Deposit{s3d1}, 400)
	// D: total 900, selected 500 => change 400.
	hD, liD := makeSwap(13, []*deposit.Deposit{s4d1}, 500)

	// Mapping deposits -> swaps (by deposit IDs).
	mapIDs := map[lntypes.Hash][]deposit.ID{
		hA: {s1d1.ID, s1d2.ID},
		hB: {s2d1.ID},
		hC: {s3d1.ID},
		hD: {s4d1.ID},
	}

	loopIns := map[lntypes.Hash]*StaticAddressLoopIn{
		hA: liA,
		hB: liB,
		hC: liC,
		hD: liD,
	}

	// Common manager with mocked dependencies; will change inputs per test.
	mgr := &Manager{
		cfg: &Config{
			DepositManager: &mockDepositManager{
				byOutpoint: map[string]*deposit.Deposit{},
			},
			Store: &mockStore{
				loopIns: loopIns,
				mapIDs:  mapIDs,
			},
		},
	}

	type testCase struct {
		name           string
		inDeps         []*deposit.Deposit // deposits referenced by tx inputs
		outputs        []*wire.TxOut      // outputs in sweep tx
		addr           *address.Parameters
		expectErr      bool
		expectedErrMsg string
	}

	cases := []testCase{
		{
			name:   "no change expected (selected == total)",
			inDeps: []*deposit.Deposit{s1d1, s1d2},
			// No change output required.
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
			},
			addr: changeAddr,
		},
		{
			name:   "single swap change present",
			inDeps: []*deposit.Deposit{s2d1}, // B -> change 500
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
				{
					Value:    500,
					PkScript: changeAddr.PkScript,
				},
			},
			addr: changeAddr,
		},
		{
			name:   "multiple swaps different change amounts",
			inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)=900
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
				{
					Value:    900,
					PkScript: changeAddr.PkScript,
				},
			},
			addr: changeAddr,
		},
		{
			name:   "two swaps with identical change values sum correctly",
			inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)=800
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
				{
					Value:    800,
					PkScript: changeAddr.PkScript,
				},
			},
			addr: changeAddr,
		},
		{
			name:           "missing change output results in error",
			inDeps:         []*deposit.Deposit{s2d1}, // expect 500
			outputs:        []*wire.TxOut{},
			addr:           changeAddr,
			expectErr:      true,
			expectedErrMsg: "couldn't find expected change",
		},
		{
			name:   "wrong address for change output",
			inDeps: []*deposit.Deposit{s2d1}, // expect 500
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
				{
					Value:    500,
					PkScript: otherAddr.PkScript,
				},
			},
			addr:           changeAddr,
			expectErr:      true,
			expectedErrMsg: "couldn't find expected change",
		},
		{
			name:   "wrong amount for change output",
			inDeps: []*deposit.Deposit{s2d1}, // expect 500
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
				{
					Value:    400,
					PkScript: changeAddr.PkScript,
				},
			},
			addr:           changeAddr,
			expectErr:      true,
			expectedErrMsg: "couldn't find expected change",
		},
		{
			name:   "mixed swaps some with change some without",
			inDeps: []*deposit.Deposit{s1d1, s1d2, s3d1}, // A(0)+C(400)=400
			outputs: []*wire.TxOut{
				{
					Value:    1337,
					PkScript: serverAddr.PkScript,
				},
				{
					Value:    400,
					PkScript: changeAddr.PkScript,
				},
				{
					Value:    1000,
					PkScript: otherAddr.PkScript,
				},
			},
			addr: changeAddr,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			// Prepare inputs mapping for deposit manager.
			mdm := &mockDepositManager{
				byOutpoint: map[string]*deposit.Deposit{},
			}
			inputs := make([]wire.OutPoint, 0, len(tc.inDeps))
			for _, d := range tc.inDeps {
				mdm.byOutpoint[outpointString(d)] = d
				inputs = append(
					inputs, wire.OutPoint{
						Hash:  d.Hash,
						Index: d.Index,
					},
				)
			}
			mgr.cfg.DepositManager = mdm

			tx := makeSweepTx(inputs, tc.outputs)
			err := mgr.checkChange(ctx, tx, tc.addr)
			if tc.expectErr {
				require.Error(t, err)
				if tc.expectedErrMsg != "" {
					require.ErrorContains(
						t, err, tc.expectedErrMsg,
					)
				}
			} else {
				require.NoError(t, err)
			}
		})
	}
}
