// These are in an external package because we need to import configfile
//
// Internal tests are in ui_internal_test.go

package config_test

import (
	"context"
	"fmt"
	"os"
	"testing"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/config"
	"github.com/rclone/rclone/fs/config/configfile"
	"github.com/rclone/rclone/fs/config/obscure"
	"github.com/rclone/rclone/fs/rc"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var simpleOptions = []fs.Option{{
	Name:       "bool",
	Default:    false,
	IsPassword: false,
}, {
	Name:       "pass",
	Default:    "",
	IsPassword: true,
}}

func testConfigFile(t *testing.T, options []fs.Option, configFileName string) func() {
	ctx := context.Background()
	ci := fs.GetConfig(ctx)
	config.ClearConfigPassword()
	_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
	_ = os.Unsetenv("RCLONE_CONFIG_PASS")
	// create temp config file
	tempFile, err := os.CreateTemp("", configFileName)
	assert.NoError(t, err)
	path := tempFile.Name()
	assert.NoError(t, tempFile.Close())

	// temporarily adapt configuration
	oldOsStdout := os.Stdout
	oldConfigPath := config.GetConfigPath()
	oldConfig := *ci
	oldConfigFile := config.Data()
	oldReadLine := config.ReadLine
	oldPassword := config.Password
	os.Stdout = nil
	assert.NoError(t, config.SetConfigPath(path))
	ci = &fs.ConfigInfo{}

	configfile.Install()
	assert.Equal(t, []string{}, config.Data().GetSectionList())

	// Fake a filesystem/backend
	backendName := "config_test_remote"
	if regInfo, _ := fs.Find(backendName); regInfo != nil {
		regInfo.Options = options
	} else {
		fs.Register(&fs.RegInfo{
			Name:    backendName,
			Options: options,
		})
	}

	// Undo the above (except registered backend, unfortunately)
	return func() {
		err := os.Remove(path)
		assert.NoError(t, err)

		os.Stdout = oldOsStdout
		assert.NoError(t, config.SetConfigPath(oldConfigPath))
		config.ReadLine = oldReadLine
		config.Password = oldPassword
		*ci = oldConfig
		config.SetData(oldConfigFile)

		_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
		_ = os.Unsetenv("RCLONE_CONFIG_PASS")
	}
}

// makeReadLine makes a simple readLine which returns a fixed list of
// strings
func makeReadLine(answers []string) func() string {
	i := 0
	return func() string {
		i = i + 1
		return answers[i-1]
	}
}

func TestCRUD(t *testing.T) {
	defer testConfigFile(t, simpleOptions, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"true",               // bool value
		"y",                  // type my own password
		"secret",             // password
		"secret",             // repeat
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "true", config.FileGet("test", "bool"))
	assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("test", "pass")))

	// normal rename, test → asdf
	config.ReadLine = makeReadLine([]string{
		"asdf",
		"asdf",
		"asdf",
	})
	config.RenameRemote("test")

	assert.Equal(t, []string{"asdf"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("asdf", "type"))
	assert.Equal(t, "true", config.FileGet("asdf", "bool"))
	assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("asdf", "pass")))

	// delete remote
	config.DeleteRemote("asdf")
	assert.Equal(t, []string{}, config.Data().GetSectionList())
}

func TestChooseOption(t *testing.T) {
	defer testConfigFile(t, simpleOptions, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"false",              // bool value
		"x",                  // bad choice
		"g",                  // generate password
		"1024",               // very big
		"y",                  // password OK
		"y",                  // looks good, save
	})
	config.Password = func(bits int) (string, error) {
		assert.Equal(t, 1024, bits)
		return "not very random password", nil
	}
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, "", config.FileGet("test", "bool")) // this is the default now
	assert.Equal(t, "not very random password", obscure.MustReveal(config.FileGet("test", "pass")))

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"true",               // bool value
		"n",                  // not required
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, "true", config.FileGet("test", "bool"))
	assert.Equal(t, "", config.FileGet("test", "pass"))
}

func TestNewRemoteName(t *testing.T) {
	defer testConfigFile(t, simpleOptions, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"true",               // bool value
		"n",                  // not required
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	config.ReadLine = makeReadLine([]string{
		"test",           // already exists
		"",               // empty string not allowed
		"bad@characters", // bad characters
		"newname",        // OK
	})

	assert.Equal(t, "newname", config.NewRemoteName())
}

func TestCreateUpdatePasswordRemote(t *testing.T) {
	ctx := context.Background()
	defer testConfigFile(t, simpleOptions, "update.conf")()

	for _, doObscure := range []bool{false, true} {
		for _, noObscure := range []bool{false, true} {
			if doObscure && noObscure {
				break
			}
			t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
				opt := config.UpdateRemoteOpt{
					Obscure:   doObscure,
					NoObscure: noObscure,
				}
				_, err := config.CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
					"bool": true,
					"pass": "potato",
				}, opt)
				require.NoError(t, err)

				assert.Equal(t, []string{"test2"}, config.Data().GetSectionList())
				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
				assert.Equal(t, "true", config.FileGet("test2", "bool"))
				gotPw := config.FileGet("test2", "pass")
				if !noObscure {
					gotPw = obscure.MustReveal(gotPw)
				}
				assert.Equal(t, "potato", gotPw)

				wantPw := obscure.MustObscure("potato2")
				_, err = config.UpdateRemote(ctx, "test2", rc.Params{
					"bool":  false,
					"pass":  wantPw,
					"spare": "spare",
				}, opt)
				require.NoError(t, err)

				assert.Equal(t, []string{"test2"}, config.Data().GetSectionList())
				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
				assert.Equal(t, "false", config.FileGet("test2", "bool"))
				gotPw = config.FileGet("test2", "pass")
				if doObscure {
					gotPw = obscure.MustReveal(gotPw)
				}
				assert.Equal(t, wantPw, gotPw)

				require.NoError(t, config.PasswordRemote(ctx, "test2", rc.Params{
					"pass": "potato3",
				}))

				assert.Equal(t, []string{"test2"}, config.Data().GetSectionList())
				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
				assert.Equal(t, "false", config.FileGet("test2", "bool"))
				assert.Equal(t, "potato3", obscure.MustReveal(config.FileGet("test2", "pass")))
			})
		}
	}
}

func TestDefaultRequired(t *testing.T) {
	// By default options are optional (sic), regardless if a default value is defined.
	// Setting Required=true means empty string is no longer allowed, except when
	// a default value is set: Default value means empty string is always allowed!
	options := []fs.Option{{
		Name:     "string_required",
		Required: true,
	}, {
		Name:    "string_default",
		Default: "AAA",
	}, {
		Name:     "string_required_default",
		Default:  "BBB",
		Required: true,
	}}

	defer testConfigFile(t, options, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"111",                // string_required
		"222",                // string_default
		"333",                // string_required_default
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "111", config.FileGet("test", "string_required"))
	assert.Equal(t, "222", config.FileGet("test", "string_default"))
	assert.Equal(t, "333", config.FileGet("test", "string_required_default"))

	// delete remote
	config.DeleteRemote("test")
	assert.Equal(t, []string{}, config.Data().GetSectionList())

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"",                   // string_required - invalid (empty string not allowed)
		"111",                // string_required - valid
		"",                   // string_default (empty string allowed, means use default)
		"",                   // string_required_default (empty string allowed, means use default)
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "111", config.FileGet("test", "string_required"))
	assert.Equal(t, "", config.FileGet("test", "string_default"))
	assert.Equal(t, "", config.FileGet("test", "string_required_default"))
}

func TestMultipleChoice(t *testing.T) {
	// Multiple-choice options can be set to the number of a predefined choice, or
	// its text. Unless Exclusive=true, tested later, any free text input is accepted.
	//
	// By default options are optional, regardless if a default value is defined.
	// Setting Required=true means empty string is no longer allowed, except when
	// a default value is set: Default value means empty string is always allowed!
	options := []fs.Option{{
		Name: "multiple_choice",
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}, {
		Name:     "multiple_choice_required",
		Required: true,
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}, {
		Name:    "multiple_choice_default",
		Default: "BBB",
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}, {
		Name:     "multiple_choice_required_default",
		Required: true,
		Default:  "BBB",
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}}

	defer testConfigFile(t, options, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"3",                  // multiple_choice
		"3",                  // multiple_choice_required
		"3",                  // multiple_choice_default
		"3",                  // multiple_choice_required_default
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice"))
	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_required"))
	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_default"))
	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_required_default"))

	// delete remote
	config.DeleteRemote("test")
	assert.Equal(t, []string{}, config.Data().GetSectionList())

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"XXX",                // multiple_choice
		"XXX",                // multiple_choice_required
		"XXX",                // multiple_choice_default
		"XXX",                // multiple_choice_required_default
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice"))
	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required"))
	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_default"))
	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required_default"))

	// delete remote
	config.DeleteRemote("test")
	assert.Equal(t, []string{}, config.Data().GetSectionList())

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"",                   // multiple_choice (empty string allowed)
		"",                   // multiple_choice_required - invalid (empty string not allowed)
		"XXX",                // multiple_choice_required - valid (value not restricted to examples)
		"",                   // multiple_choice_default (empty string allowed)
		"",                   // multiple_choice_required_default (required does nothing when default is set)
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "", config.FileGet("test", "multiple_choice"))
	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required"))
	assert.Equal(t, "", config.FileGet("test", "multiple_choice_default"))
	assert.Equal(t, "", config.FileGet("test", "multiple_choice_required_default"))
}

func TestMultipleChoiceExclusive(t *testing.T) {
	// Setting Exclusive=true on multiple-choice option means any input
	// value must be from the predefined list, but empty string is allowed.
	// Setting a default value makes no difference.
	options := []fs.Option{{
		Name:      "multiple_choice_exclusive",
		Exclusive: true,
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}, {
		Name:      "multiple_choice_exclusive_default",
		Exclusive: true,
		Default:   "CCC",
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}}

	defer testConfigFile(t, options, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"XXX",                // multiple_choice_exclusive - invalid (not a value from examples)
		"",                   // multiple_choice_exclusive - valid (empty string allowed)
		"YYY",                // multiple_choice_exclusive_default - invalid (not a value from examples)
		"",                   // multiple_choice_exclusive_default - valid (empty string allowed)
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive"))
	assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive_default"))
}

func TestMultipleChoiceExclusiveRequired(t *testing.T) {
	// Setting Required=true together with Exclusive=true on multiple-choice option
	// means empty string is no longer allowed, except when a default value is set
	// (default value means empty string is always allowed).
	options := []fs.Option{{
		Name:      "multiple_choice_exclusive_required",
		Exclusive: true,
		Required:  true,
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}, {
		Name:      "multiple_choice_exclusive_required_default",
		Exclusive: true,
		Required:  true,
		Default:   "CCC",
		Examples: []fs.OptionExample{{
			Value: "AAA",
			Help:  "This is value AAA",
		}, {
			Value: "BBB",
			Help:  "This is value BBB",
		}, {
			Value: "CCC",
			Help:  "This is value CCC",
		}},
	}}

	defer testConfigFile(t, options, "crud.conf")()
	ctx := context.Background()

	// script for creating remote
	config.ReadLine = makeReadLine([]string{
		"config_test_remote", // type
		"XXX",                // multiple_choice_exclusive_required - invalid (not a value from examples)
		"",                   // multiple_choice_exclusive_required - invalid (empty string not allowed)
		"CCC",                // multiple_choice_exclusive_required - valid
		"XXX",                // multiple_choice_exclusive_required_default - invalid (not a value from examples)
		"",                   // multiple_choice_exclusive_required_default - valid (empty string allowed)
		"y",                  // looks good, save
	})
	require.NoError(t, config.NewRemote(ctx, "test"))

	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_exclusive_required"))
	assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive_required_default"))
}
