// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/spf13/cobra"
	"gotest.tools/assert"

	"knative.dev/client/lib/test"
	"knative.dev/client/pkg/kn/config"
	"knative.dev/client/pkg/kn/root"
	"knative.dev/client/pkg/util"
)

func TestValidatePlugin(t *testing.T) {

	// Build up simple command hierarchy
	root := cobra.Command{}
	one := &cobra.Command{Use: "one"}
	one.AddCommand(&cobra.Command{Use: "eins"}, &cobra.Command{Use: "zwei"})
	two := &cobra.Command{Use: "two"}
	root.AddCommand(one, two)

	data := []struct {
		givenPluginCommandParts []string
		expectedErrors          []string
	}{
		{
			// Allowed because it add a new top-level plugin
			[]string{"test"},
			nil,
		},
		{
			// Allowed because it adds to an existing command-group
			[]string{"one", "drei"},
			nil,
		},
		{
			// Forbidden because it overrides an command-group
			[]string{"one"},
			[]string{"pluginPath", "one"},
		},
		{
			// Forbidden because it overrides a leaf-command
			[]string{"one", "zwei"},
			[]string{"pluginPath", "one", "zwei"},
		},
		{
			// Forbidden because it would mis-use a leaf-comman to a command-group
			[]string{"one", "zwei", "trois"},
			[]string{"pluginPath", "one", "zwei"},
		},
		{
			// Forbidden because it overrides a (top-level) leaf-command
			[]string{"two"},
			[]string{"pluginPath", "two"},
		},
		{
			// Forbidden because it would add to a leaf command
			[]string{"two", "deux", "and", "more"},
			[]string{"pluginPath", "two", "deux"},
		},
	}

	for i, d := range data {
		step := fmt.Sprintf("Check %d", i)
		err := validatePlugin(&root, commandPartsOnlyPlugin(d.givenPluginCommandParts))
		if len(d.expectedErrors) == 0 {
			assert.NilError(t, err, step)
		} else {
			assert.Assert(t, err != nil, step)
			assert.Assert(t, util.ContainsAll(err.Error(), d.expectedErrors...), step)
		}
	}

}

// Used above for wrapping the command part to check
type commandPartsOnlyPlugin []string

func (f commandPartsOnlyPlugin) CommandParts() []string       { return f }
func (f commandPartsOnlyPlugin) Name() string                 { return "" }
func (f commandPartsOnlyPlugin) Execute(args []string) error  { return nil }
func (f commandPartsOnlyPlugin) Description() (string, error) { return "", nil }
func (f commandPartsOnlyPlugin) Path() string                 { return "pluginPath" }

func TestArgsWithoutCommands(t *testing.T) {
	data := []struct {
		givenCmdArgs            []string
		givenPluginCommandParts []string
		expectedResult          []string
	}{
		{
			[]string{"--option", "val", "one", "second", "rest"},
			[]string{"one", "second"},
			[]string{"--option", "val", "rest"},
		},
		{
			[]string{"--option", "val", "one", "second", "rest"},
			[]string{"second", "one"},
			[]string{"--option", "val", "one", "rest"},
		},
		{
			[]string{"--option", "val", "one", "second", "third", "one", "rest"},
			[]string{"second", "one"},
			[]string{"--option", "val", "one", "third", "rest"},
		},
	}
	for _, d := range data {
		result := argsWithoutCommands(d.givenCmdArgs, d.givenPluginCommandParts)
		assert.DeepEqual(t, result, d.expectedResult)
	}
}

func TestUnknownCommands(t *testing.T) {
	oldArgs := os.Args
	defer (func() {
		os.Args = oldArgs
	})()

	data := []struct {
		givenCmdArgs  []string
		commandPath   []string
		expectedError []string
	}{
		{
			[]string{"service", "unknown", "test", "--scale-min=0"},
			[]string{"service"},
			[]string{"unknown sub-command", "unknown"},
		},
		{
			[]string{"service", "--foo=bar"},
			[]string{"service"},
			[]string{},
		},
		{
			[]string{"source", "ping", "blub", "--foo=bar"},
			[]string{"source", "ping"},
			[]string{"unknown sub-command", "blub"},
		},
	}
	for _, d := range data {
		args := append([]string{"kn"}, d.givenCmdArgs...)
		rootCmd, err := root.NewRootCommand(nil)
		rootCmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}
		os.Args = args
		assert.NilError(t, err)
		err = validateRootCommand(rootCmd)
		if len(d.expectedError) == 0 {
			assert.NilError(t, err)
			continue
		}
		assert.Assert(t, err != nil)
		assert.Assert(t, util.ContainsAll(err.Error(), d.expectedError...))
		cmd, _, e := rootCmd.Find(d.commandPath)
		assert.NilError(t, e)
		for _, sub := range cmd.Commands() {
			assert.ErrorContains(t, err, sub.Name())
		}
	}

}

func TestStripFlags(t *testing.T) {

	data := []struct {
		givenArgs        []string
		expectedCommands []string
		expectedError    string
	}{
		{
			[]string{"test", "-h", "second", "--bla"},
			[]string{"test", "second"},
			"",
		},
		{
			[]string{"--help", "test"},
			[]string{"test"},
			"",
		},
		{
			[]string{"--unknown-option", "bla", "test", "second"},
			[]string{"test", "second"},
			"",
		},
		{
			[]string{"--lookup-plugins", "bla", "test", "second"},
			[]string{"bla", "test", "second"},
			"",
		},
		{
			[]string{"--config-file", "bla", "test", "second"},
			[]string{"test", "second"},
			"",
		},
		{
			[]string{"test"},
			[]string{"test"},
			"",
		},
	}

	for i, f := range data {
		step := fmt.Sprintf("Check %d", i)
		cmd := &cobra.Command{FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}}
		config.AddBootstrapFlags(cmd.Flags())
		commands, err := stripFlags(cmd, f.givenArgs)
		assert.DeepEqual(t, commands, f.expectedCommands)
		if f.expectedError != "" {
			assert.ErrorContains(t, err, f.expectedError, step)
		} else {
			assert.NilError(t, err, step)
		}
	}

	t.Log("checking error case for stripFlags")
	cmd, err := root.NewRootCommand(nil)
	assert.NilError(t, err)
	_, err = stripFlags(cmd, []string{"--config"})
	assert.ErrorContains(t, err, "needs an argument")
}

func TestRunWithError(t *testing.T) {
	data := []struct {
		given    string
		expected string
	}{
		{
			"unknown sub-command blub",
			"Error: unknown sub-command blub",
		},
		{
			"error: unknown type blub",
			"Error: unknown type blub",
		},
	}
	for _, d := range data {
		capture := test.CaptureOutput(t)
		printError(errors.New(d.given))
		stdOut, errOut := capture.Close()

		assert.Equal(t, stdOut, "")
		assert.Assert(t, strings.Contains(errOut, d.expected))
		assert.Assert(t, util.ContainsAll(errOut, "Run", "--help", "usage"))
	}
}

func TestRunWithPluginError(t *testing.T) {
	data := []struct {
		given    string
		expected string
	}{
		{
			"exit status 1",
			"Error: exit status 1",
		},
	}
	for _, d := range data {
		capture := test.CaptureOutput(t)
		// displayHelp argument is false for plugin error
		printError(&runError{errors.New(d.given)})
		stdOut, errOut := capture.Close()

		assert.Equal(t, stdOut, "")
		assert.Assert(t, strings.Contains(errOut, d.expected))
		// check that --help message isn't displayed
		assert.Assert(t, util.ContainsNone(errOut, "Run", "--help", "usage"))
	}
}

// Smoke test
func TestRun(t *testing.T) {
	oldArgs := os.Args
	os.Args = []string{"kn", "--config", "/no/config/please.yaml", "version"}
	defer (func() {
		os.Args = oldArgs
	})()

	capture := test.CaptureOutput(t)
	err := run(os.Args[1:])
	out, _ := capture.Close()

	assert.NilError(t, err)
	assert.Assert(t, util.ContainsAllIgnoreCase(out, "version", "build", "git"))
}
