package flags

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"reflect"
	"strings"
	"testing"
)

func TestWriteIni(t *testing.T) {
	oldEnv := EnvSnapshot()
	defer oldEnv.Restore()
	os.Setenv("ENV_DEFAULT", "env-def")

	var tests = []struct {
		args     []string
		options  IniOptions
		expected string
	}{
		{
			[]string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"},
			IniDefault,
			`[Application Options]
; Show verbose debug information
verbose = true
verbose = true

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

[Other Options]
; A map from string to int
int-map = a:2
int-map = b:3

`,
		},
		{
			[]string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"},
			IniDefault | IniIncludeDefaults,
			`[Application Options]
; Show verbose debug information
verbose = true
verbose = true

; A slice of pointers to string
; PtrSlice =

EmptyDescription = false

; Test default value
Default = "Some\nvalue"

; Test default array value
DefaultArray = Some value
DefaultArray = "Other\tvalue"

; Testdefault map value
DefaultMap = another:value
DefaultMap = some:value

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

; Option with named argument
OptionWithArgName =

; Option with choices
OptionWithChoices =

; Option only available in ini
only-ini =

[Other Options]
; A slice of strings
StringSlice = some
StringSlice = value

; A map from string to int
int-map = a:2
int-map = b:3

[Subgroup]
; This is a subgroup option
Opt =

; Not hidden inside group
NotHiddenInsideGroup =

[Subsubgroup]
; This is a subsubgroup option
Opt =

[command]
; Use for extra verbosity
; ExtraVerbose =

`,
		},
		{
			[]string{"filename", "0", "3.14", "command"},
			IniDefault | IniIncludeDefaults | IniCommentDefaults,
			`[Application Options]
; Show verbose debug information
; verbose =

; A slice of pointers to string
; PtrSlice =

; EmptyDescription = false

; Test default value
; Default = "Some\nvalue"

; Test default array value
; DefaultArray = Some value
; DefaultArray = "Other\tvalue"

; Testdefault map value
; DefaultMap = another:value
; DefaultMap = some:value

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

; Option with named argument
; OptionWithArgName =

; Option with choices
; OptionWithChoices =

; Option only available in ini
; only-ini =

[Other Options]
; A slice of strings
; StringSlice = some
; StringSlice = value

; A map from string to int
; int-map = a:1

[Subgroup]
; This is a subgroup option
; Opt =

; Not hidden inside group
; NotHiddenInsideGroup =

[Subsubgroup]
; This is a subsubgroup option
; Opt =

[command]
; Use for extra verbosity
; ExtraVerbose =

`,
		},
		{
			[]string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "3.14", "command"},
			IniDefault | IniIncludeDefaults | IniCommentDefaults,
			`[Application Options]
; Show verbose debug information
; verbose =

; A slice of pointers to string
; PtrSlice =

; EmptyDescription = false

; Test default value
Default = New value

; Test default array value
DefaultArray = New value

; Testdefault map value
DefaultMap = new:value

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

; Option with named argument
; OptionWithArgName =

; Option with choices
; OptionWithChoices =

; Option only available in ini
; only-ini =

[Other Options]
; A slice of strings
; StringSlice = some
; StringSlice = value

; A map from string to int
; int-map = a:1

[Subgroup]
; This is a subgroup option
; Opt =

; Not hidden inside group
; NotHiddenInsideGroup =

[Subsubgroup]
; This is a subsubgroup option
; Opt =

[command]
; Use for extra verbosity
; ExtraVerbose =

`,
		},
	}

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

		p := NewNamedParser("TestIni", Default)
		p.AddGroup("Application Options", "The application options", &opts)

		_, err := p.ParseArgs(test.args)

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

		inip := NewIniParser(p)

		var b bytes.Buffer
		inip.Write(&b, test.options)

		got := b.String()
		expected := test.expected

		msg := fmt.Sprintf("with arguments %+v and ini options %b", test.args, test.options)
		assertDiff(t, got, expected, msg)
	}
}

func TestReadIni_flagEquivalent(t *testing.T) {
	type options struct {
		Opt1 bool `long:"opt1"`

		Group1 struct {
			Opt2 bool `long:"opt2"`
		} `group:"group1"`

		Group2 struct {
			Opt3 bool `long:"opt3"`
		} `group:"group2" namespace:"ns1"`

		Cmd1 struct {
			Opt4 bool `long:"opt4"`
			Opt5 bool `long:"foo.opt5"`

			Group1 struct {
				Opt6 bool `long:"opt6"`
				Opt7 bool `long:"foo.opt7"`
			} `group:"group1"`

			Group2 struct {
				Opt8 bool `long:"opt8"`
			} `group:"group2" namespace:"ns1"`
		} `command:"cmd1"`
	}

	a := `
opt1=true

[group1]
opt2=true

[group2]
ns1.opt3=true

[cmd1]
opt4=true
foo.opt5=true

[cmd1.group1]
opt6=true
foo.opt7=true

[cmd1.group2]
ns1.opt8=true
`
	b := `
opt1=true
opt2=true
ns1.opt3=true

[cmd1]
opt4=true
foo.opt5=true
opt6=true
foo.opt7=true
ns1.opt8=true
`

	parse := func(readIni string) (opts options, writeIni string) {
		p := NewNamedParser("TestIni", Default)
		p.AddGroup("Application Options", "The application options", &opts)

		inip := NewIniParser(p)
		err := inip.Parse(strings.NewReader(readIni))

		if err != nil {
			t.Fatalf("Unexpected error: %s\n\nFile:\n%s", err, readIni)
		}

		var b bytes.Buffer
		inip.Write(&b, Default)

		return opts, b.String()
	}

	aOpt, aIni := parse(a)
	bOpt, bIni := parse(b)

	assertDiff(t, aIni, bIni, "")
	if !reflect.DeepEqual(aOpt, bOpt) {
		t.Errorf("not equal")
	}
}

func TestReadIni(t *testing.T) {
	var opts helpOptions

	p := NewNamedParser("TestIni", Default)
	p.AddGroup("Application Options", "The application options", &opts)

	inip := NewIniParser(p)

	inic := `
; Show verbose debug information
verbose = true
verbose = true

DefaultMap = another:"value\n1"
DefaultMap = some:value 2

[Application Options]
; A slice of pointers to string
; PtrSlice =

; Test default value
Default = "New\nvalue"

; Test env-default1 value
EnvDefault1 = New value

[Other Options]
# A slice of strings
StringSlice = "some\nvalue"
StringSlice = another value

; A map from string to int
int-map = a:2
int-map = b:3

`

	b := strings.NewReader(inic)
	err := inip.Parse(b)

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

	assertBoolArray(t, opts.Verbose, []bool{true, true})

	if v := map[string]string{"another": "value\n1", "some": "value 2"}; !reflect.DeepEqual(opts.DefaultMap, v) {
		t.Fatalf("Expected %#v for DefaultMap but got %#v", v, opts.DefaultMap)
	}

	assertString(t, opts.Default, "New\nvalue")

	assertString(t, opts.EnvDefault1, "New value")

	assertStringArray(t, opts.Other.StringSlice, []string{"some\nvalue", "another value"})

	if v, ok := opts.Other.IntMap["a"]; !ok {
		t.Errorf("Expected \"a\" in Other.IntMap")
	} else if v != 2 {
		t.Errorf("Expected Other.IntMap[\"a\"] = 2, but got %v", v)
	}

	if v, ok := opts.Other.IntMap["b"]; !ok {
		t.Errorf("Expected \"b\" in Other.IntMap")
	} else if v != 3 {
		t.Errorf("Expected Other.IntMap[\"b\"] = 3, but got %v", v)
	}
}

func TestReadAndWriteIni(t *testing.T) {
	var tests = []struct {
		options IniOptions
		read    string
		write   string
	}{
		{
			IniIncludeComments,
			`[Application Options]
; Show verbose debug information
verbose = true
verbose = true

; Test default value
Default = "quote me"

; Test default array value
DefaultArray = 1
DefaultArray = "2"
DefaultArray = 3

; Testdefault map value
; DefaultMap =

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

[Other Options]
; A slice of strings
; StringSlice =

; A map from string to int
int-map = a:2
int-map = b:"3"

`,
			`[Application Options]
; Show verbose debug information
verbose = true
verbose = true

; Test default value
Default = "quote me"

; Test default array value
DefaultArray = 1
DefaultArray = 2
DefaultArray = 3

; Testdefault map value
; DefaultMap =

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

[Other Options]
; A slice of strings
; StringSlice =

; A map from string to int
int-map = a:2
int-map = b:3

`,
		},
		{
			IniIncludeComments,
			`[Application Options]
; Show verbose debug information
verbose = true
verbose = true

; Test default value
Default = "quote me"

; Test default array value
DefaultArray = "1"
DefaultArray = "2"
DefaultArray = "3"

; Testdefault map value
; DefaultMap =

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

[Other Options]
; A slice of strings
; StringSlice =

; A map from string to int
int-map = a:"2"
int-map = b:"3"

`,
			`[Application Options]
; Show verbose debug information
verbose = true
verbose = true

; Test default value
Default = "quote me"

; Test default array value
DefaultArray = "1"
DefaultArray = "2"
DefaultArray = "3"

; Testdefault map value
; DefaultMap =

; Test env-default1 value
EnvDefault1 = env-def

; Test env-default2 value
EnvDefault2 = env-def

[Other Options]
; A slice of strings
; StringSlice =

; A map from string to int
int-map = a:"2"
int-map = b:"3"

`,
		},
	}

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

		p := NewNamedParser("TestIni", Default)
		p.AddGroup("Application Options", "The application options", &opts)

		inip := NewIniParser(p)

		read := strings.NewReader(test.read)
		err := inip.Parse(read)
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		var write bytes.Buffer
		inip.Write(&write, test.options)

		got := write.String()

		msg := fmt.Sprintf("with ini options %b", test.options)
		assertDiff(t, got, test.write, msg)
	}
}

func TestReadIniWrongQuoting(t *testing.T) {
	var tests = []struct {
		iniFile    string
		lineNumber uint
	}{
		{
			iniFile:    `Default = "New\nvalue`,
			lineNumber: 1,
		},
		{
			iniFile:    `StringSlice = "New\nvalue`,
			lineNumber: 1,
		},
		{
			iniFile: `StringSlice = "New\nvalue"
			StringSlice = "Second\nvalue`,
			lineNumber: 2,
		},
		{
			iniFile:    `DefaultMap = some:"value`,
			lineNumber: 1,
		},
		{
			iniFile: `DefaultMap = some:value
			DefaultMap = another:"value`,
			lineNumber: 2,
		},
	}

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

		p := NewNamedParser("TestIni", Default)
		p.AddGroup("Application Options", "The application options", &opts)

		inip := NewIniParser(p)

		inic := test.iniFile

		b := strings.NewReader(inic)
		err := inip.Parse(b)

		if err == nil {
			t.Fatalf("Expect error")
		}

		iniError := err.(*IniError)

		if iniError.LineNumber != test.lineNumber {
			t.Fatalf("Expect error on line %d", test.lineNumber)
		}
	}
}

func TestIniCommands(t *testing.T) {
	var opts struct {
		Value string `short:"v" long:"value"`

		Add struct {
			Name int `short:"n" long:"name" ini-name:"AliasName"`

			Other struct {
				O string `short:"o" long:"other"`
			} `group:"Other Options"`
		} `command:"add"`
	}

	p := NewNamedParser("TestIni", Default)
	p.AddGroup("Application Options", "The application options", &opts)

	inip := NewIniParser(p)

	inic := `[Application Options]
value = some value

[add]
AliasName = 5

[add.Other Options]
other = subgroup

`

	b := strings.NewReader(inic)
	err := inip.Parse(b)

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

	assertString(t, opts.Value, "some value")

	if opts.Add.Name != 5 {
		t.Errorf("Expected opts.Add.Name to be 5, but got %v", opts.Add.Name)
	}

	assertString(t, opts.Add.Other.O, "subgroup")

	// Test writing it back
	buf := &bytes.Buffer{}

	inip.Write(buf, IniDefault)

	assertDiff(t, buf.String(), inic, "ini contents")
}

func TestIniNoIni(t *testing.T) {
	var opts struct {
		NoValue string `short:"n" long:"novalue" no-ini:"yes"`
		Value   string `short:"v" long:"value"`
	}

	p := NewNamedParser("TestIni", Default)
	p.AddGroup("Application Options", "The application options", &opts)

	inip := NewIniParser(p)

	// read INI
	inic := `[Application Options]
novalue = some value
value = some other value
`

	b := strings.NewReader(inic)
	err := inip.Parse(b)

	if err == nil {
		t.Fatalf("Expected error")
	}

	iniError := err.(*IniError)

	if v := uint(2); iniError.LineNumber != v {
		t.Errorf("Expected opts.Add.Name to be %d, but got %d", v, iniError.LineNumber)
	}

	if v := "unknown option: novalue"; iniError.Message != v {
		t.Errorf("Expected opts.Add.Name to be %s, but got %s", v, iniError.Message)
	}

	// write INI
	opts.NoValue = "some value"
	opts.Value = "some other value"

	file, err := ioutil.TempFile("", "")
	if err != nil {
		t.Fatalf("Cannot create temporary file: %s", err)
	}
	defer os.Remove(file.Name())

	err = inip.WriteFile(file.Name(), IniIncludeDefaults)
	if err != nil {
		t.Fatalf("Could not write ini file: %s", err)
	}

	found, err := ioutil.ReadFile(file.Name())
	if err != nil {
		t.Fatalf("Could not read written ini file: %s", err)
	}

	expected := "[Application Options]\nValue = some other value\n\n"

	assertDiff(t, string(found), expected, "ini content")
}

func TestIniParse(t *testing.T) {
	file, err := ioutil.TempFile("", "")
	if err != nil {
		t.Fatalf("Cannot create temporary file: %s", err)
	}
	defer os.Remove(file.Name())

	_, err = file.WriteString("value = 123")
	if err != nil {
		t.Fatalf("Cannot write to temporary file: %s", err)
	}

	file.Close()

	var opts struct {
		Value int `long:"value"`
	}

	err = IniParse(file.Name(), &opts)
	if err != nil {
		t.Fatalf("Could not parse ini: %s", err)
	}

	if opts.Value != 123 {
		t.Fatalf("Expected Value to be \"123\" but was \"%d\"", opts.Value)
	}
}

func TestIniCliOverrides(t *testing.T) {
	file, err := ioutil.TempFile("", "")

	if err != nil {
		t.Fatalf("Cannot create temporary file: %s", err)
	}

	defer os.Remove(file.Name())

	_, err = file.WriteString("values = 123\n")
	_, err = file.WriteString("values = 456\n")

	if err != nil {
		t.Fatalf("Cannot write to temporary file: %s", err)
	}

	file.Close()

	var opts struct {
		Values []int `long:"values"`
	}

	p := NewParser(&opts, Default)
	err = NewIniParser(p).ParseFile(file.Name())

	if err != nil {
		t.Fatalf("Could not parse ini: %s", err)
	}

	_, err = p.ParseArgs([]string{"--values", "111", "--values", "222"})

	if err != nil {
		t.Fatalf("Failed to parse arguments: %s", err)
	}

	if len(opts.Values) != 2 {
		t.Fatalf("Expected Values to contain two elements, but got %d", len(opts.Values))
	}

	if opts.Values[0] != 111 {
		t.Fatalf("Expected Values[0] to be 111, but got '%d'", opts.Values[0])
	}

	if opts.Values[1] != 222 {
		t.Fatalf("Expected Values[1] to be 222, but got '%d'", opts.Values[1])
	}
}

func TestIniOverrides(t *testing.T) {
	file, err := ioutil.TempFile("", "")

	if err != nil {
		t.Fatalf("Cannot create temporary file: %s", err)
	}

	defer os.Remove(file.Name())

	_, err = file.WriteString("value-with-default = \"ini-value\"\n")
	_, err = file.WriteString("value-with-default-override-cli = \"ini-value\"\n")

	if err != nil {
		t.Fatalf("Cannot write to temporary file: %s", err)
	}

	file.Close()

	var opts struct {
		ValueWithDefault            string `long:"value-with-default" default:"value"`
		ValueWithDefaultOverrideCli string `long:"value-with-default-override-cli" default:"value"`
	}

	p := NewParser(&opts, Default)
	err = NewIniParser(p).ParseFile(file.Name())

	if err != nil {
		t.Fatalf("Could not parse ini: %s", err)
	}

	_, err = p.ParseArgs([]string{"--value-with-default-override-cli", "cli-value"})

	if err != nil {
		t.Fatalf("Failed to parse arguments: %s", err)
	}

	assertString(t, opts.ValueWithDefault, "ini-value")
	assertString(t, opts.ValueWithDefaultOverrideCli, "cli-value")
}

func TestIniRequired(t *testing.T) {
	var opts struct {
		Required string               `short:"r" required:"yes" description:"required"`
		Config   func(s string) error `long:"config" default:"no-ini-file" no-ini:"true"`
	}

	p := NewParser(&opts, Default)

	opts.Config = func(s string) error {
		inip := NewIniParser(p)
		inip.ParseAsDefaults = true
		return inip.Parse(strings.NewReader("Required = ini-value\n"))
	}

	_, err := p.ParseArgs([]string{"-r", "cli-value"})

	if err != nil {
		t.Fatalf("Failed to parse arguments: %s", err)
	}

	assertString(t, opts.Required, "cli-value")
}

func TestWriteFile(t *testing.T) {
	file, err := ioutil.TempFile("", "")
	if err != nil {
		t.Fatalf("Cannot create temporary file: %s", err)
	}
	defer os.Remove(file.Name())

	var opts struct {
		Value int `long:"value"`
	}

	opts.Value = 123

	p := NewParser(&opts, Default)
	ini := NewIniParser(p)

	err = ini.WriteFile(file.Name(), IniIncludeDefaults)
	if err != nil {
		t.Fatalf("Could not write ini file: %s", err)
	}

	found, err := ioutil.ReadFile(file.Name())
	if err != nil {
		t.Fatalf("Could not read written ini file: %s", err)
	}

	expected := "[Application Options]\nValue = 123\n\n"

	assertDiff(t, string(found), expected, "ini content")
}

func TestOverwriteRequiredOptions(t *testing.T) {
	var tests = []struct {
		args     []string
		expected []string
	}{
		{
			args: []string{"--value", "from CLI"},
			expected: []string{
				"from CLI",
				"from default",
			},
		},
		{
			args: []string{"--value", "from CLI", "--default", "from CLI"},
			expected: []string{
				"from CLI",
				"from CLI",
			},
		},
		{
			args: []string{"--config", "no file name"},
			expected: []string{
				"from INI",
				"from INI",
			},
		},
		{
			args: []string{"--value", "from CLI before", "--default", "from CLI before", "--config", "no file name"},
			expected: []string{
				"from INI",
				"from INI",
			},
		},
		{
			args: []string{"--value", "from CLI before", "--default", "from CLI before", "--config", "no file name", "--value", "from CLI after", "--default", "from CLI after"},
			expected: []string{
				"from CLI after",
				"from CLI after",
			},
		},
	}

	for _, test := range tests {
		var opts struct {
			Config  func(s string) error `long:"config" no-ini:"true"`
			Value   string               `long:"value" required:"true"`
			Default string               `long:"default" required:"true" default:"from default"`
		}

		p := NewParser(&opts, Default)

		opts.Config = func(s string) error {
			ini := NewIniParser(p)

			return ini.Parse(bytes.NewBufferString("value = from INI\ndefault = from INI"))
		}

		_, err := p.ParseArgs(test.args)
		if err != nil {
			t.Fatalf("Unexpected error %s with args %+v", err, test.args)
		}

		if opts.Value != test.expected[0] {
			t.Fatalf("Expected Value to be \"%s\" but was \"%s\" with args %+v", test.expected[0], opts.Value, test.args)
		}

		if opts.Default != test.expected[1] {
			t.Fatalf("Expected Default to be \"%s\" but was \"%s\" with args %+v", test.expected[1], opts.Default, test.args)
		}
	}
}

func TestIniOverwriteOptions(t *testing.T) {
	var tests = []struct {
		args     []string
		expected string
		toggled  bool
	}{
		{
			args:     []string{},
			expected: "from default",
		},
		{
			args:     []string{"--value", "from CLI"},
			expected: "from CLI",
		},
		{
			args:     []string{"--config", "no file name"},
			expected: "from INI",
			toggled:  true,
		},
		{
			args:     []string{"--value", "from CLI before", "--config", "no file name"},
			expected: "from CLI before",
			toggled:  true,
		},
		{
			args:     []string{"--config", "no file name", "--value", "from CLI after"},
			expected: "from CLI after",
			toggled:  true,
		},
		{
			args:     []string{"--toggle"},
			toggled:  true,
			expected: "from default",
		},
	}

	for _, test := range tests {
		var opts struct {
			Config string `long:"config" no-ini:"true"`
			Value  string `long:"value" default:"from default"`
			Toggle bool   `long:"toggle"`
		}

		p := NewParser(&opts, Default)

		_, err := p.ParseArgs(test.args)
		if err != nil {
			t.Fatalf("Unexpected error %s with args %+v", err, test.args)
		}

		if opts.Config != "" {
			inip := NewIniParser(p)
			inip.ParseAsDefaults = true

			err = inip.Parse(bytes.NewBufferString("value = from INI\ntoggle = true"))
			if err != nil {
				t.Fatalf("Unexpected error %s with args %+v", err, test.args)
			}
		}

		if opts.Value != test.expected {
			t.Fatalf("Expected Value to be \"%s\" but was \"%s\" with args %+v", test.expected, opts.Value, test.args)
		}

		if opts.Toggle != test.toggled {
			t.Fatalf("Expected Toggle to be \"%v\" but was \"%v\" with args %+v", test.toggled, opts.Toggle, test.args)
		}

	}
}
