package flags

import (
	"fmt"
	"os"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"
)

type defaultOptions struct {
	Int        int `long:"i"`
	IntDefault int `long:"id" default:"1"`

	Float64        float64 `long:"f"`
	Float64Default float64 `long:"fd" default:"-3.14"`

	NumericFlag bool `short:"3"`

	String            string `long:"str"`
	StringDefault     string `long:"strd" default:"abc"`
	StringNotUnquoted string `long:"strnot" unquote:"false"`

	Time        time.Duration `long:"t"`
	TimeDefault time.Duration `long:"td" default:"1m"`

	Map        map[string]int `long:"m"`
	MapDefault map[string]int `long:"md" default:"a:1"`

	Slice        []int `long:"s"`
	SliceDefault []int `long:"sd" default:"1" default:"2"`
}

func TestDefaults(t *testing.T) {
	var tests = []struct {
		msg      string
		args     []string
		expected defaultOptions
	}{
		{
			msg:  "no arguments, expecting default values",
			args: []string{},
			expected: defaultOptions{
				Int:        0,
				IntDefault: 1,

				Float64:        0.0,
				Float64Default: -3.14,

				NumericFlag: false,

				String:        "",
				StringDefault: "abc",

				Time:        0,
				TimeDefault: time.Minute,

				Map:        map[string]int{},
				MapDefault: map[string]int{"a": 1},

				Slice:        []int{},
				SliceDefault: []int{1, 2},
			},
		},
		{
			msg:  "non-zero value arguments, expecting overwritten arguments",
			args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"},
			expected: defaultOptions{
				Int:        3,
				IntDefault: 3,

				Float64:        -2.71,
				Float64Default: 2.71,

				NumericFlag: true,

				String:        "def",
				StringDefault: "def",

				Time:        3 * time.Millisecond,
				TimeDefault: 3 * time.Millisecond,

				Map:        map[string]int{"c": 3},
				MapDefault: map[string]int{"c": 3},

				Slice:        []int{3},
				SliceDefault: []int{3},
			},
		},
		{
			msg:  "zero value arguments, expecting overwritten arguments",
			args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"},
			expected: defaultOptions{
				Int:        0,
				IntDefault: 0,

				Float64:        0,
				Float64Default: 0,

				String:        "",
				StringDefault: "",

				Time:        0,
				TimeDefault: 0,

				Map:        map[string]int{"": 0},
				MapDefault: map[string]int{"": 0},

				Slice:        []int{0},
				SliceDefault: []int{0},
			},
		},
	}

	for _, test := range tests {
		var opts defaultOptions

		_, err := ParseArgs(&opts, test.args)
		if err != nil {
			t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
		}

		if opts.Slice == nil {
			opts.Slice = []int{}
		}

		if !reflect.DeepEqual(opts, test.expected) {
			t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
		}
	}
}

func TestNoDefaultsForBools(t *testing.T) {
	var opts struct {
		DefaultBool bool `short:"d" default:"true"`
	}

	if runtime.GOOS == "windows" {
		assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts)
	} else {
		assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts)
	}
}

func TestUnquoting(t *testing.T) {
	var tests = []struct {
		arg   string
		err   error
		value string
	}{
		{
			arg:   "\"abc",
			err:   strconv.ErrSyntax,
			value: "",
		},
		{
			arg:   "\"\"abc\"",
			err:   strconv.ErrSyntax,
			value: "",
		},
		{
			arg:   "\"abc\"",
			err:   nil,
			value: "abc",
		},
		{
			arg:   "\"\\\"abc\\\"\"",
			err:   nil,
			value: "\"abc\"",
		},
		{
			arg:   "\"\\\"abc\"",
			err:   nil,
			value: "\"abc",
		},
	}

	for _, test := range tests {
		var opts defaultOptions

		for _, delimiter := range []bool{false, true} {
			p := NewParser(&opts, None)

			var err error
			if delimiter {
				_, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg})
			} else {
				_, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg})
			}

			if test.err == nil {
				if err != nil {
					t.Fatalf("Expected no error but got: %v", err)
				}

				if test.value != opts.String {
					t.Fatalf("Expected String to be %q but got %q", test.value, opts.String)
				}
				if q := strconv.Quote(test.value); q != opts.StringNotUnquoted {
					t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted)
				}
			} else {
				if err == nil {
					t.Fatalf("Expected error")
				} else if e, ok := err.(*Error); ok {
					if strings.HasPrefix(e.Message, test.err.Error()) {
						t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message)
					}
				}
			}
		}
	}
}

// EnvRestorer keeps a copy of a set of env variables and can restore the env from them
type EnvRestorer struct {
	env map[string]string
}

func (r *EnvRestorer) Restore() {
	os.Clearenv()

	for k, v := range r.env {
		os.Setenv(k, v)
	}
}

// EnvSnapshot returns a snapshot of the currently set env variables
func EnvSnapshot() *EnvRestorer {
	r := EnvRestorer{make(map[string]string)}

	for _, kv := range os.Environ() {
		parts := strings.SplitN(kv, "=", 2)

		if len(parts) != 2 {
			panic("got a weird env variable: " + kv)
		}

		r.env[parts[0]] = parts[1]
	}

	return &r
}

type envDefaultOptions struct {
	Int   int            `long:"i" default:"1" env:"TEST_I"`
	Time  time.Duration  `long:"t" default:"1m" env:"TEST_T"`
	Map   map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"`
	Slice []int          `long:"s" default:"1" default:"2" env:"TEST_S"  env-delim:","`
}

func TestEnvDefaults(t *testing.T) {
	var tests = []struct {
		msg      string
		args     []string
		expected envDefaultOptions
		env      map[string]string
	}{
		{
			msg:  "no arguments, no env, expecting default values",
			args: []string{},
			expected: envDefaultOptions{
				Int:   1,
				Time:  time.Minute,
				Map:   map[string]int{"a": 1},
				Slice: []int{1, 2},
			},
		},
		{
			msg:  "no arguments, env defaults, expecting env default values",
			args: []string{},
			expected: envDefaultOptions{
				Int:   2,
				Time:  2 * time.Minute,
				Map:   map[string]int{"a": 2, "b": 3},
				Slice: []int{4, 5, 6},
			},
			env: map[string]string{
				"TEST_I": "2",
				"TEST_T": "2m",
				"TEST_M": "a:2;b:3",
				"TEST_S": "4,5,6",
			},
		},
		{
			msg:  "non-zero value arguments, expecting overwritten arguments",
			args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"},
			expected: envDefaultOptions{
				Int:   3,
				Time:  3 * time.Millisecond,
				Map:   map[string]int{"c": 3},
				Slice: []int{3},
			},
			env: map[string]string{
				"TEST_I": "2",
				"TEST_T": "2m",
				"TEST_M": "a:2;b:3",
				"TEST_S": "4,5,6",
			},
		},
		{
			msg:  "zero value arguments, expecting overwritten arguments",
			args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"},
			expected: envDefaultOptions{
				Int:   0,
				Time:  0,
				Map:   map[string]int{"": 0},
				Slice: []int{0},
			},
			env: map[string]string{
				"TEST_I": "2",
				"TEST_T": "2m",
				"TEST_M": "a:2;b:3",
				"TEST_S": "4,5,6",
			},
		},
	}

	oldEnv := EnvSnapshot()
	defer oldEnv.Restore()

	for _, test := range tests {
		var opts envDefaultOptions
		oldEnv.Restore()
		for envKey, envValue := range test.env {
			os.Setenv(envKey, envValue)
		}
		_, err := ParseArgs(&opts, test.args)
		if err != nil {
			t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
		}

		if opts.Slice == nil {
			opts.Slice = []int{}
		}

		if !reflect.DeepEqual(opts, test.expected) {
			t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
		}
	}
}

func TestOptionAsArgument(t *testing.T) {
	var tests = []struct {
		args        []string
		expectError bool
		errType     ErrorType
		errMsg      string
		rest        []string
	}{
		{
			// short option must not be accepted as argument
			args:        []string{"--string-slice", "foobar", "--string-slice", "-o"},
			expectError: true,
			errType:     ErrExpectedArgument,
			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'",
		},
		{
			// long option must not be accepted as argument
			args:        []string{"--string-slice", "foobar", "--string-slice", "--other-option"},
			expectError: true,
			errType:     ErrExpectedArgument,
			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'",
		},
		{
			// long option must not be accepted as argument
			args:        []string{"--string-slice", "--"},
			expectError: true,
			errType:     ErrExpectedArgument,
			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'",
		},
		{
			// quoted and appended option should be accepted as argument (even if it looks like an option)
			args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""},
		},
		{
			// Accept any single character arguments including '-'
			args: []string{"--string-slice", "-"},
		},
		{
			// Do not accept arguments which start with '-' even if the next character is a digit
			args:        []string{"--string-slice", "-3.14"},
			expectError: true,
			errType:     ErrExpectedArgument,
			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'",
		},
		{
			// Do not accept arguments which start with '-' if the next character is not a digit
			args:        []string{"--string-slice", "-character"},
			expectError: true,
			errType:     ErrExpectedArgument,
			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'",
		},
		{
			args: []string{"-o", "-", "-"},
			rest: []string{"-", "-"},
		},
		{
			// Accept arguments which start with '-' if the next character is a digit, for number options only
			args: []string{"--int-slice", "-3"},
		},
		{
			// Accept arguments which start with '-' if the next character is a digit, for number options only
			args: []string{"--int16", "-3"},
		},
		{
			// Accept arguments which start with '-' if the next character is a digit, for number options only
			args: []string{"--float32", "-3.2"},
		},
		{
			// Accept arguments which start with '-' if the next character is a digit, for number options only
			args: []string{"--float32ptr", "-3.2"},
		},
	}

	var opts struct {
		StringSlice []string `long:"string-slice"`
		IntSlice    []int    `long:"int-slice"`
		Int16       int16    `long:"int16"`
		Float32     float32  `long:"float32"`
		Float32Ptr  *float32 `long:"float32ptr"`
		OtherOption bool     `long:"other-option" short:"o"`
	}

	for _, test := range tests {
		if test.expectError {
			assertParseFail(t, test.errType, test.errMsg, &opts, test.args...)
		} else {
			args := assertParseSuccess(t, &opts, test.args...)

			assertStringArray(t, args, test.rest)
		}
	}
}

func TestUnknownFlagHandler(t *testing.T) {

	var opts struct {
		Flag1 string `long:"flag1"`
		Flag2 string `long:"flag2"`
	}

	p := NewParser(&opts, None)

	var unknownFlag1 string
	var unknownFlag2 bool
	var unknownFlag3 string

	// Set up a callback to intercept unknown options during parsing
	p.UnknownOptionHandler = func(option string, arg SplitArgument, args []string) ([]string, error) {
		if option == "unknownFlag1" {
			if argValue, ok := arg.Value(); ok {
				unknownFlag1 = argValue
				return args, nil
			}
			// consume a value from remaining args list
			unknownFlag1 = args[0]
			return args[1:], nil
		} else if option == "unknownFlag2" {
			// treat this one as a bool switch, don't consume any args
			unknownFlag2 = true
			return args, nil
		} else if option == "unknownFlag3" {
			if argValue, ok := arg.Value(); ok {
				unknownFlag3 = argValue
				return args, nil
			}
			// consume a value from remaining args list
			unknownFlag3 = args[0]
			return args[1:], nil
		}

		return args, fmt.Errorf("Unknown flag: %v", option)
	}

	// Parse args containing some unknown flags, verify that
	// our callback can handle all of them
	_, err := p.ParseArgs([]string{"--flag1=stuff", "--unknownFlag1", "blah", "--unknownFlag2", "--unknownFlag3=baz", "--flag2=foo"})

	if err != nil {
		assertErrorf(t, "Parser returned unexpected error %v", err)
	}

	assertString(t, opts.Flag1, "stuff")
	assertString(t, opts.Flag2, "foo")
	assertString(t, unknownFlag1, "blah")
	assertString(t, unknownFlag3, "baz")

	if !unknownFlag2 {
		assertErrorf(t, "Flag should have been set by unknown handler, but had value: %v", unknownFlag2)
	}

	// Parse args with unknown flags that callback doesn't handle, verify it returns error
	_, err = p.ParseArgs([]string{"--flag1=stuff", "--unknownFlagX", "blah", "--flag2=foo"})

	if err == nil {
		assertErrorf(t, "Parser should have returned error, but returned nil")
	}
}

func TestChoices(t *testing.T) {
	var opts struct {
		Choice string `long:"choose" choice:"v1" choice:"v2"`
	}

	assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid")
	assertParseSuccess(t, &opts, "--choose", "v2")
	assertString(t, opts.Choice, "v2")
}

func TestEmbedded(t *testing.T) {
	type embedded struct {
		V bool `short:"v"`
	}
	var opts struct {
		embedded
	}

	assertParseSuccess(t, &opts, "-v")

	if !opts.V {
		t.Errorf("Expected V to be true")
	}
}

type command struct {
}

func (c *command) Execute(args []string) error {
	return nil
}

func TestCommandHandlerNoCommand(t *testing.T) {
	var opts = struct {
		Value bool `short:"v"`
	}{}

	parser := NewParser(&opts, Default&^PrintErrors)

	var executedCommand Commander
	var executedArgs []string

	executed := false

	parser.CommandHandler = func(command Commander, args []string) error {
		executed = true

		executedCommand = command
		executedArgs = args

		return nil
	}

	_, err := parser.ParseArgs([]string{"arg1", "arg2"})

	if err != nil {
		t.Fatalf("Unexpected parse error: %s", err)
	}

	if !executed {
		t.Errorf("Expected command handler to be executed")
	}

	if executedCommand != nil {
		t.Errorf("Did not exect an executed command")
	}

	assertStringArray(t, executedArgs, []string{"arg1", "arg2"})
}

func TestCommandHandler(t *testing.T) {
	var opts = struct {
		Value bool `short:"v"`

		Command command `command:"cmd"`
	}{}

	parser := NewParser(&opts, Default&^PrintErrors)

	var executedCommand Commander
	var executedArgs []string

	executed := false

	parser.CommandHandler = func(command Commander, args []string) error {
		executed = true

		executedCommand = command
		executedArgs = args

		return nil
	}

	_, err := parser.ParseArgs([]string{"cmd", "arg1", "arg2"})

	if err != nil {
		t.Fatalf("Unexpected parse error: %s", err)
	}

	if !executed {
		t.Errorf("Expected command handler to be executed")
	}

	if executedCommand == nil {
		t.Errorf("Expected command handler to be executed")
	}

	assertStringArray(t, executedArgs, []string{"arg1", "arg2"})
}
