package migtest

import (
	"fmt"
	"os"
	"testing"

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

// MakeDB creates a new instance of the ChannelDB for testing purposes.
func MakeDB(t testing.TB) (kvdb.Backend, error) {
	// Create temporary database for mission control.
	file, err := os.CreateTemp("", "*.db")
	if err != nil {
		return nil, err
	}

	dbPath := file.Name()
	t.Cleanup(func() {
		require.NoError(t, file.Close())
		require.NoError(t, os.Remove(dbPath))
	})

	db, err := kvdb.Open(
		kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
	)
	if err != nil {
		return nil, err
	}
	t.Cleanup(func() {
		require.NoError(t, db.Close())
	})

	return db, nil
}

// ApplyMigration is a helper test function that encapsulates the general steps
// which are needed to properly check the result of applying migration function.
func ApplyMigration(t *testing.T,
	beforeMigration, afterMigration, migrationFunc func(tx kvdb.RwTx) error,
	shouldFail bool) {

	t.Helper()

	cdb, err := MakeDB(t)
	if err != nil {
		t.Fatal(err)
	}

	// beforeMigration usually used for populating the database
	// with test data.
	err = kvdb.Update(cdb, beforeMigration, func() {})
	if err != nil {
		t.Fatal(err)
	}

	defer func() {
		t.Helper()

		if r := recover(); r != nil {
			err = newError(r)
		}

		if err == nil && shouldFail {
			t.Fatal("error wasn't received on migration stage")
		} else if err != nil && !shouldFail {
			t.Fatalf("error was received on migration stage: %v", err)
		}

		// afterMigration usually used for checking the database state and
		// throwing the error if something went wrong.
		err = kvdb.Update(cdb, afterMigration, func() {})
		if err != nil {
			t.Fatal(err)
		}
	}()

	// Apply migration.
	err = kvdb.Update(cdb, migrationFunc, func() {})
	if err != nil {
		t.Logf("migration error: %v", err)
	}
}

// ApplyMigrationWithDB is a helper test function that encapsulates the general
// steps which are needed to properly check the result of applying migration
// function. This function differs from ApplyMigration as it requires the
// supplied migration functions to take a db instance and construct their own
// database transactions.
func ApplyMigrationWithDB(t testing.TB, beforeMigration,
	afterMigration func(db kvdb.Backend) error,
	migrationFunc func(db kvdb.Backend) error, shouldFail bool) {

	t.Helper()

	cdb, err := MakeDB(t)
	if err != nil {
		t.Fatal(err)
	}

	// beforeMigration usually used for populating the database
	// with test data.
	if err := beforeMigration(cdb); err != nil {
		t.Fatalf("beforeMigration error: %v", err)
	}

	// Apply migration.
	err = migrationFunc(cdb)
	if shouldFail {
		require.Error(t, err)
	} else {
		require.NoError(t, err)
	}

	// If there's no afterMigration, exit here.
	if afterMigration == nil {
		return
	}

	// afterMigration usually used for checking the database state
	// and throwing the error if something went wrong.
	if err := afterMigration(cdb); err != nil {
		t.Fatalf("afterMigration error: %v", err)
	}
}

func newError(e interface{}) error {
	var err error
	switch e := e.(type) {
	case error:
		err = e
	default:
		err = fmt.Errorf("%v", e)
	}

	return err
}
