package loopdb

import (
	"context"
	"crypto/sha256"
	"errors"
	"math/rand"
	"reflect"
	"testing"
	"time"

	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/lightninglabs/loop/loopdb/sqlc"
	"github.com/lightninglabs/loop/test"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/stretchr/testify/require"
)

const (
	testLabel = "test label"
)

var (
	testAssetId = []byte{
		1, 1, 1, 1, 2, 2, 2, 2,
		3, 3, 3, 3, 4, 4, 4, 4,
		1, 1, 1, 1, 2, 2, 2, 2,
		3, 3, 3, 3, 4, 4, 4, 4,
	}
)

// TestSqliteLoopOutStore tests all the basic functionality of the current
// sqlite swap store.
func TestSqliteLoopOutStore(t *testing.T) {
	destAddr := test.GetDestAddr(t, 0)
	initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)

	// Next, we'll make a new pending swap that we'll insert into the
	// database shortly.
	unrestrictedSwap := LoopOutContract{
		SwapContract: SwapContract{
			AmountRequested: 100,
			Preimage:        testPreimage,
			CltvExpiry:      144,
			HtlcKeys: HtlcKeys{
				SenderScriptKey:        senderKey,
				ReceiverScriptKey:      receiverKey,
				SenderInternalPubKey:   senderInternalKey,
				ReceiverInternalPubKey: receiverInternalKey,
				ClientScriptKeyLocator: keychain.KeyLocator{
					Family: 1,
					Index:  2,
				},
			},
			MaxMinerFee: 10,
			MaxSwapFee:  20,

			InitiationHeight: 99,

			InitiationTime:  initiationTime,
			ProtocolVersion: ProtocolVersionMuSig2,
		},
		MaxPrepayRoutingFee:     40,
		PrepayInvoice:           "prepayinvoice",
		DestAddr:                destAddr,
		SwapInvoice:             "swapinvoice",
		MaxSwapRoutingFee:       30,
		SweepConfTarget:         2,
		HtlcConfirmations:       2,
		SwapPublicationDeadline: initiationTime,
		PaymentTimeout:          time.Second * 11,
	}

	t.Run("no outgoing set", func(t *testing.T) {
		testSqliteLoopOutStore(t, &unrestrictedSwap)
	})

	restrictedSwap := unrestrictedSwap
	restrictedSwap.OutgoingChanSet = ChannelSet{1, 2}

	t.Run("two channel outgoing set", func(t *testing.T) {
		testSqliteLoopOutStore(t, &restrictedSwap)
	})

	labelledSwap := unrestrictedSwap
	labelledSwap.Label = testLabel
	t.Run("labelled swap", func(t *testing.T) {
		testSqliteLoopOutStore(t, &labelledSwap)
	})

	assetSwap := unrestrictedSwap
	assetSwap.AssetSwapInfo = &LoopOutAssetSwap{
		AssetId:     testAssetId,
		PrepayRfqId: testAssetId,
		SwapRfqId:   testAssetId,
	}

	t.Run("asset swap", func(t *testing.T) {
		testSqliteLoopOutStore(t, &assetSwap)
	})
}

// testSqliteLoopOutStore tests the basic functionality of the current sqlite
// swap store for specific swap parameters.
func testSqliteLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
	store := NewTestDB(t)

	ctxb := context.Background()

	// First, verify that an empty database has no active swaps.
	swaps, err := store.FetchLoopOutSwaps(ctxb)

	require.NoError(t, err)
	require.Empty(t, swaps)

	hash := pendingSwap.Preimage.Hash()

	// checkSwap is a test helper function that'll assert the state of a
	// swap.
	checkSwap := func(expectedState SwapState) {
		t.Helper()

		swaps, err := store.FetchLoopOutSwaps(ctxb)
		require.NoError(t, err)

		require.Len(t, swaps, 1)

		swap, err := store.FetchLoopOutSwap(ctxb, hash)
		require.NoError(t, err)

		require.Equal(t, hash, swap.Hash)
		require.Equal(t, hash, swaps[0].Hash)

		swapContract := swap.Contract

		require.Equal(t, swapContract, pendingSwap)

		require.Equal(t, expectedState, swap.State().State)

		if expectedState == StatePreimageRevealed {
			require.NotNil(t, swap.State().HtlcTxHash)
		}

		require.Equal(t, time.Second*11, swap.Contract.PaymentTimeout)
	}

	// If we create a new swap, then it should show up as being initialized
	// right after.
	err = store.CreateLoopOut(ctxb, hash, pendingSwap)
	require.NoError(t, err)

	checkSwap(StateInitiated)

	// Trying to make the same swap again should result in an error.
	err = store.CreateLoopOut(ctxb, hash, pendingSwap)
	require.Error(t, err)
	checkSwap(StateInitiated)

	// Next, we'll update to the next state of the pre-image being
	// revealed. The state should be reflected here again.
	err = store.UpdateLoopOut(
		ctxb, hash, testTime,
		SwapStateData{
			State:      StatePreimageRevealed,
			HtlcTxHash: &chainhash.Hash{1, 6, 2},
		},
	)
	require.NoError(t, err)

	checkSwap(StatePreimageRevealed)

	// Next, we'll update to the final state to ensure that the state is
	// properly updated.
	err = store.UpdateLoopOut(
		ctxb, hash, testTime,
		SwapStateData{
			State: StateFailInsufficientValue,
		},
	)
	require.NoError(t, err)
	checkSwap(StateFailInsufficientValue)

	err = store.Close()
	require.NoError(t, err)
}

// TestSQLliteLoopInStore tests all the basic functionality of the current
// sqlite swap store.
func TestSQLliteLoopInStore(t *testing.T) {
	initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)

	// Next, we'll make a new pending swap that we'll insert into the
	// database shortly.
	lastHop := route.Vertex{1, 2, 3}

	pendingSwap := LoopInContract{
		SwapContract: SwapContract{
			AmountRequested: 100,
			Preimage:        testPreimage,
			CltvExpiry:      144,
			HtlcKeys: HtlcKeys{
				SenderScriptKey:        senderKey,
				ReceiverScriptKey:      receiverKey,
				SenderInternalPubKey:   senderInternalKey,
				ReceiverInternalPubKey: receiverInternalKey,
				ClientScriptKeyLocator: keychain.KeyLocator{
					Family: 1,
					Index:  2,
				},
			},
			MaxMinerFee:      10,
			MaxSwapFee:       20,
			InitiationHeight: 99,

			// Convert to/from unix to remove timezone, so that it
			// doesn't interfere with DeepEqual.
			InitiationTime:  initiationTime,
			ProtocolVersion: ProtocolVersionMuSig2,
		},
		HtlcConfTarget: 2,
		LastHop:        &lastHop,
		ExternalHtlc:   true,
	}

	t.Run("loop in", func(t *testing.T) {
		testSqliteLoopInStore(t, pendingSwap)
	})

	labelledSwap := pendingSwap
	labelledSwap.Label = testLabel
	t.Run("loop in with label", func(t *testing.T) {
		testSqliteLoopInStore(t, labelledSwap)
	})
}

func testSqliteLoopInStore(t *testing.T, pendingSwap LoopInContract) {
	store := NewTestDB(t)

	ctxb := context.Background()

	// First, verify that an empty database has no active swaps.
	swaps, err := store.FetchLoopInSwaps(ctxb)
	require.NoError(t, err)
	require.Empty(t, swaps)

	hash := sha256.Sum256(testPreimage[:])

	// checkSwap is a test helper function that'll assert the state of a
	// swap.
	checkSwap := func(expectedState SwapState) {
		t.Helper()

		swaps, err := store.FetchLoopInSwaps(ctxb)
		require.NoError(t, err)
		require.Len(t, swaps, 1)

		swap := swaps[0].Contract

		require.Equal(t, swap, &pendingSwap)

		require.Equal(t, swaps[0].State().State, expectedState)
	}

	// If we create a new swap, then it should show up as being initialized
	// right after.
	err = store.CreateLoopIn(ctxb, hash, &pendingSwap)
	require.NoError(t, err)

	checkSwap(StateInitiated)

	// Trying to make the same swap again should result in an error.
	err = store.CreateLoopIn(ctxb, hash, &pendingSwap)
	require.Error(t, err)

	checkSwap(StateInitiated)

	// Next, we'll update to the next state of the pre-image being
	// revealed. The state should be reflected here again.
	err = store.UpdateLoopIn(
		ctxb, hash, testTime,
		SwapStateData{
			State: StatePreimageRevealed,
		},
	)
	require.NoError(t, err)

	checkSwap(StatePreimageRevealed)

	// Next, we'll update to the final state to ensure that the state is
	// properly updated.
	err = store.UpdateLoopIn(
		ctxb, hash, testTime,
		SwapStateData{
			State: StateFailInsufficientValue,
		},
	)
	require.NoError(t, err)
	checkSwap(StateFailInsufficientValue)

	err = store.Close()
	require.NoError(t, err)
}

// TestSqliteLiquidityParams checks that reading and writing to liquidty bucket are
// as expected.
func TestSqliteLiquidityParams(t *testing.T) {
	ctxb := context.Background()

	store := NewTestDB(t)

	// Test when there's no params saved before, an empty bytes is
	// returned.
	params, err := store.FetchLiquidityParams(ctxb)
	require.NoError(t, err, "failed to fetch params")
	require.Empty(t, params, "expect empty bytes")
	require.Nil(t, params, "expected nil byte array")

	params = []byte("test")

	// Test we can save the params.
	err = store.PutLiquidityParams(ctxb, params)
	require.NoError(t, err, "failed to put params")

	// Now fetch the db again should return the above saved bytes.
	paramsRead, err := store.FetchLiquidityParams(ctxb)
	require.NoError(t, err, "failed to fetch params")
	require.Equal(t, params, paramsRead, "unexpected return value")
}

// TestSqliteTypeConversion is a small test that checks that we can safely
// convert between the :one and :many types from sqlc.
func TestSqliteTypeConversion(t *testing.T) {
	loopOutSwapRow := sqlc.GetLoopOutSwapRow{}
	err := randomStruct(&loopOutSwapRow)
	require.NoError(t, err)
	require.NotNil(t, loopOutSwapRow.DestAddress)

	loopOutSwapsRow := sqlc.GetLoopOutSwapsRow(loopOutSwapRow)
	require.EqualValues(t, loopOutSwapRow, loopOutSwapsRow)
}

// TestIssue615 tests that on faulty timestamps, the database will be fixed.
// Reference: https://github.com/lightninglabs/lightning-terminal/issues/615
func TestIssue615(t *testing.T) {
	ctxb := context.Background()

	// Create an invoice to get the timestamp from.
	invoice := "lnbc5u1pje2dyusp5qs356crpns9u3we8hw7w9gntfz89zkcaxu6w6h6a" +
		"pw6jlgc0cynqpp5y2xdzu4eqasuttxp3nrk72vqdzce3wead7nmf693uqpgx" +
		"2hd533qdpcyfnx2etyyp3ks6trddjkuueqw3hkketwwv7kgvrd0py95d6vvv" +
		"65z0fzxqzfvcqpjrzjqd82srutzjx82prr234anxdlwvs6peklcc92lp9aqs" +
		"q296xnwmqd2rrf9gqqtwqqqqqqqqqqqqqqqqqq9q9qxpqysgq768236z7cx6" +
		"gyy766wajrmpnpt6wavkf5nypwyj6r3dcxm89aggq2jm2kznaxvr0lrsqgv7" +
		"592upfh5ruyrwzy5tethpzere78xfgwqp64jrpa"

	// Create a new sqlite store for testing.
	sqlDB := NewTestDB(t)

	// Create a faulty loopout swap.
	destAddr := test.GetDestAddr(t, 0)
	// Corresponds to 55563-06-27 02:09:24 +0000 UTC.
	faultyTime := time.Unix(1691247002964, 0)

	t.Log(faultyTime.Unix())

	unrestrictedSwap := LoopOutContract{
		SwapContract: SwapContract{
			AmountRequested: 100,
			Preimage:        testPreimage,
			CltvExpiry:      144,
			HtlcKeys: HtlcKeys{
				SenderScriptKey:        senderKey,
				ReceiverScriptKey:      receiverKey,
				SenderInternalPubKey:   senderInternalKey,
				ReceiverInternalPubKey: receiverInternalKey,
				ClientScriptKeyLocator: keychain.KeyLocator{
					Family: 1,
					Index:  2,
				},
			},
			MaxMinerFee:      10,
			MaxSwapFee:       20,
			InitiationHeight: 99,
			InitiationTime:   time.Now(),
			ProtocolVersion:  ProtocolVersionMuSig2,
		},
		MaxPrepayRoutingFee:     40,
		PrepayInvoice:           "prepayinvoice",
		DestAddr:                destAddr,
		SwapInvoice:             invoice,
		MaxSwapRoutingFee:       30,
		SweepConfTarget:         2,
		HtlcConfirmations:       2,
		SwapPublicationDeadline: faultyTime,
	}

	err := sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap)
	require.NoError(t, err)

	// This should fail because of the faulty timestamp.
	_, err = sqlDB.GetLoopOutSwaps(ctxb)

	// If we're using sqlite, we expect an error.
	if testDBType == "sqlite" {
		require.Error(t, err)
	} else {
		require.NoError(t, err)
	}

	// Fix the faulty timestamp.
	err = sqlDB.FixFaultyTimestamps(ctxb)
	require.NoError(t, err)

	_, err = sqlDB.GetLoopOutSwaps(ctxb)
	require.NoError(t, err)
}

// TestBatchUpdateCost tests that we can batch update the cost of multiple swaps
// at once.
func TestBatchUpdateCost(t *testing.T) {
	// Create a new sqlite store for testing.
	store := NewTestDB(t)

	destAddr := test.GetDestAddr(t, 0)
	initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)

	testContract := LoopOutContract{
		SwapContract: SwapContract{
			AmountRequested: 100,
			CltvExpiry:      144,
			HtlcKeys: HtlcKeys{
				SenderScriptKey:        senderKey,
				ReceiverScriptKey:      receiverKey,
				SenderInternalPubKey:   senderInternalKey,
				ReceiverInternalPubKey: receiverInternalKey,
				ClientScriptKeyLocator: keychain.KeyLocator{
					Family: 1,
					Index:  2,
				},
			},
			MaxMinerFee: 10,
			MaxSwapFee:  20,

			InitiationHeight: 99,

			InitiationTime:  initiationTime,
			ProtocolVersion: ProtocolVersionMuSig2,
		},
		MaxPrepayRoutingFee:     40,
		PrepayInvoice:           "prepayinvoice",
		DestAddr:                destAddr,
		SwapInvoice:             "swapinvoice",
		MaxSwapRoutingFee:       30,
		SweepConfTarget:         2,
		HtlcConfirmations:       2,
		SwapPublicationDeadline: initiationTime,
		PaymentTimeout:          time.Second * 11,
	}

	makeSwap := func(preimage lntypes.Preimage) *LoopOutContract {
		contract := testContract
		contract.Preimage = preimage

		return &contract
	}

	// Next, we'll add two swaps to the database.
	preimage1 := testPreimage
	preimage2 := lntypes.Preimage{4, 4, 4}

	ctxb := context.Background()
	swap1 := makeSwap(preimage1)
	swap2 := makeSwap(preimage2)

	hash1 := swap1.Preimage.Hash()
	err := store.CreateLoopOut(ctxb, hash1, swap1)
	require.NoError(t, err)

	hash2 := swap2.Preimage.Hash()
	err = store.CreateLoopOut(ctxb, hash2, swap2)
	require.NoError(t, err)

	// Add an update to both swaps containing the cost.
	err = store.UpdateLoopOut(
		ctxb, hash1, testTime,
		SwapStateData{
			State: StateSuccess,
			Cost: SwapCost{
				Server:   1,
				Onchain:  2,
				Offchain: 3,
			},
		},
	)
	require.NoError(t, err)

	err = store.UpdateLoopOut(
		ctxb, hash2, testTime,
		SwapStateData{
			State: StateSuccess,
			Cost: SwapCost{
				Server:   4,
				Onchain:  5,
				Offchain: 6,
			},
		},
	)
	require.NoError(t, err)

	updateMap := map[lntypes.Hash]SwapCost{
		hash1: {
			Server:   2,
			Onchain:  3,
			Offchain: 4,
		},
		hash2: {
			Server:   6,
			Onchain:  7,
			Offchain: 8,
		},
	}
	require.NoError(t, store.BatchUpdateLoopOutSwapCosts(ctxb, updateMap))

	swaps, err := store.FetchLoopOutSwaps(ctxb)
	require.NoError(t, err)
	require.Len(t, swaps, 2)

	swapsMap := make(map[lntypes.Hash]*LoopOut)
	swapsMap[swaps[0].Hash] = swaps[0]
	swapsMap[swaps[1].Hash] = swaps[1]

	require.Equal(t, updateMap[hash1], swapsMap[hash1].State().Cost)
	require.Equal(t, updateMap[hash2], swapsMap[hash2].State().Cost)
}

// TestMigrationTracker tests the migration tracker functionality.
func TestMigrationTracker(t *testing.T) {
	ctxb := context.Background()

	// Create a new sqlite store for testing.
	sqlDB := NewTestDB(t)
	hasMigration, err := sqlDB.HasMigration(ctxb, "test")
	require.NoError(t, err)
	require.False(t, hasMigration)

	require.NoError(t, sqlDB.SetMigration(ctxb, "test"))
	hasMigration, err = sqlDB.HasMigration(ctxb, "test")
	require.NoError(t, err)
	require.True(t, hasMigration)
}

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func randomString(length int) string {
	b := make([]byte, length)
	for i := range b {
		b[i] = charset[rand.Intn(len(charset))]
	}
	return string(b)
}

func randomBytes(length int) []byte {
	b := make([]byte, length)
	for i := range b {
		b[i] = byte(rand.Intn(256))
	}
	return b
}

func randomStruct(v interface{}) error {
	val := reflect.ValueOf(v)
	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
		return errors.New("Input should be a pointer to a struct type")
	}

	val = val.Elem()
	for i := 0; i < val.NumField(); i++ {
		field := val.Field(i)

		switch field.Kind() {
		case reflect.Int64:
			if field.CanSet() {
				field.SetInt(rand.Int63())
			}

		case reflect.String:
			if field.CanSet() {
				field.SetString(randomString(10))
			}

		case reflect.Slice:
			if field.Type().Elem().Kind() == reflect.Uint8 {
				if field.CanSet() {
					field.SetBytes(randomBytes(32))
				}
			}

		case reflect.Struct:
			if field.Type() == reflect.TypeOf(time.Time{}) {
				if field.CanSet() {
					field.Set(reflect.ValueOf(time.Now()))
				}
			}
			if field.Type() == reflect.TypeOf(route.Vertex{}) {
				if field.CanSet() {
					vertex, err := route.NewVertexFromBytes(
						randomBytes(route.VertexSize),
					)
					if err != nil {
						return err
					}

					field.Set(reflect.ValueOf(vertex))
				}
			}
		}
	}

	return nil
}
