package fs

import (
	"encoding/json"
	"fmt"
	"os"
	"testing"

	"github.com/rclone/rclone/fs/config/configmap"
	"github.com/spf13/pflag"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// Check it satisfies the interface
var _ pflag.Value = (*Option)(nil)

func TestOption(t *testing.T) {
	d := &Option{
		Name:  "potato",
		Value: SizeSuffix(17 << 20),
	}
	assert.Equal(t, "17Mi", d.String())
	assert.Equal(t, "SizeSuffix", d.Type())
	err := d.Set("18M")
	assert.NoError(t, err)
	assert.Equal(t, SizeSuffix(18<<20), d.Value)
	err = d.Set("sdfsdf")
	assert.Error(t, err)
}

// Test options
var (
	nouncOption = Option{
		Name: "nounc",
	}
	copyLinksOption = Option{
		Name:     "copy_links",
		Default:  false,
		NoPrefix: true,
		ShortOpt: "L",
		Advanced: true,
	}
	caseInsensitiveOption = Option{
		Name:     "case_insensitive",
		Default:  false,
		Value:    true,
		Advanced: true,
	}
	testOptions = Options{nouncOption, copyLinksOption, caseInsensitiveOption}
)

func TestOptionsSetValues(t *testing.T) {
	assert.Nil(t, testOptions[0].Default)
	assert.Equal(t, false, testOptions[1].Default)
	assert.Equal(t, false, testOptions[2].Default)
	testOptions.setValues()
	assert.Equal(t, "", testOptions[0].Default)
	assert.Equal(t, false, testOptions[1].Default)
	assert.Equal(t, false, testOptions[2].Default)
}

func TestOptionsGet(t *testing.T) {
	opt := testOptions.Get("copy_links")
	assert.Equal(t, &copyLinksOption, opt)
	opt = testOptions.Get("not_found")
	assert.Nil(t, opt)
}

func TestOptionsOveridden(t *testing.T) {
	m := configmap.New()
	m1 := configmap.Simple{
		"nounc":      "m1",
		"copy_links": "m1",
	}
	m.AddGetter(m1, configmap.PriorityNormal)
	m2 := configmap.Simple{
		"nounc":            "m2",
		"case_insensitive": "m2",
	}
	m.AddGetter(m2, configmap.PriorityConfig)
	m3 := configmap.Simple{
		"nounc": "m3",
	}
	m.AddGetter(m3, configmap.PriorityDefault)
	got := testOptions.Overridden(m)
	assert.Equal(t, configmap.Simple{
		"copy_links": "m1",
		"nounc":      "m1",
	}, got)
}

func TestOptionsNonDefault(t *testing.T) {
	m := configmap.Simple{}
	got := testOptions.NonDefault(m)
	assert.Equal(t, configmap.Simple{}, got)

	m["case_insensitive"] = "false"
	got = testOptions.NonDefault(m)
	assert.Equal(t, configmap.Simple{}, got)

	m["case_insensitive"] = "true"
	got = testOptions.NonDefault(m)
	assert.Equal(t, configmap.Simple{"case_insensitive": "true"}, got)
}

func TestOptionMarshalJSON(t *testing.T) {
	out, err := json.MarshalIndent(&caseInsensitiveOption, "", "")
	assert.NoError(t, err)
	require.Equal(t, `{
"Name": "case_insensitive",
"FieldName": "",
"Help": "",
"Default": false,
"Value": true,
"Hide": 0,
"Required": false,
"IsPassword": false,
"NoPrefix": false,
"Advanced": true,
"Exclusive": false,
"Sensitive": false,
"DefaultStr": "false",
"ValueStr": "true",
"Type": "bool"
}`, string(out))
}

func TestOptionGetValue(t *testing.T) {
	assert.Equal(t, "", nouncOption.GetValue())
	assert.Equal(t, false, copyLinksOption.GetValue())
	assert.Equal(t, true, caseInsensitiveOption.GetValue())
}

func TestOptionString(t *testing.T) {
	assert.Equal(t, "", nouncOption.String())
	assert.Equal(t, "false", copyLinksOption.String())
	assert.Equal(t, "true", caseInsensitiveOption.String())
}

func TestOptionStringStringArray(t *testing.T) {
	opt := Option{
		Name:    "string_array",
		Default: []string(nil),
	}
	assert.Equal(t, "", opt.String())
	opt.Default = []string{}
	assert.Equal(t, "", opt.String())
	opt.Default = []string{"a", "b"}
	assert.Equal(t, "a,b", opt.String())
	opt.Default = []string{"hello, world!", "goodbye, world!"}
	assert.Equal(t, `"hello, world!","goodbye, world!"`, opt.String())
}

func TestOptionStringSizeSuffix(t *testing.T) {
	opt := Option{
		Name:    "size_suffix",
		Default: SizeSuffix(0),
	}
	assert.Equal(t, "0", opt.String())
	opt.Default = SizeSuffix(-1)
	assert.Equal(t, "off", opt.String())
	opt.Default = SizeSuffix(100)
	assert.Equal(t, "100B", opt.String())
	opt.Default = SizeSuffix(1024)
	assert.Equal(t, "1Ki", opt.String())
}

func TestOptionSet(t *testing.T) {
	o := caseInsensitiveOption
	assert.Equal(t, true, o.Value)
	err := o.Set("FALSE")
	assert.NoError(t, err)
	assert.Equal(t, false, o.Value)

	o = copyLinksOption
	assert.Equal(t, nil, o.Value)
	err = o.Set("True")
	assert.NoError(t, err)
	assert.Equal(t, true, o.Value)

	err = o.Set("INVALID")
	assert.Error(t, err)
	assert.Equal(t, true, o.Value)
}

func TestOptionType(t *testing.T) {
	assert.Equal(t, "string", nouncOption.Type())
	assert.Equal(t, "bool", copyLinksOption.Type())
	assert.Equal(t, "bool", caseInsensitiveOption.Type())
}

func TestOptionFlagName(t *testing.T) {
	assert.Equal(t, "local-nounc", nouncOption.FlagName("local"))
	assert.Equal(t, "copy-links", copyLinksOption.FlagName("local"))
	assert.Equal(t, "local-case-insensitive", caseInsensitiveOption.FlagName("local"))
}

func TestOptionEnvVarName(t *testing.T) {
	assert.Equal(t, "RCLONE_LOCAL_NOUNC", nouncOption.EnvVarName("local"))
	assert.Equal(t, "RCLONE_LOCAL_COPY_LINKS", copyLinksOption.EnvVarName("local"))
	assert.Equal(t, "RCLONE_LOCAL_CASE_INSENSITIVE", caseInsensitiveOption.EnvVarName("local"))
}

func TestOptionGetters(t *testing.T) {
	// Set up env vars
	envVars := [][2]string{
		{"RCLONE_CONFIG_LOCAL_POTATO_PIE", "yes"},
		{"RCLONE_COPY_LINKS", "TRUE"},
		{"RCLONE_LOCAL_NOUNC", "NOUNC"},
	}
	for _, ev := range envVars {
		assert.NoError(t, os.Setenv(ev[0], ev[1]))
	}
	defer func() {
		for _, ev := range envVars {
			assert.NoError(t, os.Unsetenv(ev[0]))
		}
	}()

	oldConfigFileGet := ConfigFileGet
	ConfigFileGet = func(section, key string) (string, bool) {
		if section == "sausage" && key == "key1" {
			return "value1", true
		}
		return "", false
	}
	defer func() {
		ConfigFileGet = oldConfigFileGet
	}()

	// set up getters

	// A configmap.Getter to read from the environment RCLONE_CONFIG_backend_option_name
	configEnvVarsGetter := configEnvVars("local")

	// A configmap.Getter to read from the environment RCLONE_option_name
	optionEnvVarsGetter := optionEnvVars{"local", testOptions}

	// A configmap.Getter to read either the default value or the set
	// value from the RegInfo.Options
	regInfoValuesGetterFalse := &regInfoValues{
		options:    testOptions,
		useDefault: false,
	}
	regInfoValuesGetterTrue := &regInfoValues{
		options:    testOptions,
		useDefault: true,
	}

	// A configmap.Setter to read from the config file
	configFileGetter := getConfigFile("sausage")

	for i, test := range []struct {
		get       configmap.Getter
		key       string
		wantValue string
		wantOk    bool
	}{
		{configEnvVarsGetter, "not_found", "", false},
		{configEnvVarsGetter, "potato_pie", "yes", true},
		{optionEnvVarsGetter, "not_found", "", false},
		{optionEnvVarsGetter, "copy_links", "TRUE", true},
		{optionEnvVarsGetter, "nounc", "NOUNC", true},
		{optionEnvVarsGetter, "case_insensitive", "", false},
		{regInfoValuesGetterFalse, "not_found", "", false},
		{regInfoValuesGetterFalse, "case_insensitive", "true", true},
		{regInfoValuesGetterFalse, "copy_links", "", false},
		{regInfoValuesGetterTrue, "not_found", "", false},
		{regInfoValuesGetterTrue, "case_insensitive", "true", true},
		{regInfoValuesGetterTrue, "copy_links", "false", true},
		{configFileGetter, "not_found", "", false},
		{configFileGetter, "key1", "value1", true},
	} {
		what := fmt.Sprintf("%d: %+v: %q", i, test.get, test.key)
		gotValue, gotOk := test.get.Get(test.key)
		assert.Equal(t, test.wantValue, gotValue, what)
		assert.Equal(t, test.wantOk, gotOk, what)
	}

}

func TestOptionsNonDefaultRC(t *testing.T) {
	type cfg struct {
		X string `config:"x"`
		Y int    `config:"y"`
	}
	c := &cfg{X: "a", Y: 6}
	opts := Options{
		{Name: "x", Default: "a"}, // at default, should be omitted
		{Name: "y", Default: 5},   // non-default, should be included
	}

	got, err := opts.NonDefaultRC(c)
	require.NoError(t, err)
	require.Equal(t, map[string]any{"Y": 6}, got)
}

func TestOptionsNonDefaultRCMissingKey(t *testing.T) {
	type cfg struct {
		X string `config:"x"`
	}
	c := &cfg{X: "a"}
	// Options refers to a key not present in the struct -> expect error
	opts := Options{{Name: "missing", Default: ""}}
	_, err := opts.NonDefaultRC(c)
	assert.ErrorContains(t, err, "not found")
}
