package manager

import (
	"path/filepath"
	"strings"
	"testing"

	"github.com/containerd/errdefs"
	"github.com/docker/cli/cli/config"
	"github.com/docker/cli/cli/config/configfile"
	"github.com/docker/cli/internal/test"
	"github.com/spf13/cobra"
	"gotest.tools/v3/assert"
	"gotest.tools/v3/fs"
)

func TestListPluginCandidates(t *testing.T) {
	// Populate a selection of directories with various shadowed and bogus/obscure plugin candidates.
	// For the purposes of this test no contents is required and permissions are irrelevant.
	dir := fs.NewDir(t, t.Name(),
		fs.WithDir(
			"plugins1",
			fs.WithFile("docker-plugin1", ""),                        // This appears in each directory
			fs.WithFile("not-a-plugin", ""),                          // Should be ignored
			fs.WithFile("docker-symlinked1", ""),                     // This and ...
			fs.WithSymlink("docker-symlinked2", "docker-symlinked1"), // ... this should both appear
			fs.WithDir("ignored1"),                                   // A directory should be ignored
		),
		fs.WithDir(
			"plugins2",
			fs.WithFile("docker-plugin1", ""),
			fs.WithFile("also-not-a-plugin", ""),
			fs.WithFile("docker-hardlink1", ""),                     // This and ...
			fs.WithHardlink("docker-hardlink2", "docker-hardlink1"), // ... this should both appear
			fs.WithDir("ignored2"),
		),
		fs.WithDir(
			"plugins3-target", // Will be referenced as a symlink from below
			fs.WithFile("docker-plugin1", ""),
			fs.WithDir("ignored3"),
			fs.WithSymlink("docker-brokensymlink", "broken"),           // A broken symlink is ignored
			fs.WithFile("non-plugin-symlinked", ""),                    // This shouldn't appear, but ...
			fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
		),
		fs.WithSymlink("plugins3", "plugins3-target"),
		fs.WithFile("/plugins4", ""),
		fs.WithSymlink("plugins5", "plugins5-nonexistent-target"),
	)
	defer dir.Remove()

	dirs := make([]string, 0, 6)
	for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
		dirs = append(dirs, dir.Join(d))
	}

	candidates := listPluginCandidates(dirs)
	exp := map[string][]string{
		"plugin1": {
			dir.Join("plugins1", "docker-plugin1"),
			dir.Join("plugins2", "docker-plugin1"),
			dir.Join("plugins3", "docker-plugin1"),
		},
		"symlinked1": {
			dir.Join("plugins1", "docker-symlinked1"),
		},
		"symlinked2": {
			dir.Join("plugins1", "docker-symlinked2"),
		},
		"hardlink1": {
			dir.Join("plugins2", "docker-hardlink1"),
		},
		"hardlink2": {
			dir.Join("plugins2", "docker-hardlink2"),
		},
		"symlinked": {
			dir.Join("plugins3", "docker-symlinked"),
		},
	}

	assert.DeepEqual(t, candidates, exp)
}

func TestListPluginCandidatesEmpty(t *testing.T) {
	tmpDir := t.TempDir()
	candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
	assert.Assert(t, len(candidates) == 0)
}

// Regression test for https://github.com/docker/cli/issues/5643.
// Check that inaccessible directories that come before accessible ones are ignored
// and do not prevent the latter from being processed.
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
	dir := fs.NewDir(t, t.Name(),
		fs.WithDir("no-perm", fs.WithMode(0)),
		fs.WithDir("plugins",
			fs.WithFile("docker-buildx", ""),
		),
	)
	defer dir.Remove()

	candidates := listPluginCandidates([]string{
		dir.Join("no-perm"),
		dir.Join("plugins"),
	})
	assert.DeepEqual(t, candidates, map[string][]string{
		"buildx": {
			dir.Join("plugins", "docker-buildx"),
		},
	})
}

func TestGetPlugin(t *testing.T) {
	dir := fs.NewDir(t, t.Name(),
		fs.WithFile("docker-bbb", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
		fs.WithFile("docker-aaa", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
	)
	defer dir.Remove()

	cli := test.NewFakeCli(nil)
	cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})

	plugin, err := GetPlugin("bbb", cli, &cobra.Command{})
	assert.NilError(t, err)
	assert.Equal(t, plugin.Name, "bbb")

	_, err = GetPlugin("ccc", cli, &cobra.Command{})
	assert.Error(t, err, "Error: No such CLI plugin: ccc")
	assert.Assert(t, errdefs.IsNotFound(err))
}

func TestListPluginsIsSorted(t *testing.T) {
	dir := fs.NewDir(t, t.Name(),
		fs.WithFile("docker-bbb", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
		fs.WithFile("docker-aaa", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
	)
	defer dir.Remove()

	cli := test.NewFakeCli(nil)
	cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})

	plugins, err := ListPlugins(cli, &cobra.Command{})
	assert.NilError(t, err)

	// We're only interested in the plugins we created for testing this, and only
	// if they appear in the expected order
	var names []string
	for _, p := range plugins {
		if p.Name == "aaa" || p.Name == "bbb" {
			names = append(names, p.Name)
		}
	}
	assert.DeepEqual(t, names, []string{"aaa", "bbb"})
}

func TestErrPluginNotFound(t *testing.T) {
	var err error = errPluginNotFound("test")
	err.(errPluginNotFound).NotFound()
	assert.Error(t, err, "Error: No such CLI plugin: test")
	assert.Assert(t, errdefs.IsNotFound(err))
	assert.Assert(t, !errdefs.IsNotFound(nil))
}

func TestGetPluginDirs(t *testing.T) {
	cli := test.NewFakeCli(nil)

	pluginDir := filepath.Join(config.Dir(), "cli-plugins")
	expected := append([]string{pluginDir}, defaultSystemPluginDirs...)

	pluginDirs := getPluginDirs(cli.ConfigFile())
	assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))

	extras := make([]string, 0, 3+len(expected))
	extras = append(extras, "foo", "bar", "baz")
	expected = append(extras, expected...)
	cli.SetConfigFile(&configfile.ConfigFile{
		CLIPluginsExtraDirs: extras,
	})
	pluginDirs = getPluginDirs(cli.ConfigFile())
	assert.DeepEqual(t, expected, pluginDirs)
}
