package complete

import (
	"bytes"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
	"testing"
)

func TestCompleter_Complete(t *testing.T) {
	initTests()

	c := Command{
		Sub: Commands{
			"sub1": {
				Flags: Flags{
					"-flag1": PredictAnything,
					"-flag2": PredictNothing,
				},
				Sub: Commands{
					"sub11": {},
				},
			},
			"sub2": {
				Flags: Flags{
					"-flag2": PredictNothing,
					"-flag3": PredictSet("opt1", "opt2", "opt12"),
				},
				Args: PredictFiles("*.md"),
			},
			"sub3": {
				Sub: Commands{
					"sub3": {},
				},
			},
		},
		Flags: Flags{
			"-o": PredictFiles("*.txt"),
		},
		GlobalFlags: Flags{
			"-h":       PredictNothing,
			"-global1": PredictAnything,
		},
	}
	cmp := New("cmd", c)

	tests := []struct {
		line  string
		point int // -1 indicates len(line)
		want  []string
	}{
		{
			line:  "cmd ",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd -",
			point: -1,
			want:  []string{"-h", "-global1", "-o"},
		},
		{
			line:  "cmd -h ",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd -global1 ", // global1 is known follow flag
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd sub",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd sub1",
			point: -1,
			want:  []string{"sub1"},
		},
		{
			line:  "cmd sub2",
			point: -1,
			want:  []string{"sub2"},
		},
		{
			line:  "cmd sub1 ",
			point: -1,
			want:  []string{"sub11"},
		},
		{
			line:  "cmd sub3 ",
			point: -1,
			want:  []string{"sub3"},
		},
		{
			line:  "cmd sub1 -",
			point: -1,
			want:  []string{"-flag1", "-flag2", "-h", "-global1"},
		},
		{
			line:  "cmd sub2 ",
			point: -1,
			want:  []string{"./", "dir/", "outer/", "readme.md"},
		},
		{
			line:  "cmd sub2 ./",
			point: -1,
			want:  []string{"./", "./readme.md", "./dir/", "./outer/"},
		},
		{
			line:  "cmd sub2 re",
			point: -1,
			want:  []string{"readme.md"},
		},
		{
			line:  "cmd sub2 ./re",
			point: -1,
			want:  []string{"./readme.md"},
		},
		{
			line:  "cmd sub2 -flag2 ",
			point: -1,
			want:  []string{"./", "dir/", "outer/", "readme.md"},
		},
		{
			line:  "cmd sub1 -fl",
			point: -1,
			want:  []string{"-flag1", "-flag2"},
		},
		{
			line:  "cmd sub1 -flag1",
			point: -1,
			want:  []string{"-flag1"},
		},
		{
			line:  "cmd sub1 -flag1 ",
			point: -1,
			want:  []string{}, // flag1 is unknown follow flag
		},
		{
			line:  "cmd sub1 -flag2 -",
			point: -1,
			want:  []string{"-flag1", "-flag2", "-h", "-global1"},
		},
		{
			line:  "cmd -no-such-flag",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd -no-such-flag ",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd -no-such-flag -",
			point: -1,
			want:  []string{"-h", "-global1", "-o"},
		},
		{
			line:  "cmd no-such-command",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd no-such-command ",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd -o ",
			point: -1,
			want:  []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"},
		},
		{
			line:  "cmd -o ./no-su",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd -o ./",
			point: -1,
			want:  []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
		},
		{
			line:  "cmd -o=./",
			point: -1,
			want:  []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
		},
		{
			line:  "cmd -o .",
			point: -1,
			want:  []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
		},
		{
			line:  "cmd -o ./b",
			point: -1,
			want:  []string{"./b.txt"},
		},
		{
			line:  "cmd -o=./b",
			point: -1,
			want:  []string{"./b.txt"},
		},
		{
			line:  "cmd -o ./read",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd -o=./read",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd -o ./readme.md",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd -o ./readme.md ",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd -o=./readme.md ",
			point: -1,
			want:  []string{"sub1", "sub2", "sub3"},
		},
		{
			line:  "cmd -o sub2 -flag3 ",
			point: -1,
			want:  []string{"opt1", "opt2", "opt12"},
		},
		{
			line:  "cmd -o sub2 -flag3 opt1",
			point: -1,
			want:  []string{"opt1", "opt12"},
		},
		{
			line:  "cmd -o sub2 -flag3 opt",
			point: -1,
			want:  []string{"opt1", "opt2", "opt12"},
		},
		{
			line: "cmd -o ./b foo",
			//               ^
			point: 10,
			want:  []string{"./b.txt"},
		},
		{
			line: "cmd -o=./b foo",
			//               ^
			point: 10,
			want:  []string{"./b.txt"},
		},
		{
			line: "cmd -o sub2 -flag3 optfoo",
			//                           ^
			point: 22,
			want:  []string{"opt1", "opt2", "opt12"},
		},
		{
			line: "cmd -o ",
			//         ^
			point: 4,
			want:  []string{"sub1", "sub2", "sub3"},
		},
	}

	for _, tt := range tests {
		t.Run(fmt.Sprintf("%s@%d", tt.line, tt.point), func(t *testing.T) {
			got := runComplete(cmp, tt.line, tt.point)

			sort.Strings(tt.want)
			sort.Strings(got)

			if !equalSlices(got, tt.want) {
				t.Errorf("failed '%s'\ngot: %s\nwant: %s", t.Name(), got, tt.want)
			}
		})
	}
}

func TestCompleter_Complete_SharedPrefix(t *testing.T) {
	initTests()

	c := Command{
		Sub: Commands{
			"status": {
				Flags: Flags{
					"-f3": PredictNothing,
				},
			},
			"job": {
				Sub: Commands{
					"status": {
						Flags: Flags{
							"-f4": PredictNothing,
						},
					},
				},
			},
		},
		Flags: Flags{
			"-o": PredictFiles("*.txt"),
		},
		GlobalFlags: Flags{
			"-h":       PredictNothing,
			"-global1": PredictAnything,
		},
	}

	cmp := New("cmd", c)

	tests := []struct {
		line  string
		point int // -1 indicates len(line)
		want  []string
	}{
		{
			line:  "cmd ",
			point: -1,
			want:  []string{"status", "job"},
		},
		{
			line:  "cmd -",
			point: -1,
			want:  []string{"-h", "-global1", "-o"},
		},
		{
			line:  "cmd j",
			point: -1,
			want:  []string{"job"},
		},
		{
			line:  "cmd job ",
			point: -1,
			want:  []string{"status"},
		},
		{
			line:  "cmd job -",
			point: -1,
			want:  []string{"-h", "-global1"},
		},
		{
			line:  "cmd job status ",
			point: -1,
			want:  []string{},
		},
		{
			line:  "cmd job status -",
			point: -1,
			want:  []string{"-f4", "-h", "-global1"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.line, func(t *testing.T) {
			got := runComplete(cmp, tt.line, tt.point)

			sort.Strings(tt.want)
			sort.Strings(got)

			if !equalSlices(got, tt.want) {
				t.Errorf("failed '%s'\ngot = %s\nwant: %s", t.Name(), got, tt.want)
			}
		})
	}
}

// runComplete runs the complete login for test purposes
// it gets the complete struct and command line arguments and returns
// the complete options
func runComplete(c *Complete, line string, point int) (completions []string) {
	if point == -1 {
		point = len(line)
	}
	os.Setenv(envLine, line)
	os.Setenv(envPoint, strconv.Itoa(point))
	b := bytes.NewBuffer(nil)
	c.Out = b
	c.Complete()
	completions = parseOutput(b.String())
	return
}

func parseOutput(output string) []string {
	lines := strings.Split(output, "\n")
	options := []string{}
	for _, l := range lines {
		if l != "" {
			options = append(options, l)
		}
	}
	return options
}

func equalSlices(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
