// Copyright © 2014 Steve Francia <spf@spf13.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

package viper

import (
	"bytes"
	"encoding/json"
	"io"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/go-viper/mapstructure/v2"
	"github.com/sagikazarmark/locafero"
	"github.com/spf13/afero"
	"github.com/spf13/cast"
	"github.com/spf13/pflag"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/spf13/viper/internal/testutil"
)

// var yamlExample = []byte(`Hacker: true
// name: steve
// hobbies:
//     - skateboarding
//     - snowboarding
//     - go
// clothing:
//     jacket: leather
//     trousers: denim
//     pants:
//         size: large
// age: 35
// eyes : brown
// beard: true
// `)

var yamlExampleWithExtras = []byte(`Existing: true
Bogus: true
`)

type testUnmarshalExtra struct {
	Existing bool
}

var tomlExample = []byte(`
title = "TOML Example"

[owner]
organization = "MongoDB"
Bio = "MongoDB Chief Developer Advocate & Hacker at Large"
dob = 1979-05-27T07:32:00Z # First class dates? Why not?`)

var dotenvExample = []byte(`
TITLE_DOTENV="DotEnv Example"
TYPE_DOTENV=donut
NAME_DOTENV=Cake`)

var jsonExample = []byte(`{
"id": "0001",
"type": "donut",
"name": "Cake",
"ppu": 0.55,
"batters": {
        "batter": [
                { "type": "Regular" },
                { "type": "Chocolate" },
                { "type": "Blueberry" },
                { "type": "Devil's Food" }
            ]
    }
}`)

var remoteExample = []byte(`{
"id":"0002",
"type":"cronut",
"newkey":"remote"
}`)

func initConfigs(v *Viper) {
	var r io.Reader
	v.SetConfigType("yaml")
	r = bytes.NewReader(yamlExample)
	v.unmarshalReader(r, v.config)

	v.SetConfigType("json")
	r = bytes.NewReader(jsonExample)
	v.unmarshalReader(r, v.config)

	v.SetConfigType("toml")
	r = bytes.NewReader(tomlExample)
	v.unmarshalReader(r, v.config)

	v.SetConfigType("env")
	r = bytes.NewReader(dotenvExample)
	v.unmarshalReader(r, v.config)

	v.SetConfigType("json")
	remote := bytes.NewReader(remoteExample)
	v.unmarshalReader(remote, v.kvstore)
}

func initConfig(typ, config string, v *Viper) {
	v.SetConfigType(typ)
	r := strings.NewReader(config)

	if err := v.unmarshalReader(r, v.config); err != nil {
		panic(err)
	}
}

// initDirs makes directories for testing.
func initDirs(t *testing.T) (string, string) {
	var (
		testDirs = []string{`a a`, `b`, `C_`}
		config   = `improbable`
	)

	if runtime.GOOS != "windows" {
		testDirs = append(testDirs, `d\d`)
	}

	root := t.TempDir()

	for _, dir := range testDirs {
		innerDir := filepath.Join(root, dir)
		err := os.Mkdir(innerDir, 0o750)
		require.NoError(t, err)

		err = os.WriteFile(
			filepath.Join(innerDir, config+".toml"),
			[]byte(`key = "value is `+dir+`"`+"\n"),
			0o640)
		require.NoError(t, err)
	}

	return root, config
}

// stubs for PFlag Values.
type stringValue string

func newStringValue(val string, p *string) *stringValue {
	*p = val
	return (*stringValue)(p)
}

func (s *stringValue) Set(val string) error {
	*s = stringValue(val)
	return nil
}

func (s *stringValue) Type() string {
	return "string"
}

func (s *stringValue) String() string {
	return string(*s)
}

func TestGetConfigFile(t *testing.T) {
	t.Run("config file set", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/etc/viper")
		v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename)
		assert.NoError(t, err)
	})

	t.Run("find file", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/etc/viper")

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename)
		assert.NoError(t, err)
	})

	t.Run("find files only", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/config"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/config/config.yaml"))
		require.NoError(t, err)

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/etc")
		v.AddConfigPath("/etc/config")

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/etc/config/config.yaml"), filename)
		assert.NoError(t, err)
	})

	t.Run("precedence", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/home/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/home/viper/config.zml"))
		require.NoError(t, err)

		err = fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.bml"))
		require.NoError(t, err)

		err = fs.Mkdir(testutil.AbsFilePath(t, "/var/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/var/viper/config.yaml"))
		require.NoError(t, err)

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/home/viper")
		v.AddConfigPath("/etc/viper")
		v.AddConfigPath("/var/viper")

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/var/viper/config.yaml"), filename)
		assert.NoError(t, err)
	})

	t.Run("without extension", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"))
		require.NoError(t, err)

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/etc/viper")
		v.SetConfigName(".dotfilenoext")
		v.SetConfigType("yaml")

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"), filename)
		assert.NoError(t, err)
	})

	t.Run("without extension and config type", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"))
		require.NoError(t, err)

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/etc/viper")
		v.SetConfigName(".dotfilenoext")

		_, err = v.getConfigFile()
		// unless config type is set, files without extension
		// are not considered
		assert.Error(t, err)
	})

	t.Run("experimental finder", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		v := NewWithOptions(ExperimentalFinder())

		v.SetFs(fs)

		v.AddConfigPath("/etc/viper")

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), testutil.AbsFilePath(t, filename))
		assert.NoError(t, err)
	})

	t.Run("finder", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		finder := locafero.Finder{
			Paths: []string{testutil.AbsFilePath(t, "/etc/viper")},
			Names: locafero.NameWithExtensions("config", SupportedExts...),
			Type:  locafero.FileTypeFile,
		}

		v := NewWithOptions(WithFinder(finder))

		v.SetFs(fs)

		// These should be ineffective
		v.AddConfigPath("/etc/something_else")
		v.SetConfigName("not-config")

		filename, err := v.getConfigFile()
		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), testutil.AbsFilePath(t, filename))
		assert.NoError(t, err)
	})
}

func TestReadInConfig(t *testing.T) {
	t.Run("config file set", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir("/etc/viper", 0o777)
		require.NoError(t, err)

		file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		_, err = file.WriteString(`key: value`)
		require.NoError(t, err)

		file.Close()

		v := New()

		v.SetFs(fs)
		v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))

		err = v.ReadInConfig()
		require.NoError(t, err)

		assert.Equal(t, "value", v.Get("key"))
	})

	t.Run("find file", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		_, err = file.WriteString(`key: value`)
		require.NoError(t, err)

		file.Close()

		v := New()

		v.SetFs(fs)
		v.AddConfigPath("/etc/viper")

		err = v.ReadInConfig()
		require.NoError(t, err)

		assert.Equal(t, "value", v.Get("key"))
	})

	t.Run("find file with experimental finder", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		_, err = file.WriteString(`key: value`)
		require.NoError(t, err)

		file.Close()

		v := NewWithOptions(ExperimentalFinder())

		v.SetFs(fs)
		v.AddConfigPath("/etc/viper")

		err = v.ReadInConfig()
		require.NoError(t, err)

		assert.Equal(t, "value", v.Get("key"))
	})

	t.Run("find file using a finder", func(t *testing.T) {
		fs := afero.NewMemMapFs()

		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
		require.NoError(t, err)

		file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
		require.NoError(t, err)

		_, err = file.WriteString(`key: value`)
		require.NoError(t, err)

		file.Close()

		finder := locafero.Finder{
			Paths: []string{testutil.AbsFilePath(t, "/etc/viper")},
			Names: locafero.NameWithExtensions("config", SupportedExts...),
			Type:  locafero.FileTypeFile,
		}

		v := NewWithOptions(WithFinder(finder))

		v.SetFs(fs)

		// These should be ineffective
		v.AddConfigPath("/etc/something_else")
		v.SetConfigName("not-config")

		err = v.ReadInConfig()
		require.NoError(t, err)

		assert.Equal(t, "value", v.Get("key"))
	})
}

func TestDefault(t *testing.T) {
	v := New()
	v.SetDefault("age", 45)
	assert.Equal(t, 45, v.Get("age"))

	v.SetDefault("clothing.jacket", "slacks")
	assert.Equal(t, "slacks", v.Get("clothing.jacket"))

	v.SetConfigType("yaml")
	err := v.ReadConfig(bytes.NewBuffer(yamlExample))

	require.NoError(t, err)
	assert.Equal(t, "leather", v.Get("clothing.jacket"))
}

func TestUnmarshaling(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")
	r := bytes.NewReader(yamlExample)

	v.unmarshalReader(r, v.config)
	assert.True(t, v.InConfig("name"))
	assert.True(t, v.InConfig("clothing.jacket"))
	assert.False(t, v.InConfig("state"))
	assert.False(t, v.InConfig("clothing.hat"))
	assert.Equal(t, "steve", v.Get("name"))
	assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
	assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing"))
	assert.Equal(t, 35, v.Get("age"))
}

func TestUnmarshalExact(t *testing.T) {
	v := New()
	target := &testUnmarshalExtra{}
	v.SetConfigType("yaml")
	r := bytes.NewReader(yamlExampleWithExtras)
	v.ReadConfig(r)
	err := v.UnmarshalExact(target)
	assert.Error(t, err, "UnmarshalExact should error when populating a struct from a conf that contains unused fields")
}

func TestOverrides(t *testing.T) {
	v := New()
	v.Set("age", 40)
	assert.Equal(t, 40, v.Get("age"))
}

func TestDefaultPost(t *testing.T) {
	v := New()
	assert.NotEqual(t, "NYC", v.Get("state"))
	v.SetDefault("state", "NYC")
	assert.Equal(t, "NYC", v.Get("state"))
}

func TestAliases(t *testing.T) {
	v := New()
	v.Set("age", 40)
	v.RegisterAlias("years", "age")
	assert.Equal(t, 40, v.Get("years"))
	v.Set("years", 45)
	assert.Equal(t, 45, v.Get("age"))
}

func TestAliasInConfigFile(t *testing.T) {
	v := New()

	v.SetConfigType("yaml")

	// Read the YAML data into Viper configuration
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)), "Error reading YAML data")

	v.RegisterAlias("beard", "hasbeard")
	assert.Equal(t, true, v.Get("hasbeard"))

	v.Set("hasbeard", false)
	assert.Equal(t, false, v.Get("beard"))
}

func TestYML(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")

	// Read the YAML data into Viper configuration
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)), "Error reading YAML data")

	assert.Equal(t, "steve", v.Get("name"))
}

func TestJSON(t *testing.T) {
	v := New()

	v.SetConfigType("json")
	// Read the JSON data into Viper configuration
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading JSON data")

	assert.Equal(t, "0001", v.Get("id"))
}

func TestTOML(t *testing.T) {
	v := New()
	v.SetConfigType("toml")

	// Read the TOML data into Viper configuration
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(tomlExample)), "Error reading toml data")

	assert.Equal(t, "TOML Example", v.Get("title"))
}

func TestDotEnv(t *testing.T) {
	v := New()
	v.SetConfigType("env")
	// Read the dotenv data into Viper configuration
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(dotenvExample)), "Error reading env data")

	assert.Equal(t, "DotEnv Example", v.Get("title_dotenv"))
}

func TestRemotePrecedence(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	// Read the remote data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data")

	assert.Equal(t, "0001", v.Get("id"))

	// update the kvstore with the remoteExample which should overite the key in v.config
	remote := bytes.NewReader(remoteExample)
	require.NoError(t, v.unmarshalReader(remote, v.kvstore), "Error reading json data in to kvstore")

	assert.Equal(t, "0001", v.Get("id"))
	assert.NotEqual(t, "cronut", v.Get("type"))
	assert.Equal(t, "remote", v.Get("newkey"))
	v.Set("newkey", "newvalue")
	assert.NotEqual(t, "remote", v.Get("newkey"))
	assert.Equal(t, "newvalue", v.Get("newkey"))
}

func TestEnv(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	// Read the JSON data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data")

	v.BindEnv("id")
	v.BindEnv("f", "FOOD", "OLD_FOOD")

	t.Setenv("ID", "13")
	t.Setenv("FOOD", "apple")
	t.Setenv("OLD_FOOD", "banana")
	t.Setenv("NAME", "crunk")

	assert.Equal(t, "13", v.Get("id"))
	assert.Equal(t, "apple", v.Get("f"))
	assert.Equal(t, "Cake", v.Get("name"))

	v.AutomaticEnv()

	assert.Equal(t, "crunk", v.Get("name"))
}

func TestMultipleEnv(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	// Read the JSON data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data")

	v.BindEnv("f", "FOOD", "OLD_FOOD")

	t.Setenv("OLD_FOOD", "banana")

	assert.Equal(t, "banana", v.Get("f"))
}

func TestEmptyEnv(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	// Read the JSON data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data")

	v.BindEnv("type") // Empty environment variable
	v.BindEnv("name") // Bound, but not set environment variable

	t.Setenv("TYPE", "")

	assert.Equal(t, "donut", v.Get("type"))
	assert.Equal(t, "Cake", v.Get("name"))
}

func TestEmptyEnv_Allowed(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	// Read the JSON data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data")

	v.AllowEmptyEnv(true)

	v.BindEnv("type") // Empty environment variable
	v.BindEnv("name") // Bound, but not set environment variable

	t.Setenv("TYPE", "")

	assert.Equal(t, "", v.Get("type"))
	assert.Equal(t, "Cake", v.Get("name"))
}

func TestEnvPrefix(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	// Read the JSON data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data")

	v.SetEnvPrefix("foo") // will be uppercased automatically
	v.BindEnv("id")
	v.BindEnv("f", "FOOD") // not using prefix

	t.Setenv("FOO_ID", "13")
	t.Setenv("FOOD", "apple")
	t.Setenv("FOO_NAME", "crunk")

	assert.Equal(t, "13", v.Get("id"))
	assert.Equal(t, "apple", v.Get("f"))
	assert.Equal(t, "Cake", v.Get("name"))

	v.AutomaticEnv()

	assert.Equal(t, "crunk", v.Get("name"))
}

func TestAutoEnv(t *testing.T) {
	v := New()

	v.AutomaticEnv()

	t.Setenv("FOO_BAR", "13")

	assert.Equal(t, "13", v.Get("foo_bar"))
}

func TestAutoEnvWithPrefix(t *testing.T) {
	v := New()
	v.AutomaticEnv()
	v.SetEnvPrefix("Baz")
	t.Setenv("BAZ_BAR", "13")
	assert.Equal(t, "13", v.Get("bar"))
}

func TestSetEnvKeyReplacer(t *testing.T) {
	v := New()
	v.AutomaticEnv()

	t.Setenv("REFRESH_INTERVAL", "30s")

	replacer := strings.NewReplacer("-", "_")
	v.SetEnvKeyReplacer(replacer)

	assert.Equal(t, "30s", v.Get("refresh-interval"))
}

func TestEnvKeyReplacer(t *testing.T) {
	v := NewWithOptions(EnvKeyReplacer(strings.NewReplacer("-", "_")))
	v.AutomaticEnv()
	t.Setenv("REFRESH_INTERVAL", "30s")
	assert.Equal(t, "30s", v.Get("refresh-interval"))
}

func TestEnvSubConfig(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")
	// Read the YAML data into Viper configuration v.config
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)), "Error reading json data")
	v.AutomaticEnv()
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

	t.Setenv("CLOTHING_PANTS_SIZE", "small")
	subv := v.Sub("clothing").Sub("pants")
	assert.Equal(t, "small", subv.Get("size"))

	// again with EnvPrefix
	v.SetEnvPrefix("foo") // will be uppercased automatically
	subWithPrefix := v.Sub("clothing").Sub("pants")
	t.Setenv("FOO_CLOTHING_PANTS_SIZE", "large")
	assert.Equal(t, "large", subWithPrefix.Get("size"))
}

func TestAllKeys(t *testing.T) {
	v := New()
	initConfigs(v)

	ks := []string{
		"title",
		"newkey",
		"owner.organization",
		"owner.dob",
		"owner.bio",
		"name",
		"beard",
		"ppu",
		"batters.batter",
		"hobbies",
		"clothing.jacket",
		"clothing.trousers",
		"clothing.pants.size",
		"age",
		"hacker",
		"id",
		"type",
		"eyes",
		"title_dotenv",
		"type_dotenv",
		"name_dotenv",
	}
	dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
	all := map[string]any{
		"owner": map[string]any{
			"organization": "MongoDB",
			"bio":          "MongoDB Chief Developer Advocate & Hacker at Large",
			"dob":          dob,
		},
		"title": "TOML Example",
		"ppu":   0.55,
		"eyes":  "brown",
		"clothing": map[string]any{
			"trousers": "denim",
			"jacket":   "leather",
			"pants":    map[string]any{"size": "large"},
		},
		"id": "0001",
		"batters": map[string]any{
			"batter": []any{
				map[string]any{"type": "Regular"},
				map[string]any{"type": "Chocolate"},
				map[string]any{"type": "Blueberry"},
				map[string]any{"type": "Devil's Food"},
			},
		},
		"hacker": true,
		"beard":  true,
		"hobbies": []any{
			"skateboarding",
			"snowboarding",
			"go",
		},
		"age":          35,
		"type":         "donut",
		"newkey":       "remote",
		"name":         "Cake",
		"title_dotenv": "DotEnv Example",
		"type_dotenv":  "donut",
		"name_dotenv":  "Cake",
	}

	assert.ElementsMatch(t, ks, v.AllKeys())
	assert.Equal(t, all, v.AllSettings())
}

func TestAllKeysWithEnv(t *testing.T) {
	v := New()

	// bind and define environment variables (including a nested one)
	v.BindEnv("id")
	v.BindEnv("foo.bar")
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

	t.Setenv("ID", "13")
	t.Setenv("FOO_BAR", "baz")

	assert.ElementsMatch(t, []string{"id", "foo.bar"}, v.AllKeys())
}

func TestAliasesOfAliases(t *testing.T) {
	v := New()
	v.Set("Title", "Checking Case")
	v.RegisterAlias("Foo", "Bar")
	v.RegisterAlias("Bar", "Title")
	assert.Equal(t, "Checking Case", v.Get("FOO"))
}

func TestRecursiveAliases(t *testing.T) {
	v := New()
	v.Set("baz", "bat")
	v.RegisterAlias("Baz", "Roo")
	v.RegisterAlias("Roo", "baz")
	assert.Equal(t, "bat", v.Get("Baz"))
}

func TestUnmarshal(t *testing.T) {
	v := New()
	v.SetDefault("port", 1313)
	v.Set("name", "Steve")
	v.Set("duration", "1s1ms")
	v.Set("modes", []int{1, 2, 3})

	type config struct {
		Port     int
		Name     string
		Duration time.Duration
		Modes    []int
	}

	var C config
	require.NoError(t, v.Unmarshal(&C), "unable to decode into struct")

	assert.Equal(
		t,
		&config{
			Name:     "Steve",
			Port:     1313,
			Duration: time.Second + time.Millisecond,
			Modes:    []int{1, 2, 3},
		},
		&C,
	)

	v.Set("port", 1234)
	require.NoError(t, v.Unmarshal(&C), "unable to decode into struct")

	assert.Equal(
		t,
		&config{
			Name:     "Steve",
			Port:     1234,
			Duration: time.Second + time.Millisecond,
			Modes:    []int{1, 2, 3},
		},
		&C,
	)
}

func TestUnmarshalWithDefaultDecodeHook(t *testing.T) {
	opt := mapstructure.ComposeDecodeHookFunc(
		mapstructure.StringToTimeDurationHookFunc(),
		mapstructure.StringToSliceHookFunc(","),
		// Custom Decode Hook Function
		func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
			if rf != reflect.String || rt != reflect.Map {
				return data, nil
			}
			m := map[string]string{}
			raw := data.(string)
			if raw == "" {
				return m, nil
			}
			err := json.Unmarshal([]byte(raw), &m)
			return m, err
		},
	)

	v := NewWithOptions(WithDecodeHook(opt))
	v.Set("credentials", "{\"foo\":\"bar\"}")

	type config struct {
		Credentials map[string]string
	}

	var C config

	require.NoError(t, v.Unmarshal(&C), "unable to decode into struct")

	assert.Equal(t, &config{
		Credentials: map[string]string{"foo": "bar"},
	}, &C)
}

func TestUnmarshalWithDecoderOptions(t *testing.T) {
	v := New()
	v.Set("credentials", "{\"foo\":\"bar\"}")

	opt := DecodeHook(mapstructure.ComposeDecodeHookFunc(
		mapstructure.StringToTimeDurationHookFunc(),
		mapstructure.StringToSliceHookFunc(","),
		// Custom Decode Hook Function
		func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
			if rf != reflect.String || rt != reflect.Map {
				return data, nil
			}
			m := map[string]string{}
			raw := data.(string)
			if raw == "" {
				return m, nil
			}
			err := json.Unmarshal([]byte(raw), &m)
			return m, err
		},
	))

	type config struct {
		Credentials map[string]string
	}

	var C config

	require.NoError(t, v.Unmarshal(&C, opt), "unable to decode into struct")

	assert.Equal(t, &config{
		Credentials: map[string]string{"foo": "bar"},
	}, &C)
}

func TestUnmarshalWithAutomaticEnv(t *testing.T) {
	t.Setenv("PORT", "1313")
	t.Setenv("NAME", "Steve")
	t.Setenv("DURATION", "1s1ms")
	t.Setenv("MODES", "1,2,3")
	t.Setenv("SECRET", "42")
	t.Setenv("FILESYSTEM_SIZE", "4096")

	type AuthConfig struct {
		Secret string `mapstructure:"secret"`
	}

	type StorageConfig struct {
		Size int `mapstructure:"size"`
	}

	type Configuration struct {
		Port     int           `mapstructure:"port"`
		Name     string        `mapstructure:"name"`
		Duration time.Duration `mapstructure:"duration"`

		// Infer name from struct
		Modes []int

		// Squash nested struct (omit prefix)
		Authentication AuthConfig `mapstructure:",squash"`

		// Different key
		Storage StorageConfig `mapstructure:"filesystem"`

		// Omitted field
		Flag bool `mapstructure:"flag"`
	}

	v := NewWithOptions(ExperimentalBindStruct())
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.AutomaticEnv()

	t.Run("OK", func(t *testing.T) {
		var config Configuration
		if err := v.Unmarshal(&config); err != nil {
			t.Fatalf("unable to decode into struct, %v", err)
		}

		assert.Equal(
			t,
			Configuration{
				Name:     "Steve",
				Port:     1313,
				Duration: time.Second + time.Millisecond,
				Modes:    []int{1, 2, 3},
				Authentication: AuthConfig{
					Secret: "42",
				},
				Storage: StorageConfig{
					Size: 4096,
				},
			},
			config,
		)
	})

	t.Run("Precedence", func(t *testing.T) {
		var config Configuration

		v.Set("port", 1234)
		if err := v.Unmarshal(&config); err != nil {
			t.Fatalf("unable to decode into struct, %v", err)
		}

		assert.Equal(
			t,
			Configuration{
				Name:     "Steve",
				Port:     1234,
				Duration: time.Second + time.Millisecond,
				Modes:    []int{1, 2, 3},
				Authentication: AuthConfig{
					Secret: "42",
				},
				Storage: StorageConfig{
					Size: 4096,
				},
			},
			config,
		)
	})

	t.Run("Unset", func(t *testing.T) {
		var config Configuration

		err := v.Unmarshal(&config, func(config *mapstructure.DecoderConfig) {
			config.ErrorUnset = true
		})

		assert.Error(t, err, "expected viper.Unmarshal to return error due to unset field 'FLAG'")
	})

	t.Run("Exact", func(t *testing.T) {
		var config Configuration

		v.Set("port", 1234)
		if err := v.UnmarshalExact(&config); err != nil {
			t.Fatalf("unable to decode into struct, %v", err)
		}

		assert.Equal(
			t,
			Configuration{
				Name:     "Steve",
				Port:     1234,
				Duration: time.Second + time.Millisecond,
				Modes:    []int{1, 2, 3},
				Authentication: AuthConfig{
					Secret: "42",
				},
				Storage: StorageConfig{
					Size: 4096,
				},
			},
			config,
		)
	})
}

func TestBindPFlags(t *testing.T) {
	v := New() // create independent Viper object
	flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)

	testValues := map[string]*string{
		"host":     nil,
		"port":     nil,
		"endpoint": nil,
	}

	mutatedTestValues := map[string]string{
		"host":     "localhost",
		"port":     "6060",
		"endpoint": "/public",
	}

	for name := range testValues {
		testValues[name] = flagSet.String(name, "", "test")
	}

	err := v.BindPFlags(flagSet)
	require.NoError(t, err, "error binding flag set")

	flagSet.VisitAll(func(flag *pflag.Flag) {
		flag.Value.Set(mutatedTestValues[flag.Name])
		flag.Changed = true
	})

	for name, expected := range mutatedTestValues {
		assert.Equal(t, expected, v.Get(name))
	}
}

func TestBindPFlagsStringSlice(t *testing.T) {
	tests := []struct {
		Expected []string
		Value    string
	}{
		{[]string{}, ""},
		{[]string{"jeden"}, "jeden"},
		{[]string{"dwa", "trzy"}, "dwa,trzy"},
		{[]string{"cztery", "piec , szesc"}, "cztery,\"piec , szesc\""},
	}

	v := New() // create independent Viper object
	defaultVal := []string{"default"}
	v.SetDefault("stringslice", defaultVal)

	for _, testValue := range tests {
		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
		flagSet.StringSlice("stringslice", testValue.Expected, "test")

		for _, changed := range []bool{true, false} {
			flagSet.VisitAll(func(f *pflag.Flag) {
				f.Value.Set(testValue.Value)
				f.Changed = changed
			})

			err := v.BindPFlags(flagSet)
			require.NoError(t, err, "error binding flag set")

			type TestStr struct {
				StringSlice []string
			}
			val := &TestStr{}
			err = v.Unmarshal(val)
			require.NoError(t, err, "cannot unmarshal")
			if changed {
				assert.Equal(t, testValue.Expected, val.StringSlice)
				assert.Equal(t, testValue.Expected, v.Get("stringslice"))
			} else {
				assert.Equal(t, defaultVal, val.StringSlice)
			}
		}
	}
}

func TestBindPFlagsStringArray(t *testing.T) {
	tests := []struct {
		Expected []string
		Value    string
	}{
		{[]string{}, ""},
		{[]string{"jeden"}, "jeden"},
		{[]string{"dwa,trzy"}, "dwa,trzy"},
		{[]string{"cztery,\"piec , szesc\""}, "cztery,\"piec , szesc\""},
	}

	v := New() // create independent Viper object
	defaultVal := []string{"default"}
	v.SetDefault("stringarray", defaultVal)

	for _, testValue := range tests {
		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
		flagSet.StringArray("stringarray", testValue.Expected, "test")

		for _, changed := range []bool{true, false} {
			flagSet.VisitAll(func(f *pflag.Flag) {
				f.Value.Set(testValue.Value)
				f.Changed = changed
			})

			err := v.BindPFlags(flagSet)
			require.NoError(t, err, "error binding flag set")

			type TestStr struct {
				StringArray []string
			}
			val := &TestStr{}
			err = v.Unmarshal(val)
			require.NoError(t, err, "cannot unmarshal")
			if changed {
				assert.Equal(t, testValue.Expected, val.StringArray)
				assert.Equal(t, testValue.Expected, v.Get("stringarray"))
			} else {
				assert.Equal(t, defaultVal, val.StringArray)
			}
		}
	}
}

func TestBindPFlagsSlices(t *testing.T) {
	set := pflag.NewFlagSet("test", pflag.ContinueOnError)
	set.IntSlice("intslice", []int{}, "")
	set.BoolSlice("boolslice", []bool{}, "")
	set.Float64Slice("float64slice", []float64{}, "")
	set.UintSlice("uintslice", []uint{}, "")

	v := New()
	v.BindPFlags(set)

	set.Set("intslice", "1,2")
	assert.Equal(t, []int{1, 2}, v.Get("intslice"))

	set.Set("boolslice", "true,false")
	assert.Equal(t, []bool{true, false}, v.Get("boolslice"))

	set.Set("float64slice", "1.1,2.2")
	assert.Equal(t, []float64{1.1, 2.2}, v.Get("float64slice"))

	set.Set("uintslice", "1,2")
	assert.Equal(t, []uint{1, 2}, v.Get("uintslice"))
}

func TestSliceFlagsReturnCorrectType(t *testing.T) {
	flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
	flagSet.IntSlice("int", []int{1, 2}, "")
	flagSet.StringSlice("str", []string{"3", "4"}, "")
	flagSet.DurationSlice("duration", []time.Duration{5 * time.Second}, "")

	v := New()
	v.BindPFlags(flagSet)

	all := v.AllSettings()

	assert.IsType(t, []int{}, all["int"])
	assert.IsType(t, []string{}, all["str"])
	assert.IsType(t, []time.Duration{}, all["duration"])
}

func TestBindPFlagsIntSlice(t *testing.T) {
	tests := []struct {
		Expected []int
		Value    string
	}{
		{[]int{}, ""},
		{[]int{1}, "1"},
		{[]int{2, 3}, "2,3"},
	}

	v := New() // create independent Viper object
	defaultVal := []int{0}
	v.SetDefault("intslice", defaultVal)

	for _, testValue := range tests {
		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
		flagSet.IntSlice("intslice", testValue.Expected, "test")

		for _, changed := range []bool{true, false} {
			flagSet.VisitAll(func(f *pflag.Flag) {
				f.Value.Set(testValue.Value)
				f.Changed = changed
			})

			err := v.BindPFlags(flagSet)
			require.NoError(t, err, "error binding flag set")

			type TestInt struct {
				IntSlice []int
			}
			val := &TestInt{}
			err = v.Unmarshal(val)
			require.NoError(t, err, "cannot unmarshal")
			if changed {
				assert.Equal(t, testValue.Expected, val.IntSlice)
				assert.Equal(t, testValue.Expected, v.Get("intslice"))
			} else {
				assert.Equal(t, defaultVal, val.IntSlice)
			}
		}
	}
}

func TestBindPFlag(t *testing.T) {
	v := New()
	testString := "testing"
	testValue := newStringValue(testString, &testString)

	flag := &pflag.Flag{
		Name:    "testflag",
		Value:   testValue,
		Changed: false,
	}

	v.BindPFlag("testvalue", flag)

	assert.Equal(t, testString, v.Get("testvalue"))

	flag.Value.Set("testing_mutate")
	flag.Changed = true // hack for pflag usage

	assert.Equal(t, "testing_mutate", v.Get("testvalue"))
}

func TestBindPFlagDetectNilFlag(t *testing.T) {
	v := New()
	result := v.BindPFlag("testvalue", nil)
	assert.Error(t, result)
}

func TestBindPFlagStringToString(t *testing.T) {
	tests := []struct {
		Expected map[string]string
		Value    string
	}{
		{map[string]string{}, ""},
		{map[string]string{"yo": "hi"}, "yo=hi"},
		{map[string]string{"yo": "hi", "oh": "hi=there"}, "yo=hi,oh=hi=there"},
		{map[string]string{"yo": ""}, "yo="},
		{map[string]string{"yo": "", "oh": "hi=there"}, "yo=,oh=hi=there"},
	}

	v := New() // create independent Viper object
	defaultVal := map[string]string{}
	v.SetDefault("stringtostring", defaultVal)

	for _, testValue := range tests {
		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
		flagSet.StringToString("stringtostring", testValue.Expected, "test")

		for _, changed := range []bool{true, false} {
			flagSet.VisitAll(func(f *pflag.Flag) {
				f.Value.Set(testValue.Value)
				f.Changed = changed
			})

			err := v.BindPFlags(flagSet)
			require.NoError(t, err, "error binding flag set")

			type TestMap struct {
				StringToString map[string]string
			}
			val := &TestMap{}
			err = v.Unmarshal(val)
			require.NoError(t, err, "cannot unmarshal")
			if changed {
				assert.Equal(t, testValue.Expected, val.StringToString)
			} else {
				assert.Equal(t, defaultVal, val.StringToString)
			}
		}
	}
}

func TestBindPFlagStringToInt(t *testing.T) {
	tests := []struct {
		Expected map[string]int
		Value    string
	}{
		{map[string]int{"yo": 1, "oh": 21}, "yo=1,oh=21"},
		{map[string]int{"yo": 100000000, "oh": 0}, "yo=100000000,oh=0"},
		{map[string]int{}, "yo=2,oh=21.0"},
		{map[string]int{}, "yo=,oh=20.99"},
		{map[string]int{}, "yo=,oh="},
	}

	v := New() // create independent Viper object
	defaultVal := map[string]int{}
	v.SetDefault("stringtoint", defaultVal)

	for _, testValue := range tests {
		flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
		flagSet.StringToInt("stringtoint", testValue.Expected, "test")

		for _, changed := range []bool{true, false} {
			flagSet.VisitAll(func(f *pflag.Flag) {
				f.Value.Set(testValue.Value)
				f.Changed = changed
			})

			err := v.BindPFlags(flagSet)
			require.NoError(t, err, "error binding flag set")

			type TestMap struct {
				StringToInt map[string]int
			}
			val := &TestMap{}
			err = v.Unmarshal(val)
			require.NoError(t, err, "cannot unmarshal")
			if changed {
				assert.Equal(t, testValue.Expected, val.StringToInt)
			} else {
				assert.Equal(t, defaultVal, val.StringToInt)
			}
		}
	}
}

func TestBoundCaseSensitivity(t *testing.T) {
	v := New()
	initConfigs(v)
	assert.Equal(t, "brown", v.Get("eyes"))

	v.BindEnv("eYEs", "TURTLE_EYES")

	t.Setenv("TURTLE_EYES", "blue")

	assert.Equal(t, "blue", v.Get("eyes"))

	testString := "green"
	testValue := newStringValue(testString, &testString)

	flag := &pflag.Flag{
		Name:    "eyeballs",
		Value:   testValue,
		Changed: true,
	}

	v.BindPFlag("eYEs", flag)
	assert.Equal(t, "green", v.Get("eyes"))
}

func TestSizeInBytes(t *testing.T) {
	input := map[string]uint{
		"":               0,
		"b":              0,
		"12 bytes":       0,
		"200000000000gb": 0,
		"12 b":           12,
		"43 MB":          43 * (1 << 20),
		"10mb":           10 * (1 << 20),
		"1gb":            1 << 30,
	}

	for str, expected := range input {
		assert.Equal(t, expected, parseSizeInBytes(str), str)
	}
}

func TestFindsNestedKeys(t *testing.T) {
	v := New()
	initConfigs(v)
	dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")

	v.Set("super", map[string]any{
		"deep": map[string]any{
			"nested": "value",
		},
	})

	expected := map[string]any{
		"super": map[string]any{
			"deep": map[string]any{
				"nested": "value",
			},
		},
		"super.deep": map[string]any{
			"nested": "value",
		},
		"super.deep.nested":  "value",
		"owner.organization": "MongoDB",
		"batters.batter": []any{
			map[string]any{
				"type": "Regular",
			},
			map[string]any{
				"type": "Chocolate",
			},
			map[string]any{
				"type": "Blueberry",
			},
			map[string]any{
				"type": "Devil's Food",
			},
		},
		"hobbies": []any{
			"skateboarding", "snowboarding", "go",
		},
		"TITLE_DOTENV": "DotEnv Example",
		"TYPE_DOTENV":  "donut",
		"NAME_DOTENV":  "Cake",
		"title":        "TOML Example",
		"newkey":       "remote",
		"batters": map[string]any{
			"batter": []any{
				map[string]any{
					"type": "Regular",
				},
				map[string]any{
					"type": "Chocolate",
				},
				map[string]any{
					"type": "Blueberry",
				},
				map[string]any{
					"type": "Devil's Food",
				},
			},
		},
		"eyes": "brown",
		"age":  35,
		"owner": map[string]any{
			"organization": "MongoDB",
			"bio":          "MongoDB Chief Developer Advocate & Hacker at Large",
			"dob":          dob,
		},
		"owner.bio": "MongoDB Chief Developer Advocate & Hacker at Large",
		"type":      "donut",
		"id":        "0001",
		"name":      "Cake",
		"hacker":    true,
		"ppu":       0.55,
		"clothing": map[string]any{
			"jacket":   "leather",
			"trousers": "denim",
			"pants": map[string]any{
				"size": "large",
			},
		},
		"clothing.jacket":     "leather",
		"clothing.pants.size": "large",
		"clothing.trousers":   "denim",
		"owner.dob":           dob,
		"beard":               true,
	}

	for key, expectedValue := range expected {
		assert.Equal(t, expectedValue, v.Get(key))
	}
}

func TestReadConfig(t *testing.T) {
	t.Run("ok", func(t *testing.T) {
		v := New()
		v.SetConfigType("yaml")
		err := v.ReadConfig(bytes.NewBuffer(yamlExample))
		require.NoError(t, err)
		t.Log(v.AllKeys())

		assert.True(t, v.InConfig("name"))
		assert.True(t, v.InConfig("clothing.jacket"))
		assert.False(t, v.InConfig("state"))
		assert.False(t, v.InConfig("clothing.hat"))
		assert.Equal(t, "steve", v.Get("name"))
		assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
		assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing"))
		assert.Equal(t, 35, v.Get("age"))
	})

	t.Run("missing config type", func(t *testing.T) {
		v := New()
		err := v.ReadConfig(bytes.NewBuffer(yamlExample))
		require.Error(t, err)
	})
}

func TestReadConfigWithSetConfigFile(t *testing.T) {
	v := New()
	v.SetConfigFile("config.yaml") // Dummy value to infer config type from file extension
	err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc))
	require.NoError(t, err)
	assert.Equal(t, 45000, v.GetInt("hello.pop"))
}

func TestIsSet(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")

	/* config and defaults */
	v.ReadConfig(bytes.NewBuffer(yamlExample))
	v.SetDefault("clothing.shoes", "sneakers")

	assert.True(t, v.IsSet("clothing"))
	assert.True(t, v.IsSet("clothing.jacket"))
	assert.False(t, v.IsSet("clothing.jackets"))
	assert.True(t, v.IsSet("clothing.shoes"))

	/* state change */
	assert.False(t, v.IsSet("helloworld"))
	v.Set("helloworld", "fubar")
	assert.True(t, v.IsSet("helloworld"))

	/* env */
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.BindEnv("eyes")
	v.BindEnv("foo")
	v.BindEnv("clothing.hat")
	v.BindEnv("clothing.hats")

	t.Setenv("FOO", "bar")
	t.Setenv("CLOTHING_HAT", "bowler")

	assert.True(t, v.IsSet("eyes"))           // in the config file
	assert.True(t, v.IsSet("foo"))            // in the environment
	assert.True(t, v.IsSet("clothing.hat"))   // in the environment
	assert.False(t, v.IsSet("clothing.hats")) // not defined

	/* flags */
	flagset := pflag.NewFlagSet("testisset", pflag.ContinueOnError)
	flagset.Bool("foobaz", false, "foobaz")
	flagset.Bool("barbaz", false, "barbaz")
	foobaz, barbaz := flagset.Lookup("foobaz"), flagset.Lookup("barbaz")
	v.BindPFlag("foobaz", foobaz)
	v.BindPFlag("barbaz", barbaz)
	barbaz.Value.Set("true")
	barbaz.Changed = true // hack for pflag usage

	assert.False(t, v.IsSet("foobaz"))
	assert.True(t, v.IsSet("barbaz"))
}

func TestDirsSearch(t *testing.T) {
	root, config := initDirs(t)

	v := New()
	v.SetConfigName(config)
	v.SetDefault(`key`, `default`)

	entries, err := os.ReadDir(root)
	require.NoError(t, err)
	for _, e := range entries {
		if e.IsDir() {
			v.AddConfigPath(filepath.Join(root, e.Name()))
		}
	}

	err = v.ReadInConfig()
	require.NoError(t, err)

	assert.Equal(t, `value is `+filepath.Base(v.configPaths[0]), v.GetString(`key`))
}

func TestWrongDirsSearchNotFound(t *testing.T) {
	_, config := initDirs(t)

	v := New()
	v.SetConfigName(config)
	v.SetDefault(`key`, `default`)

	v.AddConfigPath(`whattayoutalkingbout`)
	v.AddConfigPath(`thispathaintthere`)

	err := v.ReadInConfig()
	assert.IsType(t, ConfigFileNotFoundError{"", ""}, err)

	// Even though config did not load and the error might have
	// been ignored by the client, the default still loads
	assert.Equal(t, `default`, v.GetString(`key`))
}

func TestWrongDirsSearchNotFoundForMerge(t *testing.T) {
	_, config := initDirs(t)

	v := New()
	v.SetConfigName(config)
	v.SetDefault(`key`, `default`)

	v.AddConfigPath(`whattayoutalkingbout`)
	v.AddConfigPath(`thispathaintthere`)

	err := v.MergeInConfig()
	assert.Equal(t, reflect.TypeOf(ConfigFileNotFoundError{"", ""}), reflect.TypeOf(err))

	// Even though config did not load and the error might have
	// been ignored by the client, the default still loads
	assert.Equal(t, `default`, v.GetString(`key`))
}

var yamlInvalid = []byte(`hash: map
- foo
- bar
`)

func TestUnwrapParseErrors(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")
	assert.ErrorAs(t, v.ReadConfig(bytes.NewBuffer(yamlInvalid)), &ConfigParseError{})
}

func TestSub(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")
	v.ReadConfig(bytes.NewBuffer(yamlExample))

	subv := v.Sub("clothing")
	assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("pants.size"))

	subv = v.Sub("clothing.pants")
	assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("size"))

	subv = v.Sub("clothing.pants.size")
	assert.Equal(t, (*Viper)(nil), subv)

	subv = v.Sub("missing.key")
	assert.Equal(t, (*Viper)(nil), subv)

	subv = v.Sub("clothing")
	assert.Equal(t, []string{"clothing"}, subv.parents)

	subv = v.Sub("clothing").Sub("pants")
	assert.Equal(t, []string{"clothing", "pants"}, subv.parents)
}

func TestSubWithKeyDelimiter(t *testing.T) {
	v := NewWithOptions(KeyDelimiter("::"))
	v.SetConfigType("yaml")
	r := strings.NewReader(string(yamlExampleWithDot))
	err := v.unmarshalReader(r, v.config)
	require.NoError(t, err)

	subv := v.Sub("emails")
	assert.Equal(t, "01/02/03", subv.Get("steve@hacker.com::created"))
}

var jsonWriteExpected = []byte(`{
  "batters": {
    "batter": [
      {
        "type": "Regular"
      },
      {
        "type": "Chocolate"
      },
      {
        "type": "Blueberry"
      },
      {
        "type": "Devil's Food"
      }
    ]
  },
  "id": "0001",
  "name": "Cake",
  "ppu": 0.55,
  "type": "donut"
}`)

// var yamlWriteExpected = []byte(`age: 35
// beard: true
// clothing:
//     jacket: leather
//     pants:
//         size: large
//     trousers: denim
// eyes: brown
// hacker: true
// hobbies:
//     - skateboarding
//     - snowboarding
//     - go
// name: steve
// `)

func TestWriteConfig(t *testing.T) {
	fs := afero.NewMemMapFs()
	testCases := map[string]struct {
		configName      string
		inConfigType    string
		outConfigType   string
		fileName        string
		input           []byte
		expectedContent []byte
	}{
		"json with file extension": {
			configName:      "c",
			inConfigType:    "json",
			outConfigType:   "json",
			fileName:        "c.json",
			input:           jsonExample,
			expectedContent: jsonWriteExpected,
		},
		"json without file extension": {
			configName:      "c",
			inConfigType:    "json",
			outConfigType:   "json",
			fileName:        "c",
			input:           jsonExample,
			expectedContent: jsonWriteExpected,
		},
		"json with file extension and mismatch type": {
			configName:      "c",
			inConfigType:    "json",
			outConfigType:   "hcl",
			fileName:        "c.json",
			input:           jsonExample,
			expectedContent: jsonWriteExpected,
		},
		"yaml with file extension": {
			configName:      "c",
			inConfigType:    "yaml",
			outConfigType:   "yaml",
			fileName:        "c.yaml",
			input:           yamlExample,
			expectedContent: yamlWriteExpected,
		},
		"yaml without file extension": {
			configName:      "c",
			inConfigType:    "yaml",
			outConfigType:   "yaml",
			fileName:        "c",
			input:           yamlExample,
			expectedContent: yamlWriteExpected,
		},
		"yaml with file extension and mismatch type": {
			configName:      "c",
			inConfigType:    "yaml",
			outConfigType:   "json",
			fileName:        "c.yaml",
			input:           yamlExample,
			expectedContent: yamlWriteExpected,
		},
	}
	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			v := New()
			v.SetFs(fs)
			v.SetConfigName(tc.fileName)
			v.SetConfigType(tc.inConfigType)

			err := v.ReadConfig(bytes.NewBuffer(tc.input))
			require.NoError(t, err)
			v.SetConfigType(tc.outConfigType)
			err = v.WriteConfigAs(tc.fileName)
			require.NoError(t, err)
			read, err := afero.ReadFile(fs, tc.fileName)
			require.NoError(t, err)
			assert.Equal(t, tc.expectedContent, read)
		})
	}
}

func TestWriteConfigTOML(t *testing.T) {
	fs := afero.NewMemMapFs()

	testCases := map[string]struct {
		configName string
		configType string
		fileName   string
		input      []byte
	}{
		"with file extension": {
			configName: "c",
			configType: "toml",
			fileName:   "c.toml",
			input:      tomlExample,
		},
		"without file extension": {
			configName: "c",
			configType: "toml",
			fileName:   "c",
			input:      tomlExample,
		},
	}
	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			v := New()
			v.SetFs(fs)
			v.SetConfigName(tc.configName)
			v.SetConfigType(tc.configType)
			err := v.ReadConfig(bytes.NewBuffer(tc.input))
			require.NoError(t, err)
			err = v.WriteConfigAs(tc.fileName)
			require.NoError(t, err)

			// The TOML String method does not order the contents.
			// Therefore, we must read the generated file and compare the data.
			v2 := New()
			v2.SetFs(fs)
			v2.SetConfigName(tc.configName)
			v2.SetConfigType(tc.configType)
			v2.SetConfigFile(tc.fileName)
			err = v2.ReadInConfig()
			require.NoError(t, err)

			assert.Equal(t, v.GetString("title"), v2.GetString("title"))
			assert.Equal(t, v.GetString("owner.bio"), v2.GetString("owner.bio"))
			assert.Equal(t, v.GetString("owner.dob"), v2.GetString("owner.dob"))
			assert.Equal(t, v.GetString("owner.organization"), v2.GetString("owner.organization"))
		})
	}
}

func TestWriteConfigDotEnv(t *testing.T) {
	fs := afero.NewMemMapFs()
	testCases := map[string]struct {
		configName string
		configType string
		fileName   string
		input      []byte
	}{
		"with file extension": {
			configName: "c",
			configType: "env",
			fileName:   "c.env",
			input:      dotenvExample,
		},
		"without file extension": {
			configName: "c",
			configType: "env",
			fileName:   "c",
			input:      dotenvExample,
		},
	}
	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			v := New()
			v.SetFs(fs)
			v.SetConfigName(tc.configName)
			v.SetConfigType(tc.configType)
			err := v.ReadConfig(bytes.NewBuffer(tc.input))
			require.NoError(t, err)
			err = v.WriteConfigAs(tc.fileName)
			require.NoError(t, err)

			// The TOML String method does not order the contents.
			// Therefore, we must read the generated file and compare the data.
			v2 := New()
			v2.SetFs(fs)
			v2.SetConfigName(tc.configName)
			v2.SetConfigType(tc.configType)
			v2.SetConfigFile(tc.fileName)
			err = v2.ReadInConfig()
			require.NoError(t, err)

			assert.Equal(t, v.GetString("title_dotenv"), v2.GetString("title_dotenv"))
			assert.Equal(t, v.GetString("type_dotenv"), v2.GetString("type_dotenv"))
			assert.Equal(t, v.GetString("kind_dotenv"), v2.GetString("kind_dotenv"))
		})
	}
}

func TestSafeWriteConfig(t *testing.T) {
	v := New()
	fs := afero.NewMemMapFs()
	v.SetFs(fs)
	v.AddConfigPath("/test")
	v.SetConfigName("c")
	v.SetConfigType("yaml")
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)))
	require.NoError(t, v.SafeWriteConfig())
	read, err := afero.ReadFile(fs, testutil.AbsFilePath(t, "/test/c.yaml"))
	require.NoError(t, err)
	assert.YAMLEq(t, string(yamlWriteExpected), string(read))
}

func TestSafeWriteConfigWithMissingConfigPath(t *testing.T) {
	v := New()
	fs := afero.NewMemMapFs()
	v.SetFs(fs)
	v.SetConfigName("c")
	v.SetConfigType("yaml")
	require.EqualError(t, v.SafeWriteConfig(), "missing configuration for 'configPath'")
}

func TestSafeWriteConfigWithExistingFile(t *testing.T) {
	v := New()
	fs := afero.NewMemMapFs()
	fs.Create(testutil.AbsFilePath(t, "/test/c.yaml"))
	v.SetFs(fs)
	v.AddConfigPath("/test")
	v.SetConfigName("c")
	v.SetConfigType("yaml")
	err := v.SafeWriteConfig()
	require.Error(t, err)
	_, ok := err.(ConfigFileAlreadyExistsError)
	assert.True(t, ok, "Expected ConfigFileAlreadyExistsError")
}

func TestSafeWriteAsConfig(t *testing.T) {
	v := New()
	fs := afero.NewMemMapFs()
	v.SetFs(fs)
	v.SetConfigType("yaml")
	err := v.ReadConfig(bytes.NewBuffer(yamlExample))
	require.NoError(t, err)
	require.NoError(t, v.SafeWriteConfigAs("/test/c.yaml"))
	_, err = afero.ReadFile(fs, "/test/c.yaml")
	require.NoError(t, err)
}

func TestSafeWriteConfigAsWithExistingFile(t *testing.T) {
	v := New()
	fs := afero.NewMemMapFs()
	fs.Create("/test/c.yaml")
	v.SetFs(fs)
	err := v.SafeWriteConfigAs("/test/c.yaml")
	require.Error(t, err)
	_, ok := err.(ConfigFileAlreadyExistsError)
	assert.True(t, ok, "Expected ConfigFileAlreadyExistsError")
}

func TestWriteHiddenFile(t *testing.T) {
	v := New()
	fs := afero.NewMemMapFs()
	fs.Create(testutil.AbsFilePath(t, "/test/.config"))
	v.SetFs(fs)

	v.SetConfigName(".config")
	v.SetConfigType("yaml")
	v.AddConfigPath("/test")

	err := v.ReadInConfig()
	require.NoError(t, err)

	err = v.WriteConfig()
	require.NoError(t, err)
}

var yamlMergeExampleTgt = []byte(`
hello:
    pop: 37890
    largenum: 765432101234567
    num2pow63: 9223372036854775808
    universe: null
    world:
    - us
    - uk
    - fr
    - de
`)

var yamlMergeExampleSrc = []byte(`
hello:
    pop: 45000
    largenum: 7654321001234567
    universe:
    - mw
    - ad
    ints:
    - 1
    - 2
fu: bar
`)

var jsonMergeExampleTgt = []byte(`
{
	"hello": {
		"foo": null,
		"pop": 123456
	}
}
`)

var jsonMergeExampleSrc = []byte(`
{
	"hello": {
		"foo": "foo str",
		"pop": "pop str"
	}
}
`)

func TestMergeConfig(t *testing.T) {
	v := New()
	v.SetConfigType("yml")
	err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt))
	require.NoError(t, err)

	assert.Equal(t, 37890, v.GetInt("hello.pop"))
	assert.Equal(t, int32(37890), v.GetInt32("hello.pop"))
	assert.Equal(t, int64(765432101234567), v.GetInt64("hello.largenum"))
	assert.Equal(t, uint8(2), v.GetUint8("hello.pop"))
	assert.Equal(t, uint(37890), v.GetUint("hello.pop"))
	assert.Equal(t, uint16(37890), v.GetUint16("hello.pop"))
	assert.Equal(t, uint32(37890), v.GetUint32("hello.pop"))
	assert.Equal(t, uint64(9223372036854775808), v.GetUint64("hello.num2pow63"))
	assert.Len(t, v.GetStringSlice("hello.world"), 4)
	assert.Empty(t, v.GetString("fu"))

	err = v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc))
	require.NoError(t, err)

	assert.Equal(t, 45000, v.GetInt("hello.pop"))
	assert.Equal(t, int32(45000), v.GetInt32("hello.pop"))
	assert.Equal(t, int64(7654321001234567), v.GetInt64("hello.largenum"))
	assert.Len(t, v.GetStringSlice("hello.world"), 4)
	assert.Len(t, v.GetStringSlice("hello.universe"), 2)
	assert.Len(t, v.GetIntSlice("hello.ints"), 2)
	assert.Equal(t, "bar", v.GetString("fu"))
}

func TestMergeConfigWithSetConfigFile(t *testing.T) {
	v := New()
	v.SetConfigFile("config.yaml") // Dummy value to infer config type from file extension
	err := v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc))
	require.NoError(t, err)
	assert.Equal(t, 45000, v.GetInt("hello.pop"))
}

func TestMergeConfigOverrideType(t *testing.T) {
	v := New()
	v.SetConfigType("json")
	err := v.ReadConfig(bytes.NewBuffer(jsonMergeExampleTgt))
	require.NoError(t, err)

	err = v.MergeConfig(bytes.NewBuffer(jsonMergeExampleSrc))
	require.NoError(t, err)

	assert.Equal(t, "pop str", v.GetString("hello.pop"))
	assert.Equal(t, "foo str", v.GetString("hello.foo"))
}

func TestMergeConfigNoMerge(t *testing.T) {
	v := New()
	v.SetConfigType("yml")
	err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt))
	require.NoError(t, err)

	assert.Equal(t, 37890, v.GetInt("hello.pop"))
	assert.Len(t, v.GetStringSlice("hello.world"), 4)
	assert.Empty(t, v.GetString("fu"))

	err = v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc))
	require.NoError(t, err)

	assert.Equal(t, 45000, v.GetInt("hello.pop"))
	assert.Empty(t, v.GetStringSlice("hello.world"))
	assert.Len(t, v.GetStringSlice("hello.universe"), 2)
	assert.Len(t, v.GetIntSlice("hello.ints"), 2)
	assert.Equal(t, "bar", v.GetString("fu"))
}

func TestMergeConfigMap(t *testing.T) {
	v := New()
	v.SetConfigType("yml")
	err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt))
	require.NoError(t, err)

	assertFn := func(i int) {
		large := v.GetInt64("hello.largenum")
		pop := v.GetInt("hello.pop")
		assert.Equal(t, int64(765432101234567), large)
		assert.Equal(t, i, pop)
	}

	assertFn(37890)

	update := map[string]any{
		"Hello": map[string]any{
			"Pop": 1234,
		},
		"World": map[any]any{
			"Rock": 345,
		},
	}

	err = v.MergeConfigMap(update)
	require.NoError(t, err)

	assert.Equal(t, 345, v.GetInt("world.rock"))

	assertFn(1234)
}

func TestUnmarshalingWithAliases(t *testing.T) {
	v := New()
	v.SetDefault("ID", 1)
	v.Set("name", "Steve")
	v.Set("lastname", "Owen")

	v.RegisterAlias("UserID", "ID")
	v.RegisterAlias("Firstname", "name")
	v.RegisterAlias("Surname", "lastname")

	type config struct {
		ID        int
		FirstName string
		Surname   string
	}

	var C config
	err := v.Unmarshal(&C)
	require.NoError(t, err, "unable to decode into struct")

	assert.Equal(t, &config{ID: 1, FirstName: "Steve", Surname: "Owen"}, &C)
}

func TestSetConfigNameClearsFileCache(t *testing.T) {
	v := New()
	v.SetConfigFile("/tmp/config.yaml")
	v.SetConfigName("default")
	f, err := v.getConfigFile()
	require.Error(t, err, "config file cache should have been cleared")
	assert.Empty(t, f)
}

func TestShadowedNestedValue(t *testing.T) {
	v := New()
	config := `name: steve
clothing:
  jacket: leather
  trousers: denim
  pants:
    size: large
`
	initConfig("yaml", config, v)

	assert.Equal(t, "steve", v.GetString("name"))

	polyester := "polyester"
	v.SetDefault("clothing.shirt", polyester)
	v.SetDefault("clothing.jacket.price", 100)

	assert.Equal(t, "leather", v.GetString("clothing.jacket"))
	assert.Nil(t, v.Get("clothing.jacket.price"))
	assert.Equal(t, polyester, v.GetString("clothing.shirt"))

	clothingSettings := v.AllSettings()["clothing"].(map[string]any)
	assert.Equal(t, "leather", clothingSettings["jacket"])
	assert.Equal(t, polyester, clothingSettings["shirt"])
}

func TestDotParameter(t *testing.T) {
	v := New()

	v.SetConfigType("json")

	// Read the YAML data into Viper configuration
	require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading YAML data")

	// should take precedence over batters defined in jsonExample
	r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`))
	v.unmarshalReader(r, v.config)

	actual := v.Get("batters.batter")
	expected := []any{map[string]any{"type": "Small"}}
	assert.Equal(t, expected, actual)
}

func TestCaseInsensitive(t *testing.T) {
	for _, config := range []struct {
		typ     string
		content string
	}{
		{"yaml", `
aBcD: 1
eF:
  gH: 2
  iJk: 3
  Lm:
    nO: 4
    P:
      Q: 5
      R: 6
`},
		{"json", `{
  "aBcD": 1,
  "eF": {
    "iJk": 3,
    "Lm": {
      "P": {
        "Q": 5,
        "R": 6
      },
      "nO": 4
    },
    "gH": 2
  }
}`},
		{"toml", `aBcD = 1
[eF]
gH = 2
iJk = 3
[eF.Lm]
nO = 4
[eF.Lm.P]
Q = 5
R = 6
`},
	} {
		doTestCaseInsensitive(t, config.typ, config.content)
	}
}

func TestCaseInsensitiveSet(t *testing.T) {
	v := New()
	m1 := map[string]any{
		"Foo": 32,
		"Bar": map[any]any{
			"ABc": "A",
			"cDE": "B",
		},
	}

	m2 := map[string]any{
		"Foo": 52,
		"Bar": map[any]any{
			"bCd": "A",
			"eFG": "B",
		},
	}

	v.Set("Given1", m1)
	v.Set("Number1", 42)

	v.SetDefault("Given2", m2)
	v.SetDefault("Number2", 52)

	// Verify SetDefault
	assert.Equal(t, 52, v.Get("number2"))
	assert.Equal(t, 52, v.Get("given2.foo"))
	assert.Equal(t, "A", v.Get("given2.bar.bcd"))
	_, ok := m2["Foo"]
	assert.True(t, ok)

	// Verify Set
	assert.Equal(t, 42, v.Get("number1"))
	assert.Equal(t, 32, v.Get("given1.foo"))
	assert.Equal(t, "A", v.Get("given1.bar.abc"))
	_, ok = m1["Foo"]
	assert.True(t, ok)
}

func TestParseNested(t *testing.T) {
	v := New()
	type duration struct {
		Delay time.Duration
	}

	type item struct {
		Name   string
		Delay  time.Duration
		Nested duration
	}

	config := `[[parent]]
	delay="100ms"
	[parent.nested]
	delay="200ms"
`
	initConfig("toml", config, v)

	var items []item
	err := v.UnmarshalKey("parent", &items)
	require.NoError(t, err, "unable to decode into struct")

	assert.Len(t, items, 1)
	assert.Equal(t, 100*time.Millisecond, items[0].Delay)
	assert.Equal(t, 200*time.Millisecond, items[0].Nested.Delay)
}

func doTestCaseInsensitive(t *testing.T, typ, config string) {
	v := New()
	initConfig(typ, config, v)
	v.Set("RfD", true)
	assert.Equal(t, true, v.Get("rfd"))
	assert.Equal(t, true, v.Get("rFD"))
	assert.Equal(t, 1, cast.ToInt(v.Get("abcd")))
	assert.Equal(t, 1, cast.ToInt(v.Get("Abcd")))
	assert.Equal(t, 2, cast.ToInt(v.Get("ef.gh")))
	assert.Equal(t, 3, cast.ToInt(v.Get("ef.ijk")))
	assert.Equal(t, 4, cast.ToInt(v.Get("ef.lm.no")))
	assert.Equal(t, 5, cast.ToInt(v.Get("ef.lm.p.q")))
}

func newViperWithConfigFile(t *testing.T) (*Viper, string) {
	watchDir := t.TempDir()
	configFile := path.Join(watchDir, "config.yaml")
	err := os.WriteFile(configFile, []byte("foo: bar\n"), 0o640)
	require.NoError(t, err)
	v := New()
	v.SetConfigFile(configFile)
	err = v.ReadInConfig()
	require.NoError(t, err)
	require.Equal(t, "bar", v.Get("foo"))
	return v, configFile
}

func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string) {
	watchDir := t.TempDir()
	dataDir1 := path.Join(watchDir, "data1")
	err := os.Mkdir(dataDir1, 0o777)
	require.NoError(t, err)
	realConfigFile := path.Join(dataDir1, "config.yaml")
	t.Logf("Real config file location: %s\n", realConfigFile)
	err = os.WriteFile(realConfigFile, []byte("foo: bar\n"), 0o640)
	require.NoError(t, err)
	// now, symlink the tm `data1` dir to `data` in the baseDir
	os.Symlink(dataDir1, path.Join(watchDir, "data"))
	// and link the `<watchdir>/datadir1/config.yaml` to `<watchdir>/config.yaml`
	configFile := path.Join(watchDir, "config.yaml")
	os.Symlink(path.Join(watchDir, "data", "config.yaml"), configFile)
	t.Logf("Config file location: %s\n", path.Join(watchDir, "config.yaml"))
	// init Viper
	v := New()
	v.SetConfigFile(configFile)
	err = v.ReadInConfig()
	require.NoError(t, err)
	require.Equal(t, "bar", v.Get("foo"))
	return v, watchDir, configFile
}

func TestWatchFile(t *testing.T) {
	if runtime.GOOS == "linux" {
		// TODO(bep) FIX ME
		t.Skip("Skip test on Linux ...")
	}

	t.Run("file content changed", func(t *testing.T) {
		// given a `config.yaml` file being watched
		v, configFile := newViperWithConfigFile(t)
		_, err := os.Stat(configFile)
		require.NoError(t, err)
		t.Logf("test config file: %s\n", configFile)
		wg := sync.WaitGroup{}
		wg.Add(1)
		var wgDoneOnce sync.Once // OnConfigChange is called twice on Windows
		v.OnConfigChange(func(_ fsnotify.Event) {
			t.Logf("config file changed")
			wgDoneOnce.Do(func() {
				wg.Done()
			})
		})
		v.WatchConfig()
		// when overwriting the file and waiting for the custom change notification handler to be triggered
		err = os.WriteFile(configFile, []byte("foo: baz\n"), 0o640)
		wg.Wait()
		// then the config value should have changed
		require.NoError(t, err)
		assert.Equal(t, "baz", v.Get("foo"))
	})

	t.Run("link to real file changed (à la Kubernetes)", func(t *testing.T) {
		// skip if not executed on Linux
		if runtime.GOOS != "linux" {
			t.Skipf("Skipping test as symlink replacements don't work on non-linux environment...")
		}
		v, watchDir, _ := newViperWithSymlinkedConfigFile(t)
		wg := sync.WaitGroup{}
		v.WatchConfig()
		v.OnConfigChange(func(_ fsnotify.Event) {
			t.Logf("config file changed")
			wg.Done()
		})
		wg.Add(1)
		// when link to another `config.yaml` file
		dataDir2 := path.Join(watchDir, "data2")
		err := os.Mkdir(dataDir2, 0o777)
		require.NoError(t, err)
		configFile2 := path.Join(dataDir2, "config.yaml")
		err = os.WriteFile(configFile2, []byte("foo: baz\n"), 0o640)
		require.NoError(t, err)
		// change the symlink using the `ln -sfn` command
		err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
		require.NoError(t, err)
		wg.Wait()
		// then
		require.NoError(t, err)
		assert.Equal(t, "baz", v.Get("foo"))
	})
}

func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
	flags.String("foo.bar", "cobra_flag", "")

	v := New()
	assert.NoError(t, v.BindPFlags(flags))

	config := &struct {
		Foo struct {
			Bar string
		}
	}{}

	assert.NoError(t, v.Unmarshal(config))
	assert.Equal(t, "cobra_flag", config.Foo.Bar)
}

// var yamlExampleWithDot = []byte(`Hacker: true
// name: steve
// hobbies:
//     - skateboarding
//     - snowboarding
//     - go
// clothing:
//     jacket: leather
//     trousers: denim
//     pants:
//         size: large
// age: 35
// eyes : brown
// beard: true
// emails:
//     steve@hacker.com:
//         created: 01/02/03
//         active: true
// `)

func TestKeyDelimiter(t *testing.T) {
	v := NewWithOptions(KeyDelimiter("::"))
	v.SetConfigType("yaml")
	r := strings.NewReader(string(yamlExampleWithDot))

	err := v.unmarshalReader(r, v.config)
	require.NoError(t, err)

	values := map[string]any{
		"image": map[string]any{
			"repository": "someImage",
			"tag":        "1.0.0",
		},
		"ingress": map[string]any{
			"annotations": map[string]any{
				"traefik.frontend.rule.type":                 "PathPrefix",
				"traefik.ingress.kubernetes.io/ssl-redirect": "true",
			},
		},
	}

	v.SetDefault("charts::values", values)

	assert.Equal(t, "leather", v.GetString("clothing::jacket"))
	assert.Equal(t, "01/02/03", v.GetString("emails::steve@hacker.com::created"))

	type config struct {
		Charts struct {
			Values map[string]any
		}
	}

	expected := config{
		Charts: struct {
			Values map[string]any
		}{
			Values: values,
		},
	}

	var actual config

	require.NoError(t, v.Unmarshal(&actual))

	assert.Equal(t, expected, actual)
}

var yamlDeepNestedSlices = []byte(`TV:
- title: "The Expanse"
  title_i18n:
    USA: "The Expanse"
    Japan: "エクスパンス -巨獣めざめる-"
  seasons:
  - first_released: "December 14, 2015"
    episodes:
    - title: "Dulcinea"
      air_date: "December 14, 2015"
    - title: "The Big Empty"
      air_date: "December 15, 2015"
    - title: "Remember the Cant"
      air_date: "December 22, 2015"
  - first_released: "February 1, 2017"
    episodes:
    - title: "Safe"
      air_date: "February 1, 2017"
    - title: "Doors & Corners"
      air_date: "February 1, 2017"
    - title: "Static"
      air_date: "February 8, 2017"
  episodes:
    - ["Dulcinea", "The Big Empty", "Remember the Cant"]
    - ["Safe", "Doors & Corners", "Static"]
`)

func TestSliceIndexAccess(t *testing.T) {
	v := New()
	v.SetConfigType("yaml")
	r := strings.NewReader(string(yamlDeepNestedSlices))

	err := v.unmarshalReader(r, v.config)
	require.NoError(t, err)

	assert.Equal(t, "The Expanse", v.GetString("tv.0.title"))
	assert.Equal(t, "February 1, 2017", v.GetString("tv.0.seasons.1.first_released"))
	assert.Equal(t, "Static", v.GetString("tv.0.seasons.1.episodes.2.title"))
	assert.Equal(t, "December 15, 2015", v.GetString("tv.0.seasons.0.episodes.1.air_date"))

	// Test nested keys with capital letters
	assert.Equal(t, "The Expanse", v.GetString("tv.0.title_i18n.USA"))
	assert.Equal(t, "エクスパンス -巨獣めざめる-", v.GetString("tv.0.title_i18n.Japan"))

	// Test for index out of bounds
	assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released"))

	// Accessing multidimensional arrays
	assert.Equal(t, "Static", v.GetString("tv.0.episodes.1.2"))
}

func TestIsPathShadowedInFlatMap(t *testing.T) {
	v := New()

	stringMap := map[string]string{
		"foo": "value",
	}

	flagMap := map[string]FlagValue{
		"foo": pflagValue{},
	}

	path1 := []string{"foo", "bar"}
	expected1 := "foo"

	// "foo.bar" should shadowed by "foo"
	assert.Equal(t, expected1, v.isPathShadowedInFlatMap(path1, stringMap))
	assert.Equal(t, expected1, v.isPathShadowedInFlatMap(path1, flagMap))

	path2 := []string{"bar", "foo"}
	expected2 := ""

	// "bar.foo" should not shadowed by "foo"
	assert.Equal(t, expected2, v.isPathShadowedInFlatMap(path2, stringMap))
	assert.Equal(t, expected2, v.isPathShadowedInFlatMap(path2, flagMap))
}

func TestFlagShadow(t *testing.T) {
	v := New()

	v.SetDefault("foo.bar1.bar2", "default")

	flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
	flags.String("foo.bar1", "shadowed", "")
	flags.VisitAll(func(flag *pflag.Flag) {
		flag.Changed = true
	})

	v.BindPFlags(flags)

	assert.Equal(t, "shadowed", v.GetString("foo.bar1"))
	// the default "foo.bar1.bar2" value should shadowed by flag "foo.bar1" value
	// and should return an empty string
	assert.Equal(t, "", v.GetString("foo.bar1.bar2"))
}

func BenchmarkGetBool(b *testing.B) {
	key := "BenchmarkGetBool"
	v = New()
	v.Set(key, true)

	for i := 0; i < b.N; i++ {
		if !v.GetBool(key) {
			b.Fatal("GetBool returned false")
		}
	}
}

func BenchmarkGet(b *testing.B) {
	key := "BenchmarkGet"
	v = New()
	v.Set(key, true)

	for i := 0; i < b.N; i++ {
		if !v.Get(key).(bool) {
			b.Fatal("Get returned false")
		}
	}
}

// BenchmarkGetBoolFromMap is the "perfect result" for the above.
func BenchmarkGetBoolFromMap(b *testing.B) {
	m := make(map[string]bool)
	key := "BenchmarkGetBool"
	m[key] = true

	for i := 0; i < b.N; i++ {
		if !m[key] {
			b.Fatal("Map value was false")
		}
	}
}

// Skip some tests on Windows that kept failing when Windows was added to the CI as a target.
func skipWindows(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Skip test on Windows")
	}
}
