// +build linux

package cosmovisor

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"testing"

	copy2 "github.com/otiai10/copy"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCurrentBin(t *testing.T) {
	home, err := copyTestData("validate")
	require.NoError(t, err)
	defer os.RemoveAll(home)

	cfg := Config{Home: home, Name: "dummyd"}

	currentBin, err := cfg.CurrentBin()
	require.NoError(t, err)

	assert.Equal(t, cfg.GenesisBin(), currentBin)

	// ensure we cannot set this to an invalid value
	for _, name := range []string{"missing", "nobin", "noexec"} {
		err = cfg.SetCurrentUpgrade(name)
		require.Error(t, err, name)

		currentBin, err := cfg.CurrentBin()
		require.NoError(t, err)

		assert.Equal(t, cfg.GenesisBin(), currentBin, name)
	}

	// try a few times to make sure this can be reproduced
	for _, upgrade := range []string{"chain2", "chain3", "chain2"} {
		// now set it to a valid upgrade and make sure CurrentBin is now set properly
		err = cfg.SetCurrentUpgrade(upgrade)
		require.NoError(t, err)
		// we should see current point to the new upgrade dir
		currentBin, err := cfg.CurrentBin()
		require.NoError(t, err)

		assert.Equal(t, cfg.UpgradeBin(upgrade), currentBin)
	}
}

func TestCurrentAlwaysSymlinkToDirectory(t *testing.T) {
	home, err := copyTestData("validate")
	require.NoError(t, err)
	defer os.RemoveAll(home)

	cfg := Config{Home: home, Name: "dummyd"}

	currentBin, err := cfg.CurrentBin()
	require.NoError(t, err)
	assert.Equal(t, cfg.GenesisBin(), currentBin)
	assertCurrentLink(t, cfg, "genesis")

	err = cfg.SetCurrentUpgrade("chain2")
	require.NoError(t, err)
	currentBin, err = cfg.CurrentBin()
	require.NoError(t, err)
	assert.Equal(t, cfg.UpgradeBin("chain2"), currentBin)
	assertCurrentLink(t, cfg, filepath.Join("upgrades", "chain2"))
}

func assertCurrentLink(t *testing.T, cfg Config, target string) {
	link := filepath.Join(cfg.Root(), currentLink)
	// ensure this is a symlink
	info, err := os.Lstat(link)
	require.NoError(t, err)
	require.Equal(t, os.ModeSymlink, info.Mode()&os.ModeSymlink)

	dest, err := os.Readlink(link)
	require.NoError(t, err)
	expected := filepath.Join(cfg.Root(), target)
	require.Equal(t, expected, dest)
}

// TODO: test with download (and test all download functions)
func TestDoUpgradeNoDownloadUrl(t *testing.T) {
	home, err := copyTestData("validate")
	require.NoError(t, err)
	defer os.RemoveAll(home)

	cfg := &Config{Home: home, Name: "dummyd", AllowDownloadBinaries: true}

	currentBin, err := cfg.CurrentBin()
	require.NoError(t, err)

	assert.Equal(t, cfg.GenesisBin(), currentBin)

	// do upgrade ignores bad files
	for _, name := range []string{"missing", "nobin", "noexec"} {
		info := &UpgradeInfo{Name: name}
		err = DoUpgrade(cfg, info)
		require.Error(t, err, name)
		currentBin, err := cfg.CurrentBin()
		require.NoError(t, err)
		assert.Equal(t, cfg.GenesisBin(), currentBin, name)
	}

	// make sure it updates a few times
	for _, upgrade := range []string{"chain2", "chain3"} {
		// now set it to a valid upgrade and make sure CurrentBin is now set properly
		info := &UpgradeInfo{Name: upgrade}
		err = DoUpgrade(cfg, info)
		require.NoError(t, err)
		// we should see current point to the new upgrade dir
		upgradeBin := cfg.UpgradeBin(upgrade)
		currentBin, err := cfg.CurrentBin()
		require.NoError(t, err)

		assert.Equal(t, upgradeBin, currentBin)
	}
}

func TestOsArch(t *testing.T) {
	// all download tests will fail if we are not on linux...
	assert.Equal(t, "linux/amd64", osArch())
}

func TestGetDownloadURL(t *testing.T) {
	// all download tests will fail if we are not on linux...
	ref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/ref_zipped"))
	require.NoError(t, err)
	badref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/zip_binary/autod.zip"))
	require.NoError(t, err)

	cases := map[string]struct {
		info  string
		url   string
		isErr bool
	}{
		"missing": {
			isErr: true,
		},
		"follow reference": {
			info: ref,
			url:  "https://github.com/cosmos/cosmos-sdk/raw/aa5d6140ad4011bb33d472dca8246a0dcbe223ee/cosmovisor/testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae",
		},
		"malformated reference target": {
			info:  badref,
			isErr: true,
		},
		"missing link": {
			info:  "https://no.such.domain/exists.txt",
			isErr: true,
		},
		"proper binary": {
			info: `{"binaries": {"linux/amd64": "https://foo.bar/", "windows/amd64": "https://something.else"}}`,
			url:  "https://foo.bar/",
		},
		"any architecture not used": {
			info: `{"binaries": {"linux/amd64": "https://foo.bar/", "*": "https://something.else"}}`,
			url:  "https://foo.bar/",
		},
		"any architecture used": {
			info: `{"binaries": {"linux/arm": "https://foo.bar/arm-only", "any": "https://foo.bar/portable"}}`,
			url:  "https://foo.bar/portable",
		},
		"missing binary": {
			info:  `{"binaries": {"linux/arm": "https://foo.bar/"}}`,
			isErr: true,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			url, err := GetDownloadURL(&UpgradeInfo{Info: tc.info})
			if tc.isErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tc.url, url)
			}
		})
	}
}

func TestDownloadBinary(t *testing.T) {
	cases := map[string]struct {
		url         string
		canDownload bool
		validBinary bool
	}{
		"get raw binary": {
			url:         "./testdata/repo/raw_binary/autod",
			canDownload: true,
			validBinary: true,
		},
		"get raw binary with checksum": {
			// sha256sum ./testdata/repo/raw_binary/autod
			url:         "./testdata/repo/raw_binary/autod?checksum=sha256:e6bc7851600a2a9917f7bf88eb7bdee1ec162c671101485690b4deb089077b0d",
			canDownload: true,
			validBinary: true,
		},
		"get raw binary with invalid checksum": {
			url:         "./testdata/repo/raw_binary/autod?checksum=sha256:73e2bd6cbb99261733caf137015d5cc58e3f96248d8b01da68be8564989dd906",
			canDownload: false,
		},
		"get zipped directory": {
			url:         "./testdata/repo/zip_directory/autod.zip",
			canDownload: true,
			validBinary: true,
		},
		"get zipped directory with valid checksum": {
			// sha256sum ./testdata/repo/zip_directory/autod.zip
			url:         "./testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae",
			canDownload: true,
			validBinary: true,
		},
		"get zipped directory with invalid checksum": {
			url:         "./testdata/repo/zip_directory/autod.zip?checksum=sha256:73e2bd6cbb99261733caf137015d5cc58e3f96248d8b01da68be8564989dd906",
			canDownload: false,
		},
		"invalid url": {
			url:         "./testdata/repo/bad_dir/autod",
			canDownload: false,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			// make temp dir
			home, err := copyTestData("download")
			require.NoError(t, err)
			defer os.RemoveAll(home)

			cfg := &Config{
				Home:                  home,
				Name:                  "autod",
				AllowDownloadBinaries: true,
			}

			// if we have a relative path, make it absolute, but don't change eg. https://... urls
			url := tc.url
			if strings.HasPrefix(url, "./") {
				url, err = filepath.Abs(url)
				require.NoError(t, err)
			}

			upgrade := "amazonas"
			info := &UpgradeInfo{
				Name: upgrade,
				Info: fmt.Sprintf(`{"binaries":{"%s": "%s"}}`, osArch(), url),
			}

			err = DownloadBinary(cfg, info)
			if !tc.canDownload {
				assert.Error(t, err)
				return
			}
			require.NoError(t, err)

			err = EnsureBinary(cfg.UpgradeBin(upgrade))
			if tc.validBinary {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
			}
		})
	}
}

// copyTestData will make a tempdir and then
// "cp -r" a subdirectory under testdata there
// returns the directory (which can now be used as Config.Home) and modified safely
func copyTestData(subdir string) (string, error) {
	tmpdir, err := ioutil.TempDir("", "upgrade-manager-test")
	if err != nil {
		return "", fmt.Errorf("couldn't create temporary directory: %w", err)
	}

	src := filepath.Join("testdata", subdir)

	err = copy2.Copy(src, tmpdir)
	if err != nil {
		os.RemoveAll(tmpdir)
		return "", fmt.Errorf("couldn't copy files: %w", err)
	}

	return tmpdir, nil
}
