package cobra

import (
	"bytes"
	"regexp"
	"strings"
	"testing"
)

func TestGenZshCompletion(t *testing.T) {
	var debug bool
	var option string

	tcs := []struct {
		name                string
		root                *Command
		expectedExpressions []string
		invocationArgs      []string
		skip                string
	}{
		{
			name: "simple command",
			root: func() *Command {
				r := &Command{
					Use:  "mycommand",
					Long: "My Command long description",
					Run:  emptyRun,
				}
				r.Flags().BoolVar(&debug, "debug", debug, "description")
				return r
			}(),
			expectedExpressions: []string{
				`(?s)function _mycommand {\s+_arguments \\\s+'--debug\[description\]'.*--help.*}`,
				"#compdef _mycommand mycommand",
			},
		},
		{
			name: "flags with both long and short flags",
			root: func() *Command {
				r := &Command{
					Use:  "testcmd",
					Long: "long description",
					Run:  emptyRun,
				}
				r.Flags().BoolVarP(&debug, "debug", "d", debug, "debug description")
				return r
			}(),
			expectedExpressions: []string{
				`'\(-d --debug\)'{-d,--debug}'\[debug description\]'`,
			},
		},
		{
			name: "command with subcommands and flags with values",
			root: func() *Command {
				r := &Command{
					Use:  "rootcmd",
					Long: "Long rootcmd description",
				}
				d := &Command{
					Use:   "subcmd1",
					Short: "Subcmd1 short description",
					Run:   emptyRun,
				}
				e := &Command{
					Use:  "subcmd2",
					Long: "Subcmd2 short description",
					Run:  emptyRun,
				}
				r.PersistentFlags().BoolVar(&debug, "debug", debug, "description")
				d.Flags().StringVarP(&option, "option", "o", option, "option description")
				r.AddCommand(d, e)
				return r
			}(),
			expectedExpressions: []string{
				`commands=\(\n\s+"help:.*\n\s+"subcmd1:.*\n\s+"subcmd2:.*\n\s+\)`,
				`_arguments \\\n.*'--debug\[description]'`,
				`_arguments -C \\\n.*'--debug\[description]'`,
				`function _rootcmd_subcmd1 {`,
				`function _rootcmd_subcmd1 {`,
				`_arguments \\\n.*'\(-o --option\)'{-o,--option}'\[option description]:' \\\n`,
			},
		},
		{
			name: "filename completion with and without globs",
			root: func() *Command {
				var file string
				r := &Command{
					Use:   "mycmd",
					Short: "my command short description",
					Run:   emptyRun,
				}
				r.Flags().StringVarP(&file, "config", "c", file, "config file")
				r.MarkFlagFilename("config")
				r.Flags().String("output", "", "output file")
				r.MarkFlagFilename("output", "*.log", "*.txt")
				return r
			}(),
			expectedExpressions: []string{
				`\n +'\(-c --config\)'{-c,--config}'\[config file]:filename:_files'`,
				`:_files -g "\*.log" -g "\*.txt"`,
			},
		},
		{
			name: "repeated variables both with and without value",
			root: func() *Command {
				r := genTestCommand("mycmd", true)
				_ = r.Flags().BoolSliceP("debug", "d", []bool{}, "debug usage")
				_ = r.Flags().StringArray("option", []string{}, "options")
				return r
			}(),
			expectedExpressions: []string{
				`'\*--option\[options]`,
				`'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`,
			},
		},
		{
			name: "generated flags --help and --version should be created even when not executing root cmd",
			root: func() *Command {
				r := &Command{
					Use:     "mycmd",
					Short:   "mycmd short description",
					Version: "myversion",
				}
				s := genTestCommand("sub1", true)
				r.AddCommand(s)
				return s
			}(),
			expectedExpressions: []string{
				"--version",
				"--help",
			},
			invocationArgs: []string{
				"sub1",
			},
			skip: "--version and --help are currently not generated when not running on root command",
		},
		{
			name: "zsh generation should run on root command",
			root: func() *Command {
				r := genTestCommand("root", false)
				s := genTestCommand("sub1", true)
				r.AddCommand(s)
				return s
			}(),
			expectedExpressions: []string{
				"function _root {",
			},
		},
		{
			name: "flag description with single quote (') shouldn't break quotes in completion file",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.Flags().Bool("private", false, "Don't show public info")
				return r
			}(),
			expectedExpressions: []string{
				`--private\[Don'\\''t show public info]`,
			},
		},
		{
			name: "argument completion for file with and without patterns",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.MarkZshCompPositionalArgumentFile(1, "*.log")
				r.MarkZshCompPositionalArgumentFile(2)
				return r
			}(),
			expectedExpressions: []string{
				`'1: :_files -g "\*.log"' \\\n\s+'2: :_files`,
			},
		},
		{
			name: "argument zsh completion for words",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.MarkZshCompPositionalArgumentWords(1, "word1", "word2")
				return r
			}(),
			expectedExpressions: []string{
				`'1: :\("word1" "word2"\)`,
			},
		},
		{
			name: "argument completion for words with spaces",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.MarkZshCompPositionalArgumentWords(1, "single", "multiple words")
				return r
			}(),
			expectedExpressions: []string{
				`'1: :\("single" "multiple words"\)'`,
			},
		},
		{
			name: "argument completion when command has ValidArgs and no annotation for argument completion",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.ValidArgs = []string{"word1", "word2"}
				return r
			}(),
			expectedExpressions: []string{
				`'1: :\("word1" "word2"\)'`,
			},
		},
		{
			name: "argument completion when command has ValidArgs and no annotation for argument at argPosition 1",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.ValidArgs = []string{"word1", "word2"}
				r.MarkZshCompPositionalArgumentFile(2)
				return r
			}(),
			expectedExpressions: []string{
				`'1: :\("word1" "word2"\)' \\`,
			},
		},
		{
			name: "directory completion for flag",
			root: func() *Command {
				r := genTestCommand("root", true)
				r.Flags().String("test", "", "test")
				r.PersistentFlags().String("ptest", "", "ptest")
				r.MarkFlagDirname("test")
				r.MarkPersistentFlagDirname("ptest")
				return r
			}(),
			expectedExpressions: []string{
				`--test\[test]:filename:_files -g "-\(/\)"`,
				`--ptest\[ptest]:filename:_files -g "-\(/\)"`,
			},
		},
	}

	for _, tc := range tcs {
		t.Run(tc.name, func(t *testing.T) {
			if tc.skip != "" {
				t.Skip(tc.skip)
			}
			tc.root.Root().SetArgs(tc.invocationArgs)
			tc.root.Execute()
			buf := new(bytes.Buffer)
			if err := tc.root.GenZshCompletion(buf); err != nil {
				t.Error(err)
			}
			output := buf.Bytes()

			for _, expr := range tc.expectedExpressions {
				rgx, err := regexp.Compile(expr)
				if err != nil {
					t.Errorf("error compiling expression (%s): %v", expr, err)
				}
				if !rgx.Match(output) {
					t.Errorf("expected completion (%s) to match '%s'", buf.String(), expr)
				}
			}
		})
	}
}

func TestGenZshCompletionHidden(t *testing.T) {
	tcs := []struct {
		name                string
		root                *Command
		expectedExpressions []string
	}{
		{
			name: "hidden commands",
			root: func() *Command {
				r := &Command{
					Use:   "main",
					Short: "main short description",
				}
				s1 := &Command{
					Use:    "sub1",
					Hidden: true,
					Run:    emptyRun,
				}
				s2 := &Command{
					Use:   "sub2",
					Short: "short sub2 description",
					Run:   emptyRun,
				}
				r.AddCommand(s1, s2)

				return r
			}(),
			expectedExpressions: []string{
				"sub1",
			},
		},
		{
			name: "hidden flags",
			root: func() *Command {
				var hidden string
				r := &Command{
					Use:   "root",
					Short: "root short description",
					Run:   emptyRun,
				}
				r.Flags().StringVarP(&hidden, "hidden", "H", hidden, "hidden usage")
				if err := r.Flags().MarkHidden("hidden"); err != nil {
					t.Errorf("Error setting flag hidden: %v\n", err)
				}
				return r
			}(),
			expectedExpressions: []string{
				"--hidden",
			},
		},
	}

	for _, tc := range tcs {
		t.Run(tc.name, func(t *testing.T) {
			tc.root.Execute()
			buf := new(bytes.Buffer)
			if err := tc.root.GenZshCompletion(buf); err != nil {
				t.Error(err)
			}
			output := buf.String()

			for _, expr := range tc.expectedExpressions {
				if strings.Contains(output, expr) {
					t.Errorf("Expected completion (%s) not to contain '%s' but it does", output, expr)
				}
			}
		})
	}
}

func TestMarkZshCompPositionalArgumentFile(t *testing.T) {
	t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) {
		c := &Command{}
		if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil {
			t.Errorf("Received error when we shouldn't have: %v\n", err)
		}
		if err := c.MarkZshCompPositionalArgumentFile(1); err == nil {
			t.Error("Didn't receive an error when trying to overwrite argument position")
		}
	})

	t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) {
		c := &Command{}
		err := c.MarkZshCompPositionalArgumentFile(0, "*")
		if err == nil {
			t.Fatal("Error was not thrown when indicating argument position 0")
		}
		if !strings.Contains(err.Error(), "position") {
			t.Errorf("expected error message '%s' to contain 'position'", err.Error())
		}
	})
}

func TestMarkZshCompPositionalArgumentWords(t *testing.T) {
	t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) {
		c := &Command{}
		if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil {
			t.Errorf("Received error when we shouldn't have: %v\n", err)
		}
		if err := c.MarkZshCompPositionalArgumentWords(1, "hello"); err == nil {
			t.Error("Didn't receive an error when trying to overwrite argument position")
		}
	})

	t.Run("Doesn't allow calling without words", func(t *testing.T) {
		c := &Command{}
		if err := c.MarkZshCompPositionalArgumentWords(0); err == nil {
			t.Error("Should not allow saving empty word list for annotation")
		}
	})

	t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) {
		c := &Command{}
		err := c.MarkZshCompPositionalArgumentWords(0, "word")
		if err == nil {
			t.Fatal("Should not allow setting argument position less then 1")
		}
		if !strings.Contains(err.Error(), "position") {
			t.Errorf("Expected error '%s' to contain 'position' but didn't", err.Error())
		}
	})
}

func BenchmarkMediumSizeConstruct(b *testing.B) {
	root := constructLargeCommandHierarchy()
	// if err := root.GenZshCompletionFile("_mycmd"); err != nil {
	// 	b.Error(err)
	// }

	for i := 0; i < b.N; i++ {
		buf := new(bytes.Buffer)
		err := root.GenZshCompletion(buf)
		if err != nil {
			b.Error(err)
		}
	}
}

func TestExtractFlags(t *testing.T) {
	var debug, cmdc, cmdd bool
	c := &Command{
		Use:  "cmdC",
		Long: "Command C",
	}
	c.PersistentFlags().BoolVarP(&debug, "debug", "d", debug, "debug mode")
	c.Flags().BoolVar(&cmdc, "cmd-c", cmdc, "Command C")
	d := &Command{
		Use:  "CmdD",
		Long: "Command D",
	}
	d.Flags().BoolVar(&cmdd, "cmd-d", cmdd, "Command D")
	c.AddCommand(d)

	resC := zshCompExtractFlag(c)
	resD := zshCompExtractFlag(d)

	if len(resC) != 2 {
		t.Errorf("expected Command C to return 2 flags, got %d", len(resC))
	}
	if len(resD) != 2 {
		t.Errorf("expected Command D to return 2 flags, got %d", len(resD))
	}
}

func constructLargeCommandHierarchy() *Command {
	var config, st1, st2 string
	var long, debug bool
	var in1, in2 int
	var verbose []bool

	r := genTestCommand("mycmd", false)
	r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage")
	if err := r.MarkPersistentFlagFilename("config", "*"); err != nil {
		panic(err)
	}
	s1 := genTestCommand("sub1", true)
	s1.Flags().BoolVar(&long, "long", long, "long description")
	s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description")
	s1.Flags().StringArray("option", []string{}, "various options")
	s2 := genTestCommand("sub2", true)
	s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description")
	s3 := genTestCommand("sub3", true)
	s3.Hidden = true
	s1_1 := genTestCommand("sub1sub1", true)
	s1_1.Flags().StringVar(&st1, "st1", st1, "st1 description")
	s1_1.Flags().StringVar(&st2, "st2", st2, "st2 description")
	s1_2 := genTestCommand("sub1sub2", true)
	s1_3 := genTestCommand("sub1sub3", true)
	s1_3.Flags().IntVar(&in1, "int1", in1, "int1 description")
	s1_3.Flags().IntVar(&in2, "int2", in2, "int2 description")
	s1_3.Flags().StringArrayP("option", "O", []string{}, "more options")
	s2_1 := genTestCommand("sub2sub1", true)
	s2_2 := genTestCommand("sub2sub2", true)
	s2_3 := genTestCommand("sub2sub3", true)
	s2_4 := genTestCommand("sub2sub4", true)
	s2_5 := genTestCommand("sub2sub5", true)

	s1.AddCommand(s1_1, s1_2, s1_3)
	s2.AddCommand(s2_1, s2_2, s2_3, s2_4, s2_5)
	r.AddCommand(s1, s2, s3)
	r.Execute()
	return r
}

func genTestCommand(name string, withRun bool) *Command {
	r := &Command{
		Use:   name,
		Short: name + " short description",
		Long:  "Long description for " + name,
	}
	if withRun {
		r.Run = emptyRun
	}

	return r
}