package flag

import (
	"testing"

	"github.com/spf13/cobra"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestOptionalBoolSet(t *testing.T) {
	for _, c := range []struct {
		input    string
		accepted bool
		value    bool
	}{
		// Valid inputs documented for strconv.ParseBool == flag.BoolVar
		{"1", true, true},
		{"t", true, true},
		{"T", true, true},
		{"TRUE", true, true},
		{"true", true, true},
		{"True", true, true},
		{"0", true, false},
		{"f", true, false},
		{"F", true, false},
		{"FALSE", true, false},
		{"false", true, false},
		{"False", true, false},
		// A few invalid inputs
		{"", false, false},
		{"yes", false, false},
		{"no", false, false},
		{"2", false, false},
	} {
		var ob OptionalBool
		v := internalNewOptionalBoolValue(&ob)
		require.False(t, ob.Present())
		err := v.Set(c.input)
		if c.accepted {
			assert.NoError(t, err, c.input)
			assert.Equal(t, c.value, ob.Value())
		} else {
			assert.Error(t, err, c.input)
			assert.False(t, ob.Present()) // Just to be extra paranoid.
		}
	}

	// Nothing actually explicitly says that .Set() is never called when the flag is not present on the command line;
	// so, check that it is not being called, at least in the straightforward case (it's not possible to test that it
	// is not called in any possible situation).
	var globalOB, commandOB OptionalBool
	actionRun := false
	app := &cobra.Command{
		Use: "app",
	}
	OptionalBoolFlag(app.PersistentFlags(), &globalOB, "global-OB", "")
	cmd := &cobra.Command{
		Use: "cmd",
		RunE: func(_ *cobra.Command, args []string) error {
			assert.False(t, globalOB.Present())
			assert.False(t, commandOB.Present())
			actionRun = true
			return nil
		},
	}
	OptionalBoolFlag(cmd.Flags(), &commandOB, "command-OB", "")
	app.AddCommand(cmd)
	app.SetArgs([]string{"cmd"})
	err := app.Execute()
	require.NoError(t, err)
	assert.True(t, actionRun)
}

func TestOptionalBoolString(t *testing.T) {
	for _, c := range []struct {
		input    OptionalBool
		expected string
	}{
		{OptionalBool{present: true, value: true}, "true"},
		{OptionalBool{present: true, value: false}, "false"},
		{OptionalBool{present: false, value: true}, ""},
		{OptionalBool{present: false, value: false}, ""},
	} {
		var ob OptionalBool
		v := internalNewOptionalBoolValue(&ob)
		ob = c.input
		res := v.String()
		assert.Equal(t, c.expected, res)
	}
}

func TestOptionalBoolIsBoolFlag(t *testing.T) {
	// IsBoolFlag means that the argument value must either be part of the same argument, with =;
	// if there is no =, the value is set to true.
	// This differs form other flags, where the argument is required and may be either separated with = or supplied in the next argument.
	for _, c := range []struct {
		input        []string
		expectedOB   OptionalBool
		expectedArgs []string
	}{
		{[]string{"1", "2"}, OptionalBool{present: false}, []string{"1", "2"}},                                       // Flag not present
		{[]string{"--OB=true", "1", "2"}, OptionalBool{present: true, value: true}, []string{"1", "2"}},              // --OB=true
		{[]string{"--OB=false", "1", "2"}, OptionalBool{present: true, value: false}, []string{"1", "2"}},            // --OB=false
		{[]string{"--OB", "true", "1", "2"}, OptionalBool{present: true, value: true}, []string{"true", "1", "2"}},   // --OB true
		{[]string{"--OB", "false", "1", "2"}, OptionalBool{present: true, value: true}, []string{"false", "1", "2"}}, // --OB false
	} {
		var ob OptionalBool
		actionRun := false
		app := &cobra.Command{Use: "app"}
		cmd := &cobra.Command{
			Use: "cmd",
			RunE: func(_ *cobra.Command, args []string) error {
				assert.Equal(t, c.expectedOB, ob)     // nolint
				assert.Equal(t, c.expectedArgs, args) //nolint
				actionRun = true
				return nil
			},
		}
		OptionalBoolFlag(cmd.Flags(), &ob, "OB", "")
		app.AddCommand(cmd)

		app.SetArgs(append([]string{"cmd"}, c.input...))
		err := app.Execute()
		require.NoError(t, err)
		assert.True(t, actionRun)
	}
}

func TestOptionalStringSet(t *testing.T) {
	// Really just a smoke test, but differentiating between not present and empty.
	for _, c := range []string{"", "hello"} {
		var os OptionalString
		v := NewOptionalStringValue(&os)
		require.False(t, os.Present())
		err := v.Set(c)
		assert.NoError(t, err, c)
		assert.Equal(t, c, os.Value())
	}

	// Nothing actually explicitly says that .Set() is never called when the flag is not present on the command line;
	// so, check that it is not being called, at least in the straightforward case (it's not possible to test that it
	// is not called in any possible situation).
	var globalOS, commandOS OptionalString
	actionRun := false
	app := &cobra.Command{
		Use: "app",
	}
	app.PersistentFlags().Var(NewOptionalStringValue(&globalOS), "global-OS", "")
	cmd := &cobra.Command{
		Use: "cmd",
		RunE: func(_ *cobra.Command, args []string) error {
			assert.False(t, globalOS.Present())
			assert.False(t, commandOS.Present())
			actionRun = true
			return nil
		},
	}
	cmd.Flags().Var(NewOptionalStringValue(&commandOS), "command-OS", "")
	app.AddCommand(cmd)
	app.SetArgs([]string{"cmd"})
	err := app.Execute()
	require.NoError(t, err)
	assert.True(t, actionRun)
}

func TestOptionalStringString(t *testing.T) {
	for _, c := range []struct {
		input    OptionalString
		expected string
	}{
		{OptionalString{present: true, value: "hello"}, "hello"},
		{OptionalString{present: true, value: ""}, ""},
		{OptionalString{present: false, value: "hello"}, ""},
		{OptionalString{present: false, value: ""}, ""},
	} {
		var os OptionalString
		v := NewOptionalStringValue(&os)
		os = c.input
		res := v.String()
		assert.Equal(t, c.expected, res)
	}
}

func TestOptionalStringIsBoolFlag(t *testing.T) {
	// NOTE: OptionalStringValue does not implement IsBoolFlag!
	// IsBoolFlag means that the argument value must either be part of the same argument, with =;
	// if there is no =, the value is set to true.
	// This differs form other flags, where the argument is required and may be either separated with = or supplied in the next argument.
	for _, c := range []struct {
		input        []string
		expectedOS   OptionalString
		expectedArgs []string
	}{
		{[]string{"1", "2"}, OptionalString{present: false}, []string{"1", "2"}},                                 // Flag not present
		{[]string{"--OS=hello", "1", "2"}, OptionalString{present: true, value: "hello"}, []string{"1", "2"}},    // --OS=true
		{[]string{"--OS=", "1", "2"}, OptionalString{present: true, value: ""}, []string{"1", "2"}},              // --OS=false
		{[]string{"--OS", "hello", "1", "2"}, OptionalString{present: true, value: "hello"}, []string{"1", "2"}}, // --OS true
		{[]string{"--OS", "", "1", "2"}, OptionalString{present: true, value: ""}, []string{"1", "2"}},           // --OS false
	} {
		var os OptionalString
		actionRun := false
		app := &cobra.Command{
			Use: "app",
		}
		cmd := &cobra.Command{
			Use: "cmd",
			RunE: func(cmd *cobra.Command, args []string) error {
				assert.Equal(t, c.expectedOS, os)     // nolint
				assert.Equal(t, c.expectedArgs, args) // nolint
				actionRun = true
				return nil
			},
		}
		cmd.Flags().Var(NewOptionalStringValue(&os), "OS", "")
		app.AddCommand(cmd)
		app.SetArgs(append([]string{"cmd"}, c.input...))
		err := app.Execute()
		require.NoError(t, err)
		assert.True(t, actionRun)
	}
}
