//go:build test_db_postgres || test_db_sqlite

package migration1

import (
	"context"
	"fmt"
	"os"
	"path"
	"strings"
	"testing"
	"time"

	"github.com/btcsuite/btclog/v2"
	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/lightningnetwork/lnd/kvdb/postgres"
	"github.com/lightningnetwork/lnd/kvdb/sqlbase"
	"github.com/lightningnetwork/lnd/kvdb/sqlite"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
	"github.com/lightningnetwork/lnd/sqldb"
	"github.com/stretchr/testify/require"
)

// TestMigrationWithExternalDB tests the migration of the payment store from a
// bolt backed channel.db or a kvdb channel.sqlite to a SQL database. Note that
// this test does not attempt to be a complete migration test for all payment
// store types but rather is added as a tool for developers and users to debug
// payment migration issues with an actual channel.db/channel.sqlite file.
//
// NOTE: To use this test, place either of those files in the
// payments/db/migration1/testdata directory, uncomment the "Skipf" line, and
// set the "fileName" variable to the name of the channel database file you
// want to use for the migration test.
func TestMigrationWithExternalDB(t *testing.T) {
	ctx := context.Background()

	// NOTE: comment this line out to run the test.
	t.Skipf("skipping test meant for local debugging only")

	// NOTE: set this to the name of the channel database file you want
	// to use for the migration test. This may be either a bbolt ".db" file
	// or a SQLite ".sqlite" file. If you want to migrate from a
	// bbolt channel.db file, set this to "channel.db".
	const fileName = "channel.db"

	// NOTE: if set, this test will prefer migrating from a Postgres-backed
	// kvdb source instead of a local file. Leave empty to use fileName.
	const postgresKVDSN = ""
	const postgresKVPfx = "channeldb"
	const logSequenceOrder = false

	// Determine if we are using a SQLite file or a Bolt DB file.
	isSqlite := strings.HasSuffix(fileName, ".sqlite")

	// Set up logging for the test.
	logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout))
	UseLogger(logger)

	// migrate runs the migration from the kvdb store to the SQL store
	// and then performs a batched deep comparison of every migrated
	// payment to ensure all data (including HTLC details) was preserved
	// correctly.
	migrate := func(t *testing.T, kvBackend kvdb.Backend) {
		sqlStore := setupTestSQLDB(t)

		// Run migration in a transaction.
		err := sqlStore.db.ExecTx(
			ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error {
				migTx, ok := tx.(SQLMigrationQueries)
				if !ok {
					return fmt.Errorf("db does not " +
						"implement SQLMigrationQueries")
				}

				return MigratePaymentsKVToSQL(
					ctx, kvBackend, migTx, &SQLStoreConfig{
						QueryCfg: sqlStore.cfg.QueryCfg,
					},
				)
			}, sqldb.NoOpReset,
		)
		require.NoError(t, err)

		t.Log("========================================")
		t.Log("   Deep Validation")
		t.Log("========================================")

		// Deep compare all migrated payments in batches.
		deepValidateAllPayments(
			t, ctx, kvBackend, sqlStore,
		)

		_ = logSequenceOrder
	}

	connectPostgres := func(t *testing.T, dsn, prefix string) kvdb.Backend {
		dsn = strings.TrimSpace(dsn)
		if dsn == "" {
			t.Fatalf("missing postgres kvdb dsn")
		}

		prefix = strings.TrimSpace(prefix)
		if prefix == "" {
			prefix = "channeldb"
		}

		const (
			timeout  = 10 * time.Second
			maxConns = 5
		)
		sqlbase.Init(maxConns)

		dbCfg := &postgres.Config{
			Dsn:            dsn,
			Timeout:        timeout,
			MaxConnections: maxConns,
		}

		kvStore, err := kvdb.Open(
			kvdb.PostgresBackendName, ctx, dbCfg, prefix,
		)
		require.NoError(t, err)

		t.Cleanup(func() { _ = kvStore.Close() })

		return kvStore
	}

	connectPostgresKV := func(t *testing.T) kvdb.Backend {
		return connectPostgres(t, postgresKVDSN, postgresKVPfx)
	}

	connectBBolt := func(t *testing.T, dbPath string) kvdb.Backend {
		cfg := &kvdb.BoltBackendConfig{
			DBPath:            dbPath,
			DBFileName:        fileName,
			NoFreelistSync:    true,
			AutoCompact:       false,
			AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
			DBTimeout:         kvdb.DefaultDBTimeout,
			ReadOnly:          true,
		}

		kvStore, err := kvdb.GetBoltBackend(cfg)
		require.NoError(t, err)

		t.Cleanup(func() { _ = kvStore.Close() })

		return kvStore
	}

	connectSQLite := func(t *testing.T, dbPath string) kvdb.Backend {
		const (
			timeout  = 10 * time.Second
			maxConns = 5
		)
		sqlbase.Init(maxConns)

		cfg := &sqlite.Config{
			Timeout:        timeout,
			BusyTimeout:    timeout,
			MaxConnections: maxConns,
		}

		kvStore, err := kvdb.Open(
			kvdb.SqliteBackendName, ctx, cfg,
			dbPath, fileName,
			// NOTE: we use the raw string here else we get an
			// import cycle if we try to import lncfg.NSChannelDB.
			"channeldb",
		)
		require.NoError(t, err)

		t.Cleanup(func() { _ = kvStore.Close() })

		return kvStore
	}

	tests := []struct {
		name   string
		dbPath string
	}{
		{
			name:   "testdata",
			dbPath: "testdata",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if postgresKVDSN != "" {
				migrate(t, connectPostgresKV(t))
				return
			}

			chanDBPath := path.Join(test.dbPath, fileName)
			t.Logf("Connecting to channel DB at: %s", chanDBPath)

			connectDB := connectBBolt
			if isSqlite {
				connectDB = connectSQLite
			}

			migrate(t, connectDB(t, test.dbPath))
		})
	}
}

// kvPaymentRef holds a KV payment and its hash for batched deep validation.
type kvPaymentRef struct {
	hash    lntypes.Hash
	payment *MPPayment
}

// deepValidateAllPayments iterates all KV payments in batches and performs a
// deep comparison against their SQL counterparts. For each batch, it fetches
// the full SQL payment data (including HTLCs, routes, custom records) via
// batch queries and compares field-by-field with the KV data.
func deepValidateAllPayments(t *testing.T, ctx context.Context,
	kvBackend kvdb.Backend, sqlStore *SQLStore) {

	t.Helper()

	batchSize := int(sqlStore.cfg.QueryCfg.MaxBatchSize)

	// Get total payment count from SQL so we can show progress.
	var totalPayments int64
	err := sqlStore.db.ExecTx(
		ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
			var err error
			totalPayments, err = db.CountPayments(ctx)
			return err
		}, sqldb.NoOpReset,
	)
	require.NoError(t, err)

	var totalValidated int

	// Iterate all KV payments, collecting them into batches. For each
	// full batch, perform a batched deep comparison against SQL.
	var batch []kvPaymentRef
	err = kvdb.View(kvBackend, func(kvTx kvdb.RTx) error {
		paymentsBucket := kvTx.ReadBucket(paymentsRootBucket)
		if paymentsBucket == nil {
			return nil
		}

		return paymentsBucket.ForEach(func(k, v []byte) error {
			bucket := paymentsBucket.NestedReadBucket(k)
			if bucket == nil {
				return nil
			}

			payment, err := fetchPayment(bucket)
			if err != nil {
				return err
			}

			var hash lntypes.Hash
			copy(hash[:], k)

			batch = append(batch, kvPaymentRef{
				hash:    hash,
				payment: payment,
			})

			if len(batch) >= batchSize {
				deepCompareBatch(
					t, ctx, sqlStore, batch,
				)
				totalValidated += len(batch)
				t.Logf("Deep validated %d/%d payments",
					totalValidated, totalPayments)

				batch = batch[:0]
			}

			return nil
		})
	}, func() {
		batch = nil
		totalValidated = 0
	})
	require.NoError(t, err)

	// Validate any remaining payments in the last batch.
	if len(batch) > 0 {
		deepCompareBatch(t, ctx, sqlStore, batch)
		totalValidated += len(batch)
	}

	t.Logf("Deep validated %d/%d payments (complete)",
		totalValidated, totalPayments)
}

// deepCompareBatch performs a batched deep comparison of KV payments against
// their SQL counterparts. It fetches SQL payment data in a single read
// transaction using batch queries.
func deepCompareBatch(t *testing.T, ctx context.Context,
	sqlStore *SQLStore, batch []kvPaymentRef) {

	t.Helper()

	err := sqlStore.db.ExecTx(
		ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
			// Look up each payment by hash to get the SQL row
			// with payment ID.
			ids := make([]int64, 0, len(batch))
			rowsByHash := make(
				map[lntypes.Hash]sqlc.FetchPaymentsByIDsRow,
				len(batch),
			)
			for _, ref := range batch {
				row, err := fetchPaymentByHash(
					ctx, db, ref.hash,
				)
				if err != nil {
					return fmt.Errorf("fetch SQL payment "+
						"%x: %w", ref.hash[:8], err)
				}
				ids = append(ids, row.Payment.ID)
			}

			// Batch-fetch full payment details.
			rows, err := db.FetchPaymentsByIDs(ctx, ids)
			if err != nil {
				return fmt.Errorf("batch fetch payments: %w",
					err)
			}
			for _, row := range rows {
				var hash lntypes.Hash
				copy(hash[:], row.PaymentIdentifier)
				rowsByHash[hash] = row
			}

			batchData, err := batchLoadPaymentDetailsData(
				ctx, sqlStore.cfg.QueryCfg, db, ids,
			)
			if err != nil {
				return fmt.Errorf("batch load details: %w",
					err)
			}

			// Deep compare each payment.
			for _, ref := range batch {
				row, ok := rowsByHash[ref.hash]
				require.True(t, ok,
					"SQL payment %x not in batch "+
						"results", ref.hash[:8])

				sqlPayment, err := buildPaymentFromBatchData(
					row, batchData,
				)
				require.NoError(t, err,
					"build SQL payment %x",
					ref.hash[:8])

				normalizePaymentData(ref.payment)
				normalizePaymentData(sqlPayment)

				require.Equal(
					t, ref.payment, sqlPayment,
					"payment mismatch %x",
					ref.hash[:8],
				)
			}

			return nil
		}, sqldb.NoOpReset,
	)
	require.NoError(t, err)
}
