package cliplugins

import (
	"testing"

	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/golden"
	"gotest.tools/v3/icmd"
)

const shortHFlagDeprecated = "Flag shorthand -h has been deprecated, use --help\n"

// TestRunNonexisting ensures correct behaviour when running a nonexistent plugin.
func TestRunNonexisting(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("nonexistent"))
	res.Assert(t, icmd.Expected{
		ExitCode: 1,
		Out:      icmd.None,
	})
	golden.Assert(t, res.Stderr(), "docker-nonexistent-err.golden")
}

// TestHelpNonexisting ensures correct behaviour when invoking help on a nonexistent plugin.
func TestHelpNonexisting(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("help", "nonexistent"))
	res.Assert(t, icmd.Expected{
		ExitCode: 1,
		Out:      icmd.None,
	})
	golden.Assert(t, res.Stderr(), "docker-help-nonexistent-err.golden")
}

// TestNonexistingHelp ensures correct behaviour when invoking a
// nonexistent plugin with `--help`.
func TestNonexistingHelp(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("nonexistent", "--help"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		// This should actually be the whole docker help
		// output, so spot check instead having of a golden
		// with everything in, which will change too frequently.
		Out: "Usage:  docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers",
		Err: icmd.None,
	})
	// Short -h should be the same, modulo the deprecation message
	exp := shortHFlagDeprecated + res.Stdout()
	res = icmd.RunCmd(run("nonexistent", "-h"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		// This should be identical to the --help case above
		Out: exp,
		Err: icmd.None,
	})
}

// TestRunBad ensures correct behaviour when running an existent but invalid plugin
func TestRunBad(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("badmeta"))
	res.Assert(t, icmd.Expected{
		ExitCode: 1,
		Out:      icmd.None,
	})
	golden.Assert(t, res.Stderr(), "docker-badmeta-err.golden")
}

// TestHelpBad ensures correct behaviour when invoking help on a existent but invalid plugin.
func TestHelpBad(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("help", "badmeta"))
	res.Assert(t, icmd.Expected{
		ExitCode: 1,
		Out:      icmd.None,
	})
	golden.Assert(t, res.Stderr(), "docker-help-badmeta-err.golden")
}

// TestBadHelp ensures correct behaviour when invoking an
// existent but invalid plugin with `--help`.
func TestBadHelp(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("badmeta", "--help"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		// This should be literally the whole docker help
		// output, so spot check instead of a golden with
		// everything in which will change all the time.
		Out: "Usage:  docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers",
		Err: icmd.None,
	})
	// Short -h should be the same, modulo the deprecation message
	usage := res.Stdout()
	res = icmd.RunCmd(run("badmeta", "-h"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		// This should be identical to the --help case above
		Out: shortHFlagDeprecated + usage,
		Err: icmd.None,
	})
}

// TestRunGood ensures correct behaviour when running a valid plugin
func TestRunGood(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("helloworld"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		Out:      "Hello World!",
		Err:      icmd.None,
	})
}

// TestHelpGood ensures correct behaviour when invoking help on a
// valid plugin. A global argument is included to ensure it does not
// interfere.
func TestHelpGood(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("-l", "info", "help", "helloworld"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		Err:      icmd.None,
	})
	golden.Assert(t, res.Stdout(), "docker-help-helloworld.golden")
}

// TestGoodHelp ensures correct behaviour when calling a valid plugin
// with `--help`. A global argument is used to ensure it does not
// interfere.
func TestGoodHelp(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("-l", "info", "helloworld", "--help"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		Err:      icmd.None,
	})
	// This is the same golden file as `TestHelpGood`, above.
	golden.Assert(t, res.Stdout(), "docker-help-helloworld.golden")
	// Short -h should be the same, modulo the deprecation message
	exp := shortHFlagDeprecated + res.Stdout()
	res = icmd.RunCmd(run("-l", "info", "helloworld", "-h"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		// This should be identical to the --help case above
		Out: exp,
		Err: icmd.None,
	})
}

// TestRunGoodSubcommand ensures correct behaviour when running a valid plugin with a subcommand
func TestRunGoodSubcommand(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("helloworld", "goodbye"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		Out:      "Goodbye World!",
		Err:      icmd.None,
	})
}

// TestHelpGoodSubcommand ensures correct behaviour when invoking help on a
// valid plugin subcommand. A global argument is included to ensure it does not
// interfere.
func TestHelpGoodSubcommand(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("-l", "info", "help", "helloworld", "goodbye"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		Err:      icmd.None,
	})
	golden.Assert(t, res.Stdout(), "docker-help-helloworld-goodbye.golden")
}

// TestGoodSubcommandHelp ensures correct behaviour when calling a valid plugin
// with a subcommand and `--help`. A global argument is used to ensure it does not
// interfere.
func TestGoodSubcommandHelp(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	res := icmd.RunCmd(run("-l", "info", "helloworld", "goodbye", "--help"))
	res.Assert(t, icmd.Expected{
		ExitCode: 0,
		Err:      icmd.None,
	})
	// This is the same golden file as `TestHelpGoodSubcommand`, above.
	golden.Assert(t, res.Stdout(), "docker-help-helloworld-goodbye.golden")
}

// TestCliInitialized tests the code paths which ensure that the Cli
// object is initialized whether the plugin uses PersistentRunE or not
func TestCliInitialized(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()

	var apiversion string
	t.Run("withhook", func(t *testing.T) {
		res := icmd.RunCmd(run("helloworld", "--pre-run", "apiversion"))
		res.Assert(t, icmd.Success)
		assert.Assert(t, res.Stdout() != "")
		apiversion = res.Stdout()
		assert.Assert(t, is.Equal(res.Stderr(), "Plugin PersistentPreRunE called"))
	})
	t.Run("withouthook", func(t *testing.T) {
		res := icmd.RunCmd(run("nopersistentprerun"))
		res.Assert(t, icmd.Success)
		assert.Assert(t, is.Equal(res.Stdout(), apiversion))
	})
}

// TestPluginErrorCode tests when the plugin return with a given exit status.
// We want to verify that the exit status does not get output to stdout and also that we return the exit code.
func TestPluginErrorCode(t *testing.T) {
	run, _, cleanup := prepare(t)
	defer cleanup()
	res := icmd.RunCmd(run("helloworld", "exitstatus2"))
	res.Assert(t, icmd.Expected{
		ExitCode: 2,
		Out:      icmd.None,
		Err:      "Exiting with error status 2",
	})
}
