package migration_test

import (
	"errors"
	"fmt"
	"reflect"
	"testing"

	"github.com/btcsuite/btcwallet/walletdb"
	"github.com/btcsuite/btcwallet/walletdb/migration"
	"github.com/davecgh/go-spew/spew"
)

type mockMigrationManager struct {
	currentVersion uint32
	versions       []migration.Version
}

var _ migration.Manager = (*mockMigrationManager)(nil)

func (m *mockMigrationManager) Name() string {
	return "mock"
}

func (m *mockMigrationManager) Namespace() walletdb.ReadWriteBucket {
	return nil
}

func (m *mockMigrationManager) CurrentVersion(_ walletdb.ReadBucket) (uint32, error) {
	return m.currentVersion, nil
}

func (m *mockMigrationManager) SetVersion(_ walletdb.ReadWriteBucket, version uint32) error {
	m.currentVersion = version
	return nil
}

func (m *mockMigrationManager) Versions() []migration.Version {
	return m.versions
}

// TestGetLatestVersion ensures that we can properly retrieve the latest version
// from a slice of versions.
func TestGetLatestVersion(t *testing.T) {
	t.Parallel()

	tests := []struct {
		versions      []migration.Version
		latestVersion uint32
	}{
		{
			versions:      []migration.Version{},
			latestVersion: 0,
		},
		{
			versions: []migration.Version{
				{
					Number:    1,
					Migration: nil,
				},
			},
			latestVersion: 1,
		},
		{
			versions: []migration.Version{
				{
					Number:    1,
					Migration: nil,
				},
				{
					Number:    2,
					Migration: nil,
				},
			},
			latestVersion: 2,
		},
		{
			versions: []migration.Version{
				{
					Number:    2,
					Migration: nil,
				},
				{
					Number:    0,
					Migration: nil,
				},
				{
					Number:    1,
					Migration: nil,
				},
			},
			latestVersion: 2,
		},
	}

	for i, test := range tests {
		latestVersion := migration.GetLatestVersion(test.versions)
		if latestVersion != test.latestVersion {
			t.Fatalf("test %d: expected latest version %d, got %d",
				i, test.latestVersion, latestVersion)
		}
	}
}

// TestVersionsToApply ensures that the proper versions that needs to be applied
// are returned given the current version.
func TestVersionsToApply(t *testing.T) {
	t.Parallel()

	tests := []struct {
		currentVersion  uint32
		versions        []migration.Version
		versionsToApply []migration.Version
	}{
		{
			currentVersion: 0,
			versions: []migration.Version{
				{
					Number:    0,
					Migration: nil,
				},
			},
			versionsToApply: nil,
		},
		{
			currentVersion: 1,
			versions: []migration.Version{
				{
					Number:    0,
					Migration: nil,
				},
			},
			versionsToApply: nil,
		},
		{
			currentVersion: 0,
			versions: []migration.Version{
				{
					Number:    0,
					Migration: nil,
				},
				{
					Number:    1,
					Migration: nil,
				},
				{
					Number:    2,
					Migration: nil,
				},
			},
			versionsToApply: []migration.Version{
				{
					Number:    1,
					Migration: nil,
				},
				{
					Number:    2,
					Migration: nil,
				},
			},
		},
		{
			currentVersion: 0,
			versions: []migration.Version{
				{
					Number:    2,
					Migration: nil,
				},
				{
					Number:    0,
					Migration: nil,
				},
				{
					Number:    1,
					Migration: nil,
				},
			},
			versionsToApply: []migration.Version{
				{
					Number:    1,
					Migration: nil,
				},
				{
					Number:    2,
					Migration: nil,
				},
			},
		},
	}

	for i, test := range tests {
		versionsToApply := migration.VersionsToApply(
			test.currentVersion, test.versions,
		)

		if !reflect.DeepEqual(versionsToApply, test.versionsToApply) {
			t.Fatalf("test %d: versions to apply mismatch\n"+
				"expected: %v\ngot: %v", i,
				spew.Sdump(test.versionsToApply),
				spew.Sdump(versionsToApply))
		}
	}
}

// TestUpgradeRevert ensures that we are not able to revert to a previous
// version.
func TestUpgradeRevert(t *testing.T) {
	t.Parallel()

	m := &mockMigrationManager{
		currentVersion: 1,
		versions: []migration.Version{
			{
				Number:    0,
				Migration: nil,
			},
		},
	}

	if err := migration.Upgrade(m); err != migration.ErrReversion {
		t.Fatalf("expected Upgrade to fail with ErrReversion, got %v",
			err)
	}
}

// TestUpgradeSameVersion ensures that no upgrades happen if the current version
// matches the latest.
func TestUpgradeSameVersion(t *testing.T) {
	t.Parallel()

	m := &mockMigrationManager{
		currentVersion: 1,
		versions: []migration.Version{
			{
				Number:    0,
				Migration: nil,
			},
			{
				Number: 1,
				Migration: func(walletdb.ReadWriteBucket) error {
					return errors.New("migration should " +
						"not happen due to already " +
						"being on the latest version")
				},
			},
		},
	}

	if err := migration.Upgrade(m); err != nil {
		t.Fatalf("unable to upgrade: %v", err)
	}
}

// TestUpgradeNewVersion ensures that we can properly upgrade to a newer version
// if available.
func TestUpgradeNewVersion(t *testing.T) {
	t.Parallel()

	versions := []migration.Version{
		{
			Number:    0,
			Migration: nil,
		},
		{
			Number: 1,
			Migration: func(walletdb.ReadWriteBucket) error {
				return nil
			},
		},
	}

	m := &mockMigrationManager{
		currentVersion: 0,
		versions:       versions,
	}

	if err := migration.Upgrade(m); err != nil {
		t.Fatalf("unable to upgrade: %v", err)
	}

	latestVersion := migration.GetLatestVersion(versions)
	if m.currentVersion != latestVersion {
		t.Fatalf("expected current version to match latest: "+
			"current=%d vs latest=%d", m.currentVersion,
			latestVersion)
	}
}

// TestUpgradeMultipleVersions ensures that we can go through multiple upgrades
// in-order to reach the latest version.
func TestUpgradeMultipleVersions(t *testing.T) {
	t.Parallel()

	previousVersion := uint32(0)
	versions := []migration.Version{
		{
			Number:    previousVersion,
			Migration: nil,
		},
		{
			Number: 1,
			Migration: func(walletdb.ReadWriteBucket) error {
				if previousVersion != 0 {
					return fmt.Errorf("expected previous "+
						"version to be %d, got %d", 0,
						previousVersion)
				}

				previousVersion = 1
				return nil
			},
		},
		{
			Number: 2,
			Migration: func(walletdb.ReadWriteBucket) error {
				if previousVersion != 1 {
					return fmt.Errorf("expected previous "+
						"version to be %d, got %d", 1,
						previousVersion)
				}

				previousVersion = 2
				return nil
			},
		},
	}

	m := &mockMigrationManager{
		currentVersion: 0,
		versions:       versions,
	}

	if err := migration.Upgrade(m); err != nil {
		t.Fatalf("unable to upgrade: %v", err)
	}

	latestVersion := migration.GetLatestVersion(versions)
	if m.currentVersion != latestVersion {
		t.Fatalf("expected current version to match latest: "+
			"current=%d vs latest=%d", m.currentVersion,
			latestVersion)
	}
}