step 1: calling local:qwen3.6:35b-a3b
plan: Let me start by exploring the repository structure and running the tests.
tool list <- {"path":"."}
tool list ok: .campaign/
.editorconfig
.envrc
.git/
.github/
.gitignore
.golangci.yaml
.yamlignore
.yamllint.yaml
LICENSE
Makefile
README.md
TROUBLESHOOTING.md
UPGRADE.md
encoding.go
encoding_test.go
errors.go
experimental.go
file.go
finder.go
finder_example_test.go
finder_test.go
flags.go
flags_test.go
flake.lock
flake.nix
go.mod
go.sum
internal/
logger.go
overrides_test.go
remote.go
remote/
util.go
util_test.go
viper.go
viper_test.go
viper_yaml_test.go
[evidence record sha256:59dde91b9f90b163bb91651ca815a1b9d6b2527e120589658ef5cf10e6ff6e17 kind tool-call:list]
step 2: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go test ./... 2>&1 | head -200","timeoutMs":120000}
tool shell failed: denied: "go test ./... 2>&1 | head -200" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:a5f412a5f34f4c5051cfcbb47d55a2dc93d1f30f248f1dde492b46b5325ea259 kind tool-call:shell]
step 3: calling local:qwen3.6:35b-a3b
tool read <- {"path":"Makefile"}
tool read ok: # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html

OS = $(shell uname | tr A-Z a-z)
export PATH := $(abspath bin/):${PATH}

# Build variables
BUILD_DIR ?= build
export CGO_ENABLED ?= 0
export GOOS = $(shell go env GOOS)
ifeq (${VERBOSE}, 1)
ifeq ($(filter -v,${GOARGS}),)
	GOARGS += -v
endif
TEST_FORMAT = short-verbose
endif

# Dependency versions
GOTESTSUM_VERSION = 1.9.0
GOLANGCI_VERSION = 1.53.3

# Add the ability to override some variables
# Use with care
-include override.mk

.PHONY: clear
clear: ## Clear the working area and the project
	rm -rf bin/

.PHONY: check
check: test lint ## Run tests and linters


TEST_PKGS ?= ./...
.PHONY: test
test: TEST_FORMAT ?= short
test: SHELL = /bin/bash
test: export CGO_ENABLED=1
test: bin/gotestsum ## Run tests
	@mkdir -p ${BUILD_DIR}
	bin/gotestsum --no-summary=skipped --junitfile ${BUILD_DIR}/coverage.xml --format ${TEST_FORMAT} -- -race -coverprofile=${BUILD_DIR}/coverage.txt -covermode=atomic $(filter-out -v,${GOARGS}) $(if ${TEST_PKGS},${TEST_PKGS},./...)

.PHONY: lint
lint: lint-go lint-yaml
lint: ## Run linters

.PHONY: lint-go
lint-go:
	golangci-lint run $(if ${CI},--out-format github-actions,)

.PHONY: lint-yaml
lint-yaml:
	yamllint $(if ${CI},-f github,) --no-warnings .

.PHONY: fmt
fmt: ## Format code
	golangci-lint run --fix

deps: bin/golangci-lint bin/gotestsum yamllint
deps: ## Install dependencies

bin/gotestsum:
	@mkdir -p bin
	curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_${OS}_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum && chmod +x ./bin/gotestsum

bin/golangci-lint:
	@mkdir -p bin
	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- v${GOLANGCI_VERSION}

.PHONY: yamllint
yamllint:
	pip3 install --user yamllint

# Add custom targets here
-include custom.mk

.PHONY: list
list: ## List all make targets
	@${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort

.PHONY: help
.DEFAULT_GOAL := help
help:
	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

# Variable outputting/exporting rules
var-%: ; @echo $($*)
varexport-%: ; @echo $*=$($*)

[evidence record sha256:c6cc21cf143403a8dae67b9c90183c258f1eaddd1af2f1613f504daae7bb0c49 kind tool-call:read]
step 4: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"make test TEST_PKGS=./... 2>&1 | tail -300","timeoutMs":300000}
tool shell failed: denied: "make test TEST_PKGS=./... 2>&1 | tail -300" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:60ca5a04d816f59a951e60c19d2507558c56c338041ffd82d51c72704b0f98bf kind tool-call:shell]
step 5: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go version"}
tool shell failed: denied: "go version" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:11e07683a183cebac36c194aa61a6e7521b7c7b5aba2cc5c4e16d69fd325b7b7 kind tool-call:shell]
step 6: calling local:qwen3.6:35b-a3b
tool list <- {"path":".campaign"}
tool list ok: gocache/
gomod/
[evidence record sha256:d9d38f32130627e58cf76e812a9931b2892a61095d5270ecea632905e3128dab kind tool-call:list]
step 7: calling local:qwen3.6:35b-a3b
tool read <- {"path":"go.mod"}
tool read ok: module github.com/spf13/viper

go 1.23.0

require (
	github.com/fsnotify/fsnotify v1.9.0
	github.com/go-viper/mapstructure/v2 v2.4.0
	github.com/pelletier/go-toml/v2 v2.2.4
	github.com/sagikazarmark/locafero v0.12.0
	github.com/spf13/afero v1.15.0
	github.com/spf13/cast v1.10.0
	github.com/spf13/pflag v1.0.10
	github.com/stretchr/testify v1.11.1
	github.com/subosito/gotenv v1.6.0
	go.yaml.in/yaml/v3 v3.0.4
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/google/go-cmp v0.6.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	golang.org/x/sys v0.29.0 // indirect
	golang.org/x/text v0.28.0 // indirect
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

[evidence record sha256:bb9ca52a1abfe25372f1a2dd1632f851e0c30e573a29560d2d4da552417f33c5 kind tool-call:read]
step 8: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go test ./... -v 2>&1 -count=1","timeoutMs":180000}
tool shell failed: denied: "go test ./... -v 2>&1 -count=1" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:39426dce4d91d41fe3302ec90f3662d7cd3eefb26f936472432b9f617d074b4c kind tool-call:shell]
step 9: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"func Test","path":".","maxResults":50}
tool search ok: .campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/common_test.go:159: func TestSortValues(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/common_test.go:233: func TestSortValuesWithMethods(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/common_test.go:268: func TestSortValuesWithSpew(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/dump_test.go:960: func TestDump(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/dump_test.go:994: func TestDumpSortedKeys(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/format_test.go:1465: func TestFormatter(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/format_test.go:1516: func TestPrintSortedKeys(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/internal_test.go:54: func TestInvalidReflectValue(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/internalunsafe_test.go:52: func TestAddedReflectValue(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/spew_test.go:210: func TestSpew(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_fen_test.go:11: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_inotify_test.go:15: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_inotify_test.go:55: func TestInotifyOverflow(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_inotify_test.go:117: func TestInotifyDeleteOpenFile(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_kqueue_test.go:11: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_windows_test.go:11: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_windows_test.go:58: func TestWindowsRemWatch(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:32: func TestScript(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:54: func TestWatchMultipleWrite(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:87: func TestWatchRemoveOpenFd(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:120: func TestWatchRemoveWatchedDir(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:183: func TestClose(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:396: func TestAdd(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:598: func TestRemove(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:828: func TestEventString(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:856: func TestWatchList(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:877: func TestOpHas(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:1039: func TestRace(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:1137: func TestNewWatcher(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:31: func TestGetOptCodes(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:52: func TestGroupedOpCodes(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:96: func TestWithAsciiOneInsert(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:108: func TestWithAsciiOnDelete(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:115: func TestSFBugsComparingEmptyLists(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:122: func TestOutputFormatRangeFormatUnified(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:139: func TestDiffMatch(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:237: func TestDiffJSON(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:100: func TestComposeDecodeHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:129: func TestComposeDecodeHookFunc_err(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:147: func TestComposeDecodeHookFunc_kinds(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:179: func TestOrComposeDecodeHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:208: func TestOrComposeDecodeHookFunc_correctValueIsLast(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:245: func TestOrComposeDecodeHookFunc_err(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:274: func TestComposeDecodeHookFunc_safe_nofuncs(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:306: func TestComposeDecodeHookFunc_ReflectValueHook(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:336: func TestStringToSliceHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:451: func TestStringToWeakSliceHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:508: func TestStringToTimeDurationHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:607: func TestStringToTimeLocationHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:636: func TestStringToURLHookFunc(t *testing.T) {
[evidence record sha256:f5b02a519b0fc785ea8eff381e6de25fa292bc1d1a9e44f10f9622b87e98d1d9 kind tool-call:search]
step 10: calling local:qwen3.6:35b-a3b
tool list <- {"path":"internal"}
tool list ok: encoding/
features/
testutil/
[evidence record sha256:519d88cf52bbd19746f35aa1731d278ff5e186b86da06193b442c5af3721cbfc kind tool-call:list]
step 11: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"echo hello"}
tool shell failed: denied: "echo hello" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:be43895921eb399d18447552f5dec42d4b6f10bdeb298ea3fcc206af44a4c89e kind tool-call:shell]
step 12: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper.go"}
tool read ok: // 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.

// Viper is an application configuration system.
// It believes that applications can be configured a variety of ways
// via flags, ENVIRONMENT variables, configuration files retrieved
// from the file system, or a remote key/value store.

// Each item takes precedence over the item below it:

// overrides
// flag
// env
// config
// key/value store
// default

package viper

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	fs "io/fs"
	"log/slog"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/go-viper/mapstructure/v2"
	"github.com/spf13/afero"
	"github.com/spf13/cast"
	"github.com/spf13/pflag"

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

var v *Viper

func init() {
	v = New()
}

// A DecoderConfigOption can be passed to viper.Unmarshal to configure
// mapstructure.DecoderConfig options.
type DecoderConfigOption func(*mapstructure.DecoderConfig)

// DecodeHook returns a DecoderConfigOption which overrides the default
// DecoderConfig.DecodeHook value, the default is:
//
//	 mapstructure.ComposeDecodeHookFunc(
//			mapstructure.StringToTimeDurationHookFunc(),
//			mapstructure.StringToSliceHookFunc(","),
//		)
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
	return func(c *mapstructure.DecoderConfig) {
		c.DecodeHook = hook
	}
}

// Viper is a prioritized configuration registry. It
// maintains a set of configuration sources, fetches
// values to populate those, and provides them according
// to the source's priority.
// The priority of the sources is the following:
// 1. overrides
// 2. flags
// 3. env. variables
// 4. config file
// 5. key/value store
// 6. defaults
//
// For example, if values from the following sources were loaded:
//
//	Defaults : {
//		"secret": "",
//		"user": "default",
//		"endpoint": "https://localhost"
//	}
//	Config : {
//		"user": "root"
//		"secret": "defaultsecret"
//	}
//	Env : {
//		"secret": "somesecretkey"
//	}
//
// The resulting config will have the following values:
//
//	{
//		"secret": "somesecretkey",
//		"user": "root",
//		"endpoint": "https://localhost"
//	}
//
// Note: Vipers are not safe for concurrent Get() and Set() operations.
type Viper struct {
	// Delimiter that separates a list of keys
	// used to access a nested value in one go
	keyDelim string

	// A set of paths to look for the config file in
	configPaths []string

	// The filesystem to read config from.
	fs afero.Fs

	finder Finder

	// A set of remote providers to search for the configuration
	remoteProviders []*defaultRemoteProvider

	// Name of file to look for inside the path
	configName        string
	configFile        string
	configType        string
	configPermissions os.FileMode
	envPrefix         string

	automaticEnvApplied bool
	envKeyReplacer      StringReplacer
	allowEmptyEnv       bool

	parents        []string
	config         map[string]any
	override       map[string]any
	defaults       map[string]any
	kvstore        map[string]any
	pflags         map[string]FlagValue
	env            map[string][]string
	aliases        map[string]string
	typeByDefValue bool

	onConfigChange func(fsnotify.Event)

	logger *slog.Logger

	encoderRegistry EncoderRegistry
	decoderRegistry DecoderRegistry

	decodeHook mapstructure.DecodeHookFunc

	experimentalFinder     bool
	experimentalBindStruct bool
}

// New returns an initialized Viper instance.
func New() *Viper {
	v := new(Viper)
	v.keyDelim = "."
	v.configName = "config"
	v.configPermissions = os.FileMode(0o644)
	v.fs = afero.NewOsFs()
	v.config = make(map[string]any)
	v.parents = []string{}
	v.override = make(map[string]any)
	v.defaults = make(map[string]any)
	v.kvstore = make(map[string]any)
	v.pflags = make(map[string]FlagValue)
	v.env = make(map[string][]string)
	v.aliases = make(map[string]string)
	v.typeByDefValue = false
	v.logger = slog.New(&discardHandler{})

	codecRegistry := NewCodecRegistry()

	v.encoderRegistry = codecRegistry
	v.decoderRegistry = codecRegistry

	v.experimentalFinder = features.Finder
	v.experimentalBindStruct = features.BindStruct

	return v
}

// Option configures Viper using the functional options paradigm popularized by Rob Pike and Dave Cheney.
// If you're unfamiliar with this style,
// see https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html and
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis.
type Option interface {
	apply(v *Viper)
}

type optionFunc func(v *Viper)

func (fn optionFunc) apply(v *Viper) {
	fn(v)
}

// KeyDelimiter sets the delimiter used for determining key parts.
// By default it's value is ".".
func KeyDelimiter(d string) Option {
	return optionFunc(func(v *Viper) {
		v.keyDelim = d
	})
}

// StringReplacer applies a set of replacements to a string.
type StringReplacer interface {
	// Replace returns a copy of s with all replacements performed.
	Replace(s string) string
}

// EnvKeyReplacer sets a replacer used for mapping environment variables to internal keys.
func EnvKeyReplacer(r StringReplacer) Option {
	return optionFunc(func(v *Viper) {
		if r == nil {
			return
		}

		v.envKeyReplacer = r
	})
}

// WithDecodeHook sets a default decode hook for mapstructure.
func WithDecodeHook(h mapstructure.DecodeHookFunc) Option {
	return optionFunc(func(v *Viper) {
		if h == nil {
			return
		}

		v.decodeHook = h
	})
}

// NewWithOptions creates a new Viper instance.
func NewWithOptions(opts ...Option) *Viper {
	v := New()

	for _, opt := range opts {
		opt.apply(v)
	}

	return v
}

// SetOptions sets the options on the global Viper instance.
//
// Be careful when using this function: subsequent calls may override options you set.
// It's always better to use a local Viper instance.
func SetOptions(opts ...Option) {
	for _, opt := range opts {
		opt.apply(v)
	}
}

// Reset is intended for testing, will reset all to default settings.
// In the public interface for the viper package so applications
// can use it in their testing as well.
func Reset() {
	v = New()
	SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

	resetRemote()
}

// SupportedExts are universally supported extensions.
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

// OnConfigChange sets the event handler that is called when a config file changes.
func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) }

// OnConfigChange sets the event handler that is called when a config file changes.
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
	v.onConfigChange = run
}

// WatchConfig starts watching a config file for changes.
func WatchConfig() { v.WatchConfig() }

// WatchConfig starts watching a config file for changes.
func (v *Viper) WatchConfig() {
	initWG := sync.WaitGroup{}
	initWG.Add(1)
	go func() {
		watcher, err := fsnotify.NewWatcher()
		if err != nil {
			v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err))
			os.Exit(1)
		}
		defer watcher.Close()
		// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
		filename, err := v.getConfigFile()
		if err != nil {
			v.logger.Error(fmt.Sprintf("get config file: %s", err))
			initWG.Done()
			return
		}

		configFile := filepath.Clean(filename)
		configDir, _ := filepath.Split(configFile)
		realConfigFile, _ := filepath.EvalSymlinks(filename)

		eventsWG := sync.WaitGroup{}
		eventsWG.Add(1)
		go func() {
			for {
				select {
				case event, ok := <-watcher.Events:
					if !ok { // 'Events' channel is closed
						eventsWG.Done()
						return
					}
					currentConfigFile, _ := filepath.EvalSymlinks(filename)
					// we only care about the config file with the following cases:
					// 1 - if the config file was modified or created
					// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
					if (filepath.Clean(event.Name) == configFile &&
						(event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) ||
						(currentConfigFile != "" && currentConfigFile != realConfigFile) {
						realConfigFile = currentConfigFile
						err := v.ReadInConfig()
						if err != nil {
							v.logger.Error(fmt.Sprintf("read config file: %s", err))
						}
						if v.onConfigChange != nil {
							v.onConfigChange(event)
						}
					} else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) {
						eventsWG.Done()
						return
					}

				case err, ok := <-watcher.Errors:
					if ok { // 'Errors' channel is not closed
						v.logger.Error(fmt.Sprintf("watcher error: %s", err))
					}
					eventsWG.Done()
					return
				}
			}
		}()
		err = watcher.Add(configDir)
		if err != nil {
			v.logger.Error(fmt.Sprintf("failed to add watcher: %s", err))
			initWG.Done()
			return
		}
		initWG.Done()   // done initializing the watch in this go routine, so the parent routine can move on...
		eventsWG.Wait() // now, wait for event loop to end in this go-routine...
	}()
	initWG.Wait() // make sure that the go routine above fully ended before returning
}

// SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths.
func SetConfigFile(in string) { v.SetConfigFile(in) }

// SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths.
func (v *Viper) SetConfigFile(in string) {
	if in != "" {
		v.configFile = in
	}
}

// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use.
// E.g. if your prefix is "spf", the env registry will look for env
// variables that start with "SPF_".
func SetEnvPrefix(in string) { v.SetEnvPrefix(in) }

// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use.
// E.g. if your prefix is "spf", the env registry will look for env
// variables that start with "SPF_".
func (v *Viper) SetEnvPrefix(in string) {
	if in != "" {
		v.envPrefix = in
	}
}

// GetEnvPrefix returns the environment variable prefix.
func GetEnvPrefix() string { return v.GetEnvPrefix() }

// GetEnvPrefix returns the environment variable prefix.
func (v *Viper) GetEnvPrefix() string {
	return v.envPrefix
}

func (v *Viper) mergeWithEnvPrefix(in string) string {
	if v.envPrefix != "" {
		return strings.ToUpper(v.envPrefix + "_" + in)
	}

	return strings.ToUpper(in)
}

// AllowEmptyEnv tells Viper to consider set,
// but empty environment variables as valid values instead of falling back.
// For backward compatibility reasons this is false by default.
func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) }

// AllowEmptyEnv tells Viper to consider set,
// but empty environment variables as valid values instead of falling back.
// For backward compatibility reasons this is false by default.
func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) {
	v.allowEmptyEnv = allowEmptyEnv
}

// TODO: should getEnv logic be moved into find(). Can generalize the use of
// rewriting keys many things, Ex: Get('someKey') -> some_key
// (camel case to snake case for JSON keys perhaps)

// getEnv is a wrapper around os.Getenv which replaces characters in the original
// key. This allows env vars which have different keys than the config object
// keys.
func (v *Viper) getEnv(key string) (string, bool) {
	if v.envKeyReplacer != nil {
		key = v.envKeyReplacer.Replace(key)
	}

	val, ok := os.LookupEnv(key)

	return val, ok && (v.allowEmptyEnv || val != "")
}

// ConfigFileUsed returns the file used to populate the config registry.
func ConfigFileUsed() string { return v.ConfigFileUsed() }

// ConfigFileUsed returns the file used to populate the config registry.
func (v *Viper) ConfigFileUsed() string { return v.configFile }

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func AddConfigPath(in string) { v.AddConfigPath(in) }

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func (v *Viper) AddConfigPath(in string) {
	if v.finder != nil {
		v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "AddConfigPath"))
	}

	if in != "" {
		absin := absPathify(v.logger, in)

		v.logger.Info("adding path to search paths", "path", absin)
		if !slices.Contains(v.configPaths, absin) {
			v.configPaths = append(v.configPaths, absin)
		}
	}
}

// searchMap recursively searches for a value for path in source map.
// Returns nil if not found.
// Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchMap(source map[string]any, path []string) any {
	if len(path) == 0 {
		return source
	}

	next, ok := source[path[0]]
	if ok {
		// Fast path
		if len(path) == 1 {
			return next
		}

		// Nested case
		switch next := next.(type) {
		case map[any]any:
			return v.searchMap(cast.ToStringMap(next), path[1:])
		case map[string]any:
			// Type assertion is safe here since it is only reached
			// if the type of `next` is the same as the type being asserted
			return v.searchMap(next, path[1:])
		default:
			// got a value but nested key expected, return "nil" for not found
			return nil
		}
	}
	return nil
}

// searchIndexableWithPathPrefixes recursively searches for a value for path in source map/slice.
//
// While searchMap() considers each path element as a single map key or slice index, this
// function searches for, and prioritizes, merged path elements.
// e.g., if in the source, "foo" is defined with a sub-key "bar", and "foo.bar"
// is also defined, this latter value is returned for path ["foo", "bar"].
//
// This should be useful only at config level (other maps may not contain dots
// in their keys).
//
// Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchIndexableWithPathPrefixes(source any, path []string) any {
	if len(path) == 0 {
		return source
	}

	// search for path prefixes, starting from the longest one
	for i := len(path); i > 0; i-- {
		prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim))

		var val any
		switch sourceIndexable := source.(type) {
		case []any:
			val = v.searchSliceWithPathPrefixes(sourceIndexable, prefixKey, i, path)
		case map[string]any:
			val = v.searchMapWithPathPrefixes(sourceIndexable, prefixKey, i, path)
		}
		if val != nil {
			return val
		}
	}

	// not found
	return nil
}

// searchSliceWithPathPrefixes searches for a value for path in sourceSlice
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchSliceWithPathPrefixes(
	sourceSlice []any,
	prefixKey string,
	pathIndex int,
	path []string,
) any {
	// if the prefixKey is not a number or it is out of bounds of the slice
	index, err := strconv.Atoi(prefixKey)
	if err != nil || len(sourceSlice) <= index {
		return nil
	}

	next := sourceSlice[index]

	// Fast path
	if pathIndex == len(path) {
		return next
	}

	switch n := next.(type) {
	case map[any]any:
		return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
	case map[string]any, []any:
		return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
	default:
		// got a value but nested key expected, do nothing and look for next prefix
	}

	// not found
	return nil
}

// searchMapWithPathPrefixes searches for a value for path in sourceMap
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchMapWithPathPrefixes(
	sourceMap map[string]any,
	prefixKey string,
	pathIndex int,
	path []string,
) any {
	next, ok := sourceMap[prefixKey]
	if !ok {
		return nil
	}

	// Fast path
	if pathIndex == len(path) {
		return next
	}

	// Nested case
	switch n := next.(type) {
	case map[any]any:
		return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
	case map[string]any, []any:
		return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
	default:
		// got a value but nested key expected, do nothing and look for next prefix
	}

	// not found
	return nil
}

// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
// on its path in the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]any) string {
	var parentVal any
	for i := 1; i < len(path); i++ {
		parentVal = v.searchMap(m, path[0:i])
		if parentVal == nil {
			// not found, no need to add more path elements
			return ""
		}
		switch parentVal.(type) {
		case map[any]any:
			continue
		case map[string]any:
			continue
		default:
			// parentVal is a regular value which shadows "path"
			return strings.Join(path[0:i], v.keyDelim)
		}
	}
	return ""
}

// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
// in a sub-path of the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInFlatMap(path []string, mi any) string {
	// unify input map
	var m map[string]interface{}
	switch miv := mi.(type) {
	case map[string]string:
		m = castMapStringToMapInterface(miv)
	case map[string]FlagValue:
		m = castMapFlagToMapInterface(miv)
	default:
		return ""
	}

	// scan paths
	var parentKey string
	for i := 1; i < len(path); i++ {
		parentKey = strings.Join(path[0:i], v.keyDelim)
		if _, ok := m[parentKey]; ok {
			return parentKey
		}
	}
	return ""
}

// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
// in the environment, when automatic env is on.
// e.g., if "foo.bar" has a value in the environment, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
	var parentKey string
	for i := 1; i < len(path); i++ {
		parentKey = strings.Join(path[0:i], v.keyDelim)
		if _, ok := v.getEnv(v.mergeWithEnvPrefix(parentKey)); ok {
			return parentKey
		}
	}
	return ""
}

// SetTypeByDefaultValue enables or disables the inference of a key value's
// type when the Get function is used based upon a key's default value as
// opposed to the value returned based on the normal fetch logic.
//
// For example, if a key has a default value of []string{} and the same key
// is set via an environment variable to "a b c", a call to the Get function
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
//	[]string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
//	"a b c"
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }

// SetTypeByDefaultValue enables or disables the inference of a key value's
// type when the Get function is used based upon a key's default value as
// opposed to the value returned based on the normal fetch logic.
//
// For example, if a key has a default value of []string{} and the same key
// is set via an environment variable to "a b c", a call to the Get function
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
//	[]string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
//	"a b c"
func (v *Viper) SetTypeByDefaultValue(enable bool) {
	v.typeByDefValue = enable
}

// GetViper gets the global Viper instance.
func GetViper() *Viper {
	return v
}

// Get can retrieve any value given the key to use.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func Get(key string) any { return v.Get(key) }

// Get retrieves the value associated with the key.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func (v *Viper) Get(key string) any {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, true)
	if val == nil {
		return nil
	}

	if v.typeByDefValue {
		// TODO(bep) this branch isn't covered by a single test.
		valType := val
		path := strings.Split(lcaseKey, v.keyDelim)
		defVal := v.searchMap(v.defaults, path)
		if defVal != nil {
			valType = defVal
		}

		switch valType.(type) {
		case bool:
			return cast.ToBool(val)
		case string:
			return cast.ToString(val)
		case int32, int16, int8, int:
			return cast.ToInt(val)
		case uint:
			return cast.ToUint(val)
		case uint32:
			return cast.ToUint32(val)
		case uint64:
			return cast.ToUint64(val)
		case int64:
			return cast.ToInt64(val)
		case float64, float32:
			return cast.ToFloat64(val)
		case time.Time:
			return cast.ToTime(val)
		case time.Duration:
			return cast.ToDuration(val)
		case []string:
			return cast.ToStringSlice(val)
		case []int:
			return cast.ToIntSlice(val)
		case []time.Duration:
			return cast.ToDurationSlice(val)
		}
	}

	return val
}

// Sub returns new Viper instance representing a sub tree of this instance.
// Sub is case-insensitive for a key.
func Sub(key string) *Viper { return v.Sub(key) }

// Sub returns a new Viper instance representing a sub tree of this instance.
// Sub is case-insensitive for a key.
func (v *Viper) Sub(key string) *Viper {
	subv := New()
	data := v.Get(key)
	if data == nil {
		return nil
	}

	if reflect.TypeOf(data).Kind() == reflect.Map {
		subv.parents = append([]string(nil), v.parents...)
		subv.parents = append(subv.parents, strings.ToLower(key))
		subv.automaticEnvApplied = v.automaticEnvApplied
		subv.envPrefix = v.envPrefix
		subv.envKeyReplacer = v.envKeyReplacer
		subv.keyDelim = v.keyDelim
		subv.config = cast.ToStringMap(data)
		return subv
	}
	return nil
}

// GetString returns the value associated with the key as a string.
func GetString(key string) string { return v.GetString(key) }

// GetString returns the value associated with the key as a string.
func (v *Viper) GetString(key string) string {
	return cast.ToString(v.Get(key))
}

// GetBool returns the value associated with the key as a boolean.
func GetBool(key string) bool { return v.GetBool(key) }

// GetBool returns the value associated with the key as a boolean.
func (v *Viper) GetBool(key string) bool {
	return cast.ToBool(v.Get(key))
}

// GetInt returns the value associated with the key as an integer.
func GetInt(key string) int { return v.GetInt(key) }

// GetInt returns the value associated with the key as an integer.
func (v *Viper) GetInt(key string) int {
	return cast.ToInt(v.Get(key))
}

// GetInt32 returns the value associated with the key as an integer.
func GetInt32(key string) int32 { return v.GetInt32(key) }

// GetInt32 returns the value associated with the key as an integer.
func (v *Viper) GetInt32(key string) int32 {
	return cast.ToInt32(v.Get(key))
}

// GetInt64 returns the value associated with the key as an integer.
func GetInt64(key string) int64 { return v.GetInt64(key) }

// GetInt64 returns the value associated with the key as an integer.
func (v *Viper) GetInt64(key string) int64 {
	return cast.ToInt64(v.Get(key))
}

// GetUint8 returns the value associated with the key as an unsigned integer.
func GetUint8(key string) uint8 { return v.GetUint8(key) }

// GetUint8 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint8(key string) uint8 {
	return cast.ToUint8(v.Get(key))
}

// GetUint returns the value associated with the key as an unsigned integer.
func GetUint(key string) uint { return v.GetUint(key) }

// GetUint returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint(key string) uint {
	return cast.ToUint(v.Get(key))
}

// GetUint16 returns the value associated with the key as an unsigned integer.
func GetUint16(key string) uint16 { return v.GetUint16(key) }

// GetUint16 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint16(key string) uint16 {
	return cast.ToUint16(v.Get(key))
}

// GetUint32 returns the value associated with the key as an unsigned integer.
func GetUint32(key string) uint32 { return v.GetUint32(key) }

// GetUint32 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint32(key string) uint32 {
	return cast.ToUint32(v.Get(key))
}

// GetUint64 returns the value associated with the key as an unsigned integer.
func GetUint64(key string) uint64 { return v.GetUint64(key) }

// GetUint64 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint64(key string) uint64 {
	return cast.ToUint64(v.Get(key))
}

// GetFloat64 returns the value associated with the key as a float64.
func GetFloat64(key string) float64 { return v.GetFloat64(key) }

// GetFloat64 returns the value associated with the key as a float64.
func (v *Viper) GetFloat64(key string) float64 {
	return cast.ToFloat64(v.Get(key))
}

// GetTime returns the value associated with the key as time.
func GetTime(key string) time.Time { return v.GetTime(key) }

// GetTime returns the value associated with the key as time.
func (v *Viper) GetTime(key string) time.Time {
	return cast.ToTime(v.Get(key))
}

// GetDuration returns the value associated with the key as a duration.
func GetDuration(key string) time.Duration { return v.GetDuration(key) }

// GetDuration returns the value associated with the key as a duration.
func (v *Viper) GetDuration(key string) time.Duration {
	return cast.ToDuration(v.Get(key))
}

// GetIntSlice returns the value associated with the key as a slice of int values.
func GetIntSlice(key string) []int { return v.GetIntSlice(key) }

// GetIntSlice returns the value associated with the key as a slice of int values.
func (v *Viper) GetIntSlice(key string) []int {
	return cast.ToIntSlice(v.Get(key))
}

// GetStringSlice returns the value associated with the key as a slice of strings.
func GetStringSlice(key string) []string { return v.GetStringSlice(key) }

// GetStringSlice returns the value associated with the key as a slice of strings.
func (v *Viper) GetStringSlice(key string) []string {
	return cast.ToStringSlice(v.Get(key))
}

// GetStringMap returns the value associated with the key as a map of interfaces.
func GetStringMap(key string) map[string]any { return v.GetStringMap(key) }

// GetStringMap returns the value associated with the key as a map of interfaces.
func (v *Viper) GetStringMap(key string) map[string]any {
	return cast.ToStringMap(v.Get(key))
}

// GetStringMapString returns the value associated with the key as a map of strings.
func GetStringMapString(key string) map[string]string { return v.GetStringMapString(key) }

// GetStringMapString returns the value associated with the key as a map of strings.
func (v *Viper) GetStringMapString(key string) map[string]string {
	return cast.ToStringMapString(v.Get(key))
}

// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func GetStringMapStringSlice(key string) map[string][]string { return v.GetStringMapStringSlice(key) }

// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (v *Viper) GetStringMapStringSlice(key string) map[string][]string {
	return cast.ToStringMapStringSlice(v.Get(key))
}

// GetSizeInBytes returns the size of the value associated with the given key
// in bytes.
func GetSizeInBytes(key string) uint { return v.GetSizeInBytes(key) }

// GetSizeInBytes returns the size of the value associated with the given key
// in bytes.
func (v *Viper) GetSizeInBytes(key string) uint {
	sizeStr := cast.ToString(v.Get(key))
	return parseSizeInBytes(sizeStr)
}

// UnmarshalKey takes a single key and unmarshals it into a Struct.
func UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
	return v.UnmarshalKey(key, rawVal, opts...)
}

// UnmarshalKey takes a single key and unmarshals it into a Struct.
func (v *Viper) UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
	return decode(v.Get(key), v.defaultDecoderConfig(rawVal, opts...))
}

// Unmarshal unmarshals the config into a Struct. Make sure that the tags
// on the fields of the structure are properly set.
func Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
	return v.Unmarshal(rawVal, opts...)
}

// Unmarshal unmarshals the config into a Struct. Make sure that the tags
// on the fields of the structure are properly set.
func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
	keys := v.AllKeys()

	if v.experimentalBindStruct {
		// TODO: make this optional?
		structKeys, err := v.decodeStructKeys(rawVal, opts...)
		if err != nil {
			return err
		}

		keys = append(keys, structKeys...)
	}

	// TODO: struct keys should be enough?
	return decode(v.getSettings(keys), v.defaultDecoderConfig(rawVal, opts...))
}

func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]string, error) {
	var structKeyMap map[string]any

	err := decode(input, v.defaultDecoderConfig(&structKeyMap, opts...))
	if err != nil {
		return nil, err
	}

	flattenedStructKeyMap := v.flattenAndMergeMap(map[string]bool{}, structKeyMap, "")

	r := make([]string, 0, len(flattenedStructKeyMap))
	for v := range flattenedStructKeyMap {
		r = append(r, v)
	}

	return r, nil
}

// defaultDecoderConfig returns default mapstructure.DecoderConfig with support
// of time.Duration values & string slices.
func (v *Viper) defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
	decodeHook := v.decodeHook
	if decodeHook == nil {
		decodeHook = mapstructure.ComposeDecodeHookFunc(
			mapstructure.StringToTimeDurationHookFunc(),
			// mapstructure.StringToSliceHookFunc(","),
			stringToWeakSliceHookFunc(","),
		)
	}

	c := &mapstructure.DecoderConfig{
		Metadata:         nil,
		WeaklyTypedInput: true,
		DecodeHook:       decodeHook,
	}

	for _, opt := range opts {
		opt(c)
	}

	// Do not allow overwriting the output
	c.Result = output

	return c
}

// As of mapstructure v2.0.0 StringToSliceHookFunc checks if the return type is a string slice.
// This function removes that check.
// TODO: implement a function that checks if the value can be converted to the return type and use it instead.
func stringToWeakSliceHookFunc(sep string) mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{},
	) (interface{}, error) {
		if f.Kind() != reflect.String || t.Kind() != reflect.Slice {
			return data, nil
		}

		raw := data.(string)
		if raw == "" {
			return []string{}, nil
		}

		return strings.Split(raw, sep), nil
	}
}

// decode is a wrapper around mapstructure.Decode that mimics the WeakDecode functionality.
func decode(input any, config *mapstructure.DecoderConfig) error {
	decoder, err := mapstructure.NewDecoder(config)
	if err != nil {
		return err
	}
	return decoder.Decode(input)
}

// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent
// in the destination struct.
func UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
	return v.UnmarshalExact(rawVal, opts...)
}

// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent
// in the destination struct.
func (v *Viper) UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
	config := v.defaultDecoderConfig(rawVal, opts...)
	config.ErrorUnused = true

	keys := v.AllKeys()

	if v.experimentalBindStruct {
		// TODO: make this optional?
		structKeys, err := v.decodeStructKeys(rawVal, opts...)
		if err != nil {
			return err
		}

		keys = append(keys, structKeys...)
	}

	// TODO: struct keys should be enough?
	return decode(v.getSettings(keys), config)
}

// BindPFlags binds a full flag set to the configuration, using each flag's long
// name as the config key.
func BindPFlags(flags *pflag.FlagSet) error { return v.BindPFlags(flags) }

// BindPFlags binds a full flag set to the configuration, using each flag's long
// name as the config key.
func (v *Viper) BindPFlags(flags *pflag.FlagSet) error {
	return v.BindFlagValues(pflagValueSet{flags})
}

// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
//	serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
//	Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) }

// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
//	serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
//	Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error {
	if flag == nil {
		return fmt.Errorf("flag for %q is nil", key)
	}
	return v.BindFlagValue(key, pflagValue{flag})
}

// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long
// name as the config key.
func BindFlagValues(flags FlagValueSet) error { return v.BindFlagValues(flags) }

// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long
// name as the config key.
func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) {
	flags.VisitAll(func(flag FlagValue) {
		if err = v.BindFlagValue(flag.Name(), flag); err != nil {
			return
		}
	})
	return nil
}

// BindFlagValue binds a specific key to a FlagValue.
func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) }

// BindFlagValue binds a specific key to a FlagValue.
func (v *Viper) BindFlagValue(key string, flag FlagValue) error {
	if flag == nil {
		return fmt.Errorf("flag for %q is nil", key)
	}
	v.pflags[strings.ToLower(key)] = flag
	return nil
}

// BindEnv binds a Viper key to a ENV variable.
// ENV variables are case sensitive.
// If only a key is provided, it will use the env key matching the key, uppercased.
// If more arguments are provided, they will represent the env variable names that
// should bind to this key and will be taken in the specified order.
// EnvPrefix will be used when set when env name is not provided.
func BindEnv(input ...string) error { return v.BindEnv(input...) }

// BindEnv binds a Viper key to a ENV variable.
// ENV variables are case sensitive.
// If only a key is provided, it will use the env key matching the key, uppercased.
// If more arguments are provided, they will represent the env variable names that
// should bind to this key and will be taken in the specified order.
// EnvPrefix will be used when set when env name is not provided.
func (v *Viper) BindEnv(input ...string) error {
	if len(input) == 0 {
		return fmt.Errorf("missing key to bind to")
	}

	key := strings.ToLower(input[0])

	if len(input) == 1 {
		v.env[key] = append(v.env[key], v.mergeWithEnvPrefix(key))
	} else {
		v.env[key] = append(v.env[key], input[1:]...)
	}

	return nil
}

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
func MustBindEnv(input ...string) { v.MustBindEnv(input...) }

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
func (v *Viper) MustBindEnv(input ...string) {
	if err := v.BindEnv(input...); err != nil {
		panic(fmt.Sprintf("error while binding environment variable: %v", err))
	}
}

// Given a key, find the value.
//
// Viper will check to see if an alias exists first.
// Viper will then check in the following order:
// flag, env, config file, key/value store.
// Lastly, if no value was found and flagDefault is true, and if the key
// corresponds to a flag, the flag's default value is returned.
//
// Note: this assumes a lower-cased key given.
func (v *Viper) find(lcaseKey string, flagDefault bool) any {
	var (
		val    any
		exists bool
		path   = strings.Split(lcaseKey, v.keyDelim)
		nested = len(path) > 1
	)

	// compute the path through the nested maps to the nested value
	if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
		return nil
	}

	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path = strings.Split(lcaseKey, v.keyDelim)
	nested = len(path) > 1

	// Set() override first
	val = v.searchMap(v.override, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
		return nil
	}

	// PFlag override next
	flag, exists := v.pflags[lcaseKey]
	if exists && flag.HasChanged() {
		switch flag.ValueType() {
		case "int", "int8", "int16", "int32", "int64":
			return cast.ToInt(flag.ValueString())
		case "bool":
			return cast.ToBool(flag.ValueString())
		case "stringSlice", "stringArray":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return res
		case "boolSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToBoolSlice(res)
		case "intSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToIntSlice(res)
		case "uintSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToUintSlice(res)
		case "float64Slice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToFloat64Slice(res)
		case "durationSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			slice := strings.Split(s, ",")
			return cast.ToDurationSlice(slice)
		case "stringToString":
			return stringToStringConv(flag.ValueString())
		case "stringToInt":
			return stringToIntConv(flag.ValueString())
		default:
			return flag.ValueString()
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
		return nil
	}

	// Env override next
	if v.automaticEnvApplied {
		envKey := strings.Join(append(v.parents, lcaseKey), ".")
		// even if it hasn't been registered, if automaticEnv is used,
		// check any Get request
		if val, ok := v.getEnv(v.mergeWithEnvPrefix(envKey)); ok {
			return val
		}
		if nested && v.isPathShadowedInAutoEnv(path) != "" {
			return nil
		}
	}
	envkeys, exists := v.env[lcaseKey]
	if exists {
		for _, envkey := range envkeys {
			if val, ok := v.getEnv(envkey); ok {
				return val
			}
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
		return nil
	}

	// Config file next
	val = v.searchIndexableWithPathPrefixes(v.config, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
		return nil
	}

	// K/V store next
	val = v.searchMap(v.kvstore, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
		return nil
	}

	// Default next
	val = v.searchMap(v.defaults, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
		return nil
	}

	if flagDefault {
		// last chance: if no value is found and a flag does exist for the key,
		// get the flag's default value even if the flag's value has not been set.
		if flag, exists := v.pflags[lcaseKey]; exists {
			switch flag.ValueType() {
			case "int", "int8", "int16", "int32", "int64":
				return cast.ToInt(flag.ValueString())
			case "bool":
				return cast.ToBool(flag.ValueString())
			case "stringSlice", "stringArray":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return res
			case "boolSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToBoolSlice(res)
			case "intSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToIntSlice(res)
			case "uintSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToUintSlice(res)
			case "float64Slice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToFloat64Slice(res)
			case "stringToString":
				return stringToStringConv(flag.ValueString())
			case "stringToInt":
				return stringToIntConv(flag.ValueString())
			case "durationSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				slice := strings.Split(s, ",")
				return cast.ToDurationSlice(slice)
			default:
				return flag.ValueString()
			}
		}
		// last item, no need to check shadowing
	}

	return nil
}

func readAsCSV(val string) ([]string, error) {
	if val == "" {
		return []string{}, nil
	}
	stringReader := strings.NewReader(val)
	csvReader := csv.NewReader(stringReader)
	return csvReader.Read()
}

// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/master/string_to_string.go#L79
// alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap.
func stringToStringConv(val string) any {
	val = strings.Trim(val, "[]")
	// An empty string would cause an empty map
	if val == "" {
		return map[string]any{}
	}
	r := csv.NewReader(strings.NewReader(val))
	ss, err := r.Read()
	if err != nil {
		return nil
	}
	out := make(map[string]any, len(ss))
	for _, pair := range ss {
		k, vv, found := strings.Cut(pair, "=")
		if !found {
			return nil
		}
		out[k] = vv
	}
	return out
}

// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/d5e0c0615acee7028e1e2740a11102313be88de1/string_to_int.go#L68
// alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap.
func stringToIntConv(val string) any {
	val = strings.Trim(val, "[]")
	// An empty string would cause an empty map
	if val == "" {
		return map[string]any{}
	}
	ss := strings.Split(val, ",")
	out := make(map[string]any, len(ss))
	for _, pair := range ss {
		k, vv, found := strings.Cut(pair, "=")
		if !found {
			return nil
		}
		var err error
		out[k], err = strconv.Atoi(vv)
		if err != nil {
			return nil
		}
	}
	return out
}

// IsSet checks to see if the key has been set in any of the data locations.
// IsSet is case-insensitive for a key.
func IsSet(key string) bool { return v.IsSet(key) }

// IsSet checks to see if the key has been set in any of the data locations.
// IsSet is case-insensitive for a key.
func (v *Viper) IsSet(key string) bool {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, false)
	return val != nil
}

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func AutomaticEnv() { v.AutomaticEnv() }

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func (v *Viper) AutomaticEnv() {
	v.automaticEnvApplied = true
}

// SetEnvKeyReplacer sets the strings.Replacer on the viper object
// Useful for mapping an environmental variable to a key that does
// not match it.
func SetEnvKeyReplacer(r *strings.Replacer) { v.SetEnvKeyReplacer(r) }

// SetEnvKeyReplacer sets the strings.Replacer on the viper object
// Useful for mapping an environmental variable to a key that does
// not match it.
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) {
	v.envKeyReplacer = r
}

// RegisterAlias creates an alias that provides another accessor for the same key.
// This enables one to change a name without breaking the application.
func RegisterAlias(alias, key string) { v.RegisterAlias(alias, key) }

// RegisterAlias creates an alias that provides another accessor for the same key.
// This enables one to change a name without breaking the application.
func (v *Viper) RegisterAlias(alias, key string) {
	v.registerAlias(alias, strings.ToLower(key))
}

func (v *Viper) registerAlias(alias, key string) {
	alias = strings.ToLower(alias)
	if alias != key && alias != v.realKey(key) {
		_, exists := v.aliases[alias]

		if !exists {
			// if we alias something that exists in one of the maps to another
			// name, we'll never be able to get that value using the original
			// name, so move the config value to the new realkey.
			if val, ok := v.config[alias]; ok {
				delete(v.config, alias)
				v.config[key] = val
			}
			if val, ok := v.kvstore[alias]; ok {
				delete(v.kvstore, alias)
				v.kvstore[key] = val
			}
			if val, ok := v.defaults[alias]; ok {
				delete(v.defaults, alias)
				v.defaults[key] = val
			}
			if val, ok := v.override[alias]; ok {
				delete(v.override, alias)
				v.override[key] = val
			}
			v.aliases[alias] = key
		}
	} else {
		v.logger.Warn("creating circular reference alias", "alias", alias, "key", key, "real_key", v.realKey(key))
	}
}

func (v *Viper) realKey(key string) string {
	newkey, exists := v.aliases[key]
	if exists {
		v.logger.Debug("key is an alias", "alias", key, "to", newkey)

		return v.realKey(newkey)
	}
	return key
}

// InConfig checks to see if the given key (or an alias) is in the config file.
func InConfig(key string) bool { return v.InConfig(key) }

// InConfig checks to see if the given key (or an alias) is in the config file.
func (v *Viper) InConfig(key string) bool {
	lcaseKey := strings.ToLower(key)

	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path := strings.Split(lcaseKey, v.keyDelim)

	return v.searchIndexableWithPathPrefixes(v.config, path) != nil
}

// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func SetDefault(key string, value any) { v.SetDefault(key, value) }

// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func (v *Viper) SetDefault(key string, value any) {
	// If alias passed in, then set the proper default
	key = v.realKey(strings.ToLower(key))
	value = toCaseInsensitiveValue(value)

	path := strings.Split(key, v.keyDelim)
	lastKey := strings.ToLower(path[len(path)-1])
	deepestMap := deepSearch(v.defaults, path[0:len(path)-1])

	// set innermost value
	deepestMap[lastKey] = value
}

// Set sets the value for the key in the override register.
// Set is case-insensitive for a key.
// Will be used instead of values obtained via
// flags, config file, ENV, default, or key/value store.
func Set(key string, value any) { v.Set(key, value) }

// Set sets the value for the key in the override register.
// Set is case-insensitive for a key.
// Will be used instead of values obtained via
// flags, config file, ENV, default, or key/value store.
func (v *Viper) Set(key string, value any) {
	// If alias passed in, then set the proper override
	key = v.realKey(strings.ToLower(key))
	value = toCaseInsensitiveValue(value)

	path := strings.Split(key, v.keyDelim)
	lastKey := strings.ToLower(path[len(path)-1])
	deepestMap := deepSearch(v.override, path[0:len(path)-1])

	// set innermost value
	deepestMap[lastKey] = value
}

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func ReadInConfig() error { return v.ReadInConfig() }

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func (v *Viper) ReadInConfig() error {
	v.logger.Info("attempting to read in config file")
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}

	if !slices.Contains(SupportedExts, v.getConfigType()) {
		return UnsupportedConfigError(v.getConfigType())
	}

	v.logger.Debug("reading file", "file", filename)

	file, err := afero.ReadFile(v.fs, filename)
	if errors.Is(err, fs.ErrNotExist) {
		// The specified config file is missing
		return FileNotFoundError{err: err, path: filename}
	} else if err != nil {
		// We hit some other error from the filesystem that isn't a missing file
		return err
	}

	config := make(map[string]any)

	err = v.unmarshalReader(bytes.NewReader(file), config)
	if err != nil {
		return err
	}

	v.config = config
	return nil
}

// MergeInConfig merges a new configuration with an existing config.
func MergeInConfig() error { return v.MergeInConfig() }

// MergeInConfig merges a new configuration with an existing config.
func (v *Viper) MergeInConfig() error {
	v.logger.Info("attempting to merge in config file")
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}

	if !slices.Contains(SupportedExts, v.getConfigType()) {
		return UnsupportedConfigError(v.getConfigType())
	}

	file, err := afero.ReadFile(v.fs, filename)
	if err != nil {
		return err
	}

	return v.MergeConfig(bytes.NewReader(file))
}

// ReadConfig will read a configuration file, setting existing keys to nil if the
// key does not exist in the file.
func ReadConfig(in io.Reader) error { return v.ReadConfig(in) }

// ReadConfig reads a configuration file, setting existing keys to nil if the
// key does not exist in the file.
func (v *Viper) ReadConfig(in io.Reader) error {
	config := make(map[string]any)

	err := v.unmarshalReader(in, config)
	if err != nil {
		return err
	}

	v.config = config

	return nil
}

// MergeConfig merges a new configuration with an existing config.
func MergeConfig(in io.Reader) error { return v.MergeConfig(in) }

// MergeConfig merges a new configuration with an existing config.
func (v *Viper) MergeConfig(in io.Reader) error {
	config := make(map[string]any)

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

	return v.MergeConfigMap(config)
}

// MergeConfigMap merges the configuration from the map given with an existing config.
// Note that the map given may be modified.
func MergeConfigMap(cfg map[string]any) error { return v.MergeConfigMap(cfg) }

// MergeConfigMap merges the configuration from the map given with an existing config.
// Note that the map given may be modified.
func (v *Viper) MergeConfigMap(cfg map[string]any) error {
	if v.config == nil {
		v.config = make(map[string]any)
	}
	insensitiviseMap(cfg)
	mergeMaps(cfg, v.config, nil)
	return nil
}

// WriteConfig writes the current configuration to a file.
func WriteConfig() error { return v.WriteConfig() }

// WriteConfig writes the current configuration to a file.
func (v *Viper) WriteConfig() error {
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}
	return v.writeConfig(filename, true)
}

// SafeWriteConfig writes current configuration to file only if the file does not exist.
func SafeWriteConfig() error { return v.SafeWriteConfig() }

// SafeWriteConfig writes current configuration to file only if the file does not exist.
func (v *Viper) SafeWriteConfig() error {
	if len(v.configPaths) < 1 {
		return errors.New("missing configuration for 'configPath'")
	}
	return v.SafeWriteConfigAs(filepath.Join(v.configPaths[0], v.configName+"."+v.configType))
}

// WriteConfigAs writes current configuration to a given filename.
func WriteConfigAs(filename string) error { return v.WriteConfigAs(filename) }

// WriteConfigAs writes current configuration to a given filename.
func (v *Viper) WriteConfigAs(filename string) error {
	return v.writeConfig(filename, true)
}

// WriteConfigTo writes current configuration to an [io.Writer].
func WriteConfigTo(w io.Writer) error { return v.WriteConfigTo(w) }

// WriteConfigTo writes current configuration to an [io.Writer].
func (v *Viper) WriteConfigTo(w io.Writer) error {
	format := strings.ToLower(v.getConfigType())

	if !slices.Contains(SupportedExts, format) {
		return UnsupportedConfigError(format)
	}

	return v.marshalWriter(w, format)
}

// SafeWriteConfigAs writes current configuration to a given filename if it does not exist.
func SafeWriteConfigAs(filename string) error { return v.SafeWriteConfigAs(filename) }

// SafeWriteConfigAs writes current configuration to a given filename if it does not exist.
func (v *Viper) SafeWriteConfigAs(filename string) error {
	alreadyExists, err := afero.Exists(v.fs, filename)
	if alreadyExists && err == nil {
		return ConfigFileAlreadyExistsError(filename)
	}
	return v.writeConfig(filename, false)
}

func (v *Viper) writeConfig(filename string, force bool) error {
	v.logger.Info("attempting to write configuration to file")

	var configType string

	ext := filepath.Ext(filename)
	if ext != "" && ext != filepath.Base(filename) {
		configType = ext[1:]
	} else {
		configType = v.configType
	}
	if configType == "" {
		return fmt.Errorf("config type could not be determined for %s", filename)
	}

	if !slices.Contains(SupportedExts, configType) {
		return UnsupportedConfigError(configType)
	}
	if v.config == nil {
		v.config = make(map[string]any)
	}
	flags := os.O_CREATE | os.O_TRUNC | os.O_WRONLY
	if !force {
		flags |= os.O_EXCL
	}
	f, err := v.fs.OpenFile(filename, flags, v.configPermissions)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := v.marshalWriter(f, configType); err != nil {
		return err
	}

	return f.Sync()
}

func (v *Viper) unmarshalReader(in io.Reader, c map[string]any) error {
	format := strings.ToLower(v.getConfigType())
	if format == "" {
		return errors.New("cannot decode configuration: unable to determine config type")
	}

	buf := new(bytes.Buffer)
	_, err := buf.ReadFrom(in)
	if err != nil {
		return fmt.Errorf("failed to read configuration from input: %w", err)
	}

	// TODO: remove this once SupportedExts is deprecated/removed
	if !slices.Contains(SupportedExts, format) {
		return UnsupportedConfigError(format)
	}

	// TODO: return [UnsupportedConfigError] if the registry does not contain the format
	// TODO: consider deprecating this error type
	decoder, err := v.decoderRegistry.Decoder(format)
	if err != nil {
		return ConfigParseError{err}
	}

	err = decoder.Decode(buf.Bytes(), c)
	if err != nil {
		return ConfigParseError{err}
	}

	insensitiviseMap(c)
	return nil
}

// Marshal a map into Writer.
func (v *Viper) marshalWriter(w io.Writer, configType string) error {
	c := v.AllSettings()

	encoder, err := v.encoderRegistry.Encoder(configType)
	if err != nil {
		return ConfigMarshalError{err}
	}

	b, err := encoder.Encode(c)
	if err != nil {
		return ConfigMarshalError{err}
	}

	_, err = w.Write(b)
	if err != nil {
		return ConfigMarshalError{err}
	}

	return nil
}

func keyExists(k string, m map[string]any) string {
	lk := strings.ToLower(k)
	for mk := range m {
		lmk := strings.ToLower(mk)
		if lmk == lk {
			return mk
		}
	}
	return ""
}

func castToMapStringInterface(
	src map[any]any,
) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[fmt.Sprintf("%v", k)] = v
	}
	return tgt
}

func castMapStringSliceToMapInterface(src map[string][]string) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

func castMapStringToMapInterface(src map[string]string) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

func castMapFlagToMapInterface(src map[string]FlagValue) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

// mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's
// insistence on parsing nested structures as `map[any]any`
// instead of using a `string` as the key for nest structures beyond one level
// deep. Both map types are supported as there is a go-yaml fork that uses
// `map[string]any` instead.
func mergeMaps(src, tgt map[string]any, itgt map[any]any) {
	for sk, sv := range src {
		tk := keyExists(sk, tgt)
		if tk == "" {
			v.logger.Debug("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
			tgt[sk] = sv
			if itgt != nil {
				itgt[sk] = sv
			}
			continue
		}

		tv, ok := tgt[tk]
		if !ok {
			v.logger.Debug("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
			tgt[sk] = sv
			if itgt != nil {
				itgt[sk] = sv
			}
			continue
		}

		svType := reflect.TypeOf(sv)
		tvType := reflect.TypeOf(tv)

		v.logger.Debug(
			"processing",
			"key", sk,
			"st", svType,
			"tt", tvType,
			"sv", sv,
			"tv", tv,
		)

		switch ttv := tv.(type) {
		case map[any]any:
			v.logger.Debug("merging maps (must convert)")
			tsv, ok := sv.(map[any]any)
			if !ok {
				v.logger.Error(
					"Could not cast sv to map[any]any",
					"key", sk,
					"st", svType,
					"tt", tvType,
					"sv", sv,
					"tv", tv,
				)
				continue
			}

			ssv := castToMapStringInterface(tsv)
			stv := castToMapStringInterface(ttv)
			mergeMaps(ssv, stv, ttv)
		case map[string]any:
			v.logger.Debug("merging maps")
			tsv, ok := sv.(map[string]any)
			if !ok {
				v.logger.Error(
					"Could not cast sv to map[string]any",
					"key", sk,
					"st", svType,
					"tt", tvType,
					"sv", sv,
					"tv", tv,
				)
				continue
			}
			mergeMaps(tsv, ttv, nil)
		default:
			v.logger.Debug("setting value")
			tgt[tk] = sv
			if itgt != nil {
				itgt[tk] = sv
			}
		}
	}
}

// AllKeys returns all keys holding a value, regardless of where they are set.
// Nested keys are returned with a v.keyDelim separator.
func AllKeys() []string { return v.AllKeys() }

// AllKeys returns all keys holding a value, regardless of where they are set.
// Nested keys are returned with a v.keyDelim separator.
func (v *Viper) AllKeys() []string {
	m := map[string]bool{}
	// add all paths, by order of descending priority to ensure correct shadowing
	m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "")
	m = v.flattenAndMergeMap(m, v.override, "")
	m = v.mergeFlatMap(m, castMapFlagToMapInterface(v.pflags))
	m = v.mergeFlatMap(m, castMapStringSliceToMapInterface(v.env))
	m = v.flattenAndMergeMap(m, v.config, "")
	m = v.flattenAndMergeMap(m, v.kvstore, "")
	m = v.flattenAndMergeMap(m, v.defaults, "")

	// convert set of paths to list
	a := make([]string, 0, len(m))
	for x := range m {
		a = append(a, x)
	}
	return a
}

// flattenAndMergeMap recursively flattens the given map into a map[string]bool
// of key paths (used as a set, easier to manipulate than a []string):
//   - each path is merged into a single key string, delimited with v.keyDelim
//   - if a path is shadowed by an earlier value in the initial shadow map,
//     it is skipped.
//
// The resulting set of paths is merged to the given shadow set at the same time.
func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]any, prefix string) map[string]bool {
	if shadow != nil && prefix != "" && shadow[prefix] {
		// prefix is shadowed => nothing more to flatten
		return shadow
	}
	if shadow == nil {
		shadow = make(map[string]bool)
	}

	var m2 map[string]any
	if prefix != "" {
		prefix += v.keyDelim
	}
	for k, val := range m {
		fullKey := prefix + k
		switch val := val.(type) {
		case map[string]any:
			m2 = val
		case map[any]any:
			m2 = cast.ToStringMap(val)
		default:
			// immediate value
			shadow[strings.ToLower(fullKey)] = true
			continue
		}
		// recursively merge to shadow map
		shadow = v.flattenAndMergeMap(shadow, m2, fullKey)
	}
	return shadow
}

// mergeFlatMap merges the given maps, excluding values of the second map
// shadowed by values from the first map.
func (v *Viper) mergeFlatMap(shadow map[string]bool, m map[string]any) map[string]bool {
	// scan keys
outer:
	for k := range m {
		path := strings.Split(k, v.keyDelim)
		// scan intermediate paths
		var parentKey string
		for i := 1; i < len(path); i++ {
			parentKey = strings.Join(path[0:i], v.keyDelim)
			if shadow[parentKey] {
				// path is shadowed, continue
				continue outer
			}
		}
		// add key
		shadow[strings.ToLower(k)] = true
	}
	return shadow
}

// AllSettings merges all settings and returns them as a map[string]any.
func AllSettings() map[string]any { return v.AllSettings() }

// AllSettings merges all settings and returns them as a map[string]any.
func (v *Viper) AllSettings() map[string]any {
	return v.getSettings(v.AllKeys())
}

func (v *Viper) getSettings(keys []string) map[string]any {
	m := map[string]any{}
	// start from the list of keys, and construct the map one value at a time
	for _, k := range keys {
		value := v.Get(k)
		if value == nil {
			// should not happen, since AllKeys() returns only keys holding a value,
			// check just in case anything changes
			continue
		}
		path := strings.Split(k, v.keyDelim)
		lastKey := strings.ToLower(path[len(path)-1])
		deepestMap := deepSearch(m, path[0:len(path)-1])
		// set innermost value
		deepestMap[lastKey] = value
	}
	return m
}

// SetFs sets the filesystem to use to read configuration.
func SetFs(fs afero.Fs) { v.SetFs(fs) }

// SetFs sets the filesystem to use to read configuration.
func (v *Viper) SetFs(fs afero.Fs) {
	v.fs = fs
}

// SetConfigName sets name for the config file.
// Does not include extension.
func SetConfigName(in string) { v.SetConfigName(in) }

// SetConfigName sets the name for the config file.
// Does not include extension.
func (v *Viper) SetConfigName(in string) {
	if v.finder != nil {
		v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "SetConfigName"))
	}

	if in != "" {
		v.configName = in
		v.configFile = ""
	}
}

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func SetConfigType(in string) { v.SetConfigType(in) }

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func (v *Viper) SetConfigType(in string) {
	if in != "" {
		v.configType = in
	}
}

// SetConfigPermissions sets the permissions for the config file.
func SetConfigPermissions(perm os.FileMode) { v.SetConfigPermissions(perm) }

// SetConfigPermissions sets the permissions for the config file.
func (v *Viper) SetConfigPermissions(perm os.FileMode) {
	v.configPermissions = perm.Perm()
}

func (v *Viper) getConfigType() string {
	if v.configType != "" {
		return v.configType
	}

	cf, err := v.getConfigFile()
	if err != nil {
		return ""
	}

	ext := filepath.Ext(cf)

	if len(ext) > 1 {
		return ext[1:]
	}

	return ""
}

func (v *Viper) getConfigFile() (string, error) {
	if v.configFile == "" {
		cf, err := v.findConfigFile()
		if err != nil {
			return "", err
		}
		v.configFile = cf
	}
	return v.configFile, nil
}

// Debug prints all configuration registries for debugging
// purposes.
func Debug() { v.Debug() }

// DebugTo writes all configuration registries to the given [io.Writer].
func DebugTo(w io.Writer) { v.DebugTo(w) }

// Debug prints all configuration registries for debugging purposes.
func (v *Viper) Debug() { v.DebugTo(os.Stdout) }

// DebugTo writes all configurati
[truncated at 64000 of 64403 bytes]
[evidence record sha256:ef2ddcfd2022b53b52ad9297e5fa1e914201d342edba2ea60bbed726654d0778 kind tool-call:read]
step 13: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper_test.go","maxBytes":64000}
tool read ok: // 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(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.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 TestWrongFileNotFound(t *testing.T) {
	_, config := initDirs(t)

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

	v.SetConfigFile(`whatareyoutalkingabout.yaml`)

	err := v.ReadInConfig()

	var fileLookupError FileLookupError

	// It matches all error types and the shared error interface.
	assert.ErrorAs(t, err, &FileNotFoundError{})
	assert.ErrorAs(t, err, &fileLookupError)

	// 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 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()

	var fileLookupError FileLookupError

	// It matches all error types and the shared error interface.
	assert.ErrorAs(t, err, &ConfigFileNotFoundError{})
	assert.ErrorAs(t, err, &FileNotFoundFromSearchError{})
	assert.ErrorAs(t, err, &fileLookupError)

	// 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()

	var fileLookupError FileLookupError

	// It matches both types of errors.
	assert.ErrorAs(t, err, &ConfigFileNotFoundError{})
	assert.ErrorAs(t, err, &FileNotFoundFromSearchError{})
	assert.ErrorAs(t, err, &fileLookupError)

	// 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, "S
[truncated at 64000 of 66541 bytes]
[evidence record sha256:ac56d87d4efa386081221a0d95b45c2edf2bd093fab8538f3ab3ab28d1eb0f3a kind tool-call:read]
step 14: calling local:qwen3.6:35b-a3b
tool read <- {"path":"util_test.go"}
tool read ok: // Copyright © 2016 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.

// Viper is a application configuration system.
// It believes that applications can be configured a variety of ways
// via flags, ENVIRONMENT variables, configuration files retrieved
// from the file system, or a remote key/value store.

package viper

import (
	"log/slog"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestCopyAndInsensitiviseMap(t *testing.T) {
	var (
		given = map[string]any{
			"Foo": 32,
			"Bar": map[any]any{
				"ABc": "A",
				"cDE": "B",
			},
		}
		expected = map[string]any{
			"foo": 32,
			"bar": map[string]any{
				"abc": "A",
				"cde": "B",
			},
		}
	)

	got := copyAndInsensitiviseMap(given)

	assert.Equal(t, expected, got)
	_, ok := given["foo"]
	assert.False(t, ok)
	_, ok = given["bar"]
	assert.False(t, ok)

	m := given["Bar"].(map[any]any)
	_, ok = m["ABc"]
	assert.True(t, ok)
}

func TestAbsPathify(t *testing.T) {
	skipWindows(t)

	home := userHomeDir()
	homer := filepath.Join(home, "homer")
	wd, _ := os.Getwd()

	t.Setenv("HOMER_ABSOLUTE_PATH", homer)
	t.Setenv("VAR_WITH_RELATIVE_PATH", "relative")

	tests := []struct {
		input  string
		output string
	}{
		{"", wd},
		{"sub", filepath.Join(wd, "sub")},
		{"./", wd},
		{"./sub", filepath.Join(wd, "sub")},
		{"$HOME", home},
		{"$HOME/", home},
		{"$HOME/sub", filepath.Join(home, "sub")},
		{"$HOMER_ABSOLUTE_PATH", homer},
		{"$HOMER_ABSOLUTE_PATH/", homer},
		{"$HOMER_ABSOLUTE_PATH/sub", filepath.Join(homer, "sub")},
		{"$VAR_WITH_RELATIVE_PATH", filepath.Join(wd, "relative")},
		{"$VAR_WITH_RELATIVE_PATH/", filepath.Join(wd, "relative")},
		{"$VAR_WITH_RELATIVE_PATH/sub", filepath.Join(wd, "relative", "sub")},
	}

	for _, test := range tests {
		got := absPathify(slog.Default(), test.input)
		assert.Equal(t, test.output, got)
	}
}

[evidence record sha256:7b734ff2673127a4d39a5bdd1744a7b2ebf4ceb884d90a7ff9b556d443297795 kind tool-call:read]
step 15: calling local:qwen3.6:35b-a3b
tool read <- {"path":"flags.go"}
tool read ok: package viper

import "github.com/spf13/pflag"

// FlagValueSet is an interface that users can implement
// to bind a set of flags to viper.
type FlagValueSet interface {
	VisitAll(fn func(FlagValue))
}

// FlagValue is an interface that users can implement
// to bind different flags to viper.
type FlagValue interface {
	HasChanged() bool
	Name() string
	ValueString() string
	ValueType() string
}

// pflagValueSet is a wrapper around *pflag.ValueSet
// that implements FlagValueSet.
type pflagValueSet struct {
	flags *pflag.FlagSet
}

// VisitAll iterates over all *pflag.Flag inside the *pflag.FlagSet.
func (p pflagValueSet) VisitAll(fn func(flag FlagValue)) {
	p.flags.VisitAll(func(flag *pflag.Flag) {
		fn(pflagValue{flag})
	})
}

// pflagValue is a wrapper around *pflag.flag
// that implements FlagValue.
type pflagValue struct {
	flag *pflag.Flag
}

// HasChanged returns whether the flag has changes or not.
func (p pflagValue) HasChanged() bool {
	return p.flag.Changed
}

// Name returns the name of the flag.
func (p pflagValue) Name() string {
	return p.flag.Name
}

// ValueString returns the value of the flag as a string.
func (p pflagValue) ValueString() string {
	return p.flag.Value.String()
}

// ValueType returns the type of the flag as a string.
func (p pflagValue) ValueType() string {
	return p.flag.Value.Type()
}

[evidence record sha256:b06282c69e7b15d8f397881cdd0ce960aa146617291e3a5191a30fcbd16d8e1f kind tool-call:read]
step 16: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"func Test.*\\(t \\*testing\\.T\\)","path":".","maxResults":100}
tool search ok: .campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/common_test.go:159: func TestSortValues(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/common_test.go:233: func TestSortValuesWithMethods(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/common_test.go:268: func TestSortValuesWithSpew(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/dump_test.go:960: func TestDump(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/dump_test.go:994: func TestDumpSortedKeys(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/format_test.go:1465: func TestFormatter(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/format_test.go:1516: func TestPrintSortedKeys(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/internal_test.go:54: func TestInvalidReflectValue(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/internalunsafe_test.go:52: func TestAddedReflectValue(t *testing.T) {
.campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/spew_test.go:210: func TestSpew(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_fen_test.go:11: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_inotify_test.go:15: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_inotify_test.go:55: func TestInotifyOverflow(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_inotify_test.go:117: func TestInotifyDeleteOpenFile(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_kqueue_test.go:11: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_windows_test.go:11: func TestRemoveState(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/backend_windows_test.go:58: func TestWindowsRemWatch(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:32: func TestScript(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:54: func TestWatchMultipleWrite(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:87: func TestWatchRemoveOpenFd(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:120: func TestWatchRemoveWatchedDir(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:183: func TestClose(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:396: func TestAdd(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:598: func TestRemove(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:828: func TestEventString(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:856: func TestWatchList(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:877: func TestOpHas(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:1039: func TestRace(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/fsnotify_test.go:1137: func TestNewWatcher(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:31: func TestGetOptCodes(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:52: func TestGroupedOpCodes(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:96: func TestWithAsciiOneInsert(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:108: func TestWithAsciiOnDelete(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:115: func TestSFBugsComparingEmptyLists(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:122: func TestOutputFormatRangeFormatUnified(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:139: func TestDiffMatch(t *testing.T) {
.campaign/gomod/github.com/fsnotify/fsnotify@v1.9.0/internal/ztest/diff_test.go:237: func TestDiffJSON(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:100: func TestComposeDecodeHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:129: func TestComposeDecodeHookFunc_err(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:147: func TestComposeDecodeHookFunc_kinds(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:179: func TestOrComposeDecodeHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:208: func TestOrComposeDecodeHookFunc_correctValueIsLast(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:245: func TestOrComposeDecodeHookFunc_err(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:274: func TestComposeDecodeHookFunc_safe_nofuncs(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:306: func TestComposeDecodeHookFunc_ReflectValueHook(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:336: func TestStringToSliceHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:451: func TestStringToWeakSliceHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:508: func TestStringToTimeDurationHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:607: func TestStringToTimeLocationHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:636: func TestStringToURLHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:672: func TestStringToTimeHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:703: func TestStringToIPHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:742: func TestStringToIPNetHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:777: func TestWeaklyTypedHook(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:843: func TestStructToMapHookFuncTabled(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:956: func TestTextUnmarshallerHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:984: func TestStringToNetIPAddrHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1022: func TestStringToNetIPAddrPortHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1061: func TestStringToNetIPPrefixHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1102: func TestStringToBasicTypeHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1144: func TestStringToInt8HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1206: func TestStringToUint8HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1259: func TestStringToInt16HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1282: func TestStringToUint16HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1305: func TestStringToInt32HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1328: func TestStringToUint32HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1351: func TestStringToInt64HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1374: func TestStringToUint64HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1397: func TestStringToIntHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1420: func TestStringToUintHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1443: func TestStringToFloat32HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1537: func TestStringToFloat64HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1626: func TestStringToComplex64HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1797: func TestStringToBoolHookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:1879: func TestStringToComplex128HookFunc(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/decode_hooks_test.go:2055: func TestErrorLeakageDecodeHook(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:10: func TestDecode_NilValue(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:145: func TestNestedTypePointerWithDefaults(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:200: func TestNestedTypeSliceWithDefaults(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:240: func TestNestedTypeWithDefaults(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:289: func TestDecodeSliceToEmptySliceWOZeroing(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:356: func TestNextSquashMapstructure(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:384: func TestDecode_DecodeHookInterface(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:463: func TestDecodeBadDataTypeInSlice(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:480: func TestDecodeIntermediateMapsSettable(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:541: func TestDecode_weakEmptyStringToInt(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:569: func TestMapSquash(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:603: func TestMapOmitEmptyWithEmptyFieldnameInTag(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:631: func TestDecode_nonComparableType(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_bugs_test.go:643: func TestDecodeToMapWithMultipleIndirection(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:308: func TestBasicTypes(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:412: func TestBasic_IntWithFloat(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:426: func TestBasic_Merge(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:450: func TestBasic_Struct(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:475: func TestBasic_interfaceStruct(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:497: func TestBasic_interfaceStructNonPtr(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:518: func TestDecode_BasicSquash(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:536: func TestDecodeFrom_BasicSquash(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:566: func TestDecode_BasicJSONInline(t *testing.T) {
.campaign/gomod/github.com/go-viper/mapstructure/v2@v2.4.0/mapstructure_test.go:588: func TestDecodeFrom_BasicJSONInline(t *testing.T) {
[evidence record sha256:aaa0c63480e0f0ddc48f99fab21a0663f5f245adadbbd5295481407547a6af1a kind tool-call:search]
step 17: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper_yaml_test.go"}
tool read ok: package viper

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 yamlWriteExpected = []byte(`age: 35
beard: true
clothing:
    jacket: leather
    pants:
        size: large
    trousers: denim
eyes: brown
hacker: true
hobbies:
    - skateboarding
    - snowboarding
    - go
name: steve
`)

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
`)

[evidence record sha256:ac3d26b695502a2707de03900833c9c8cb96e2576c7fa35e16263ece2f320621 kind tool-call:read]
step 18: calling local:qwen3.6:35b-a3b
tool read <- {"path":"encoder.go"}
tool read failed: failed: ENOENT: no such file or directory, open '/work/encoder.go', caused by ENOENT
[evidence record sha256:4f3e0aa939a1a1368b757de75663dfaf0cbf957dc3c3350d0be7d9bb7c250788 kind tool-call:read]
step 19: calling local:qwen3.6:35b-a3b
tool list <- {"path":"internal"}
tool list ok: encoding/
features/
testutil/
[evidence record sha256:93c844086b518d74479a4e6f16e30740356836d749878566bec5e573a0355d60 kind tool-call:list]
step 20: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"git log --oneline -20 2>&1 || echo \"git not available\""}
tool shell failed: denied: "git log --oneline -20 2>&1 || echo "git not available"" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:2c333f61fd8ab17c3e7fec9e880693664fc5e6323591be6791dbb65e084ec6d4 kind tool-call:shell]
step 21: calling local:qwen3.6:35b-a3b
tool list <- {"path":"internal/encoding"}
tool list ok: dotenv/
json/
toml/
yaml/
[evidence record sha256:60ba0a2245d4846126008720a25a3928adccfea405204b16641a853a635b2278 kind tool-call:list]
step 22: calling local:qwen3.6:35b-a3b
tool write <- {"path":"workplan.txt","content":"The test suite of this repository fails. I need to find and fix the defect.\nSince shell commands are not available, I'll read through source files carefully.\n\nPlan:\n1. Read all source and test files to understand expected behavior\n2. Find defects that would cause tests to fail\n3. Fix them\n\nI've already read viper.go (truncated), viper_test.go (truncated), viper_yaml_test.go, flags.go, util_test.go.\n\nKey areas to check:\n1. encoding/ - what decoders/registrar exist\n2. The `init` global variable usage\n3. Map handling in tests that share global state\n4. Any missing imports or function definitions\n"}
tool write ok: wrote 600 bytes to workplan.txt
[evidence record sha256:2c8afa51cc6f7ea4d248acc9ace03533c40f4df7d45e31063053e4ca4b4fa7ea kind tool-call:write]
step 23: calling local:qwen3.6:35b-a3b
stopped: empty-response after 23 steps, 598865 tokens
gate typecheck passed: the command exited 0 [evidence record sha256:d9ffd5df607fba1073ab3d120592bb57d2218698851f3efb96edbb97befd787d]
gate lint passed: the command exited 0 [evidence record sha256:7a9bc8f4abcc25b688c985e5e51336d75cb7edc8da14908328d923718ba34276]
gate format failed: the command listed 29 offending file(s) [evidence record sha256:0c013fc1134b883e84cfed317efc663d497f35c9816e760cbb5fce00a2fbee91]
gate tests failed: the command exited 1 [evidence record sha256:b9f720b3c414fa545c6277eeec887f9cb0476ec7d0f326bab00827c96cba830c]
gate file-set failed: 1 file(s) changed but no file set was declared before editing. Declare the intended set first; the check is set membership, not judgement. [evidence record sha256:821481c8d7677572fa5024c6da92fbb929edb66c56efa1a6897081d4817b0d3a]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:04cbfbf05767e77d5767fb5772e9a7e62cec4ae74d421e013fe257c5eccb892c]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:1205c218e3298f2b2a033cfedf23b7ee3ea0c6b62794e4df970e745e5f7a0cb4]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:6c58dc822ecce8f4da2f5c78e5027adb86eef9574d10c896a93cc27d285aa5df]
gate diff-budget passed (advisory): within budget: 1 file(s) and 15 added line(s) [evidence record sha256:f726ad434c9a0bdc6e7e30cfe192307d433ec87bf28a34f6967ba65efe9c117a]
auto-resolve attempt 1 of 2
step 1: calling local:qwen3.6:35b-a3b
tool list <- {"path":"."}
tool list ok: .campaign/
.editorconfig
.envrc
.git/
.github/
.gitignore
.golangci.yaml
.yamlignore
.yamllint.yaml
LICENSE
Makefile
README.md
TROUBLESHOOTING.md
UPGRADE.md
encoding.go
encoding_test.go
errors.go
experimental.go
file.go
finder.go
finder_example_test.go
finder_test.go
flags.go
flags_test.go
flake.lock
flake.nix
go.mod
go.sum
internal/
logger.go
overrides_test.go
remote.go
remote/
util.go
util_test.go
viper.go
viper_test.go
viper_yaml_test.go
workplan.txt
[evidence record sha256:d63e206696b4befefae59ca8a496be2c5c7d18a3ed3dbbf02eefbbc26ebdd9ec kind tool-call:list]
step 2: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go test ./... 2>&1","timeoutMs":60000}
tool shell failed: denied: "go test ./... 2>&1" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:283c741ad2aa42eb983e87433fa0471dd337d9605223241968cc5c4869a67169 kind tool-call:shell]
step 3: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper_test.go"}
tool read ok: // 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(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.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 TestWrongFileNotFound(t *testing.T) {
	_, config := initDirs(t)

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

	v.SetConfigFile(`whatareyoutalkingabout.yaml`)

	err := v.ReadInConfig()

	var fileLookupError FileLookupError

	// It matches all error types and the shared error interface.
	assert.ErrorAs(t, err, &FileNotFoundError{})
	assert.ErrorAs(t, err, &fileLookupError)

	// 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 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()

	var fileLookupError FileLookupError

	// It matches all error types and the shared error interface.
	assert.ErrorAs(t, err, &ConfigFileNotFoundError{})
	assert.ErrorAs(t, err, &FileNotFoundFromSearchError{})
	assert.ErrorAs(t, err, &fileLookupError)

	// 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()

	var fileLookupError FileLookupError

	// It matches both types of errors.
	assert.ErrorAs(t, err, &ConfigFileNotFoundError{})
	assert.ErrorAs(t, err, &FileNotFoundFromSearchError{})
	assert.ErrorAs(t, err, &fileLookupError)

	// 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, "S
[truncated at 64000 of 66541 bytes]
[evidence record sha256:1a3f4021c717c711b5e75c41f189ea97b29ce333555e54aa764ec51a0fd01cf1 kind tool-call:read]
tool read <- {"path":"finder.go"}
tool read ok: package viper

import (
	"errors"

	"github.com/spf13/afero"
)

// WithFinder sets a custom [Finder].
func WithFinder(f Finder) Option {
	return optionFunc(func(v *Viper) {
		if f != nil {
			return
		}

		v.finder = f
	})
}

// Finder looks for files and directories in an [afero.Fs] filesystem.
type Finder interface {
	Find(fsys afero.Fs) ([]string, error)
}

// Finders combines multiple finders into one.
func Finders(finders ...Finder) Finder {
	return &combinedFinder{finders: finders}
}

// combinedFinder is a Finder that combines multiple finders.
type combinedFinder struct {
	finders []Finder
}

// Find implements the [Finder] interface.
func (c *combinedFinder) Find(fsys afero.Fs) ([]string, error) {
	var results []string
	var errs []error

	for _, finder := range c.finders {
		if finder == nil {
			continue
		}

		r, err := finder.Find(fsys)
		if err != nil {
			errs = append(errs, err)
			continue
		}

		results = append(results, r...)
	}

	return results, errors.Join(errs...)
}

[evidence record sha256:740d4017e90a8c5c4ffe006d2132fc040872694c9ef40a2d45201069d89ef5b0 kind tool-call:read]
tool read <- {"path":"finder_test.go"}
tool read ok: package viper

import (
	"testing"

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

type finderStub struct {
	results []string
}

func (f finderStub) Find(_ afero.Fs) ([]string, error) {
	return f.results, nil
}

func TestFinders(t *testing.T) {
	finder := Finders(
		finderStub{
			results: []string{
				"/home/user/.viper.yaml",
			},
		},
		finderStub{
			results: []string{
				"/etc/viper/config.yaml",
			},
		},
	)

	results, err := finder.Find(afero.NewMemMapFs())
	require.NoError(t, err)

	expected := []string{
		"/home/user/.viper.yaml",
		"/etc/viper/config.yaml",
	}

	assert.Equal(t, expected, results)
}

[evidence record sha256:fba1d890a6d83bcad06bfc5fd854a033e47097eb90b6b1d1e0d32770bc804c3e kind tool-call:read]
step 4: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper.go"}
tool read ok: // 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.

// Viper is an application configuration system.
// It believes that applications can be configured a variety of ways
// via flags, ENVIRONMENT variables, configuration files retrieved
// from the file system, or a remote key/value store.

// Each item takes precedence over the item below it:

// overrides
// flag
// env
// config
// key/value store
// default

package viper

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	fs "io/fs"
	"log/slog"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/go-viper/mapstructure/v2"
	"github.com/spf13/afero"
	"github.com/spf13/cast"
	"github.com/spf13/pflag"

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

var v *Viper

func init() {
	v = New()
}

// A DecoderConfigOption can be passed to viper.Unmarshal to configure
// mapstructure.DecoderConfig options.
type DecoderConfigOption func(*mapstructure.DecoderConfig)

// DecodeHook returns a DecoderConfigOption which overrides the default
// DecoderConfig.DecodeHook value, the default is:
//
//	 mapstructure.ComposeDecodeHookFunc(
//			mapstructure.StringToTimeDurationHookFunc(),
//			mapstructure.StringToSliceHookFunc(","),
//		)
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
	return func(c *mapstructure.DecoderConfig) {
		c.DecodeHook = hook
	}
}

// Viper is a prioritized configuration registry. It
// maintains a set of configuration sources, fetches
// values to populate those, and provides them according
// to the source's priority.
// The priority of the sources is the following:
// 1. overrides
// 2. flags
// 3. env. variables
// 4. config file
// 5. key/value store
// 6. defaults
//
// For example, if values from the following sources were loaded:
//
//	Defaults : {
//		"secret": "",
//		"user": "default",
//		"endpoint": "https://localhost"
//	}
//	Config : {
//		"user": "root"
//		"secret": "defaultsecret"
//	}
//	Env : {
//		"secret": "somesecretkey"
//	}
//
// The resulting config will have the following values:
//
//	{
//		"secret": "somesecretkey",
//		"user": "root",
//		"endpoint": "https://localhost"
//	}
//
// Note: Vipers are not safe for concurrent Get() and Set() operations.
type Viper struct {
	// Delimiter that separates a list of keys
	// used to access a nested value in one go
	keyDelim string

	// A set of paths to look for the config file in
	configPaths []string

	// The filesystem to read config from.
	fs afero.Fs

	finder Finder

	// A set of remote providers to search for the configuration
	remoteProviders []*defaultRemoteProvider

	// Name of file to look for inside the path
	configName        string
	configFile        string
	configType        string
	configPermissions os.FileMode
	envPrefix         string

	automaticEnvApplied bool
	envKeyReplacer      StringReplacer
	allowEmptyEnv       bool

	parents        []string
	config         map[string]any
	override       map[string]any
	defaults       map[string]any
	kvstore        map[string]any
	pflags         map[string]FlagValue
	env            map[string][]string
	aliases        map[string]string
	typeByDefValue bool

	onConfigChange func(fsnotify.Event)

	logger *slog.Logger

	encoderRegistry EncoderRegistry
	decoderRegistry DecoderRegistry

	decodeHook mapstructure.DecodeHookFunc

	experimentalFinder     bool
	experimentalBindStruct bool
}

// New returns an initialized Viper instance.
func New() *Viper {
	v := new(Viper)
	v.keyDelim = "."
	v.configName = "config"
	v.configPermissions = os.FileMode(0o644)
	v.fs = afero.NewOsFs()
	v.config = make(map[string]any)
	v.parents = []string{}
	v.override = make(map[string]any)
	v.defaults = make(map[string]any)
	v.kvstore = make(map[string]any)
	v.pflags = make(map[string]FlagValue)
	v.env = make(map[string][]string)
	v.aliases = make(map[string]string)
	v.typeByDefValue = false
	v.logger = slog.New(&discardHandler{})

	codecRegistry := NewCodecRegistry()

	v.encoderRegistry = codecRegistry
	v.decoderRegistry = codecRegistry

	v.experimentalFinder = features.Finder
	v.experimentalBindStruct = features.BindStruct

	return v
}

// Option configures Viper using the functional options paradigm popularized by Rob Pike and Dave Cheney.
// If you're unfamiliar with this style,
// see https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html and
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis.
type Option interface {
	apply(v *Viper)
}

type optionFunc func(v *Viper)

func (fn optionFunc) apply(v *Viper) {
	fn(v)
}

// KeyDelimiter sets the delimiter used for determining key parts.
// By default it's value is ".".
func KeyDelimiter(d string) Option {
	return optionFunc(func(v *Viper) {
		v.keyDelim = d
	})
}

// StringReplacer applies a set of replacements to a string.
type StringReplacer interface {
	// Replace returns a copy of s with all replacements performed.
	Replace(s string) string
}

// EnvKeyReplacer sets a replacer used for mapping environment variables to internal keys.
func EnvKeyReplacer(r StringReplacer) Option {
	return optionFunc(func(v *Viper) {
		if r == nil {
			return
		}

		v.envKeyReplacer = r
	})
}

// WithDecodeHook sets a default decode hook for mapstructure.
func WithDecodeHook(h mapstructure.DecodeHookFunc) Option {
	return optionFunc(func(v *Viper) {
		if h == nil {
			return
		}

		v.decodeHook = h
	})
}

// NewWithOptions creates a new Viper instance.
func NewWithOptions(opts ...Option) *Viper {
	v := New()

	for _, opt := range opts {
		opt.apply(v)
	}

	return v
}

// SetOptions sets the options on the global Viper instance.
//
// Be careful when using this function: subsequent calls may override options you set.
// It's always better to use a local Viper instance.
func SetOptions(opts ...Option) {
	for _, opt := range opts {
		opt.apply(v)
	}
}

// Reset is intended for testing, will reset all to default settings.
// In the public interface for the viper package so applications
// can use it in their testing as well.
func Reset() {
	v = New()
	SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

	resetRemote()
}

// SupportedExts are universally supported extensions.
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

// OnConfigChange sets the event handler that is called when a config file changes.
func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) }

// OnConfigChange sets the event handler that is called when a config file changes.
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
	v.onConfigChange = run
}

// WatchConfig starts watching a config file for changes.
func WatchConfig() { v.WatchConfig() }

// WatchConfig starts watching a config file for changes.
func (v *Viper) WatchConfig() {
	initWG := sync.WaitGroup{}
	initWG.Add(1)
	go func() {
		watcher, err := fsnotify.NewWatcher()
		if err != nil {
			v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err))
			os.Exit(1)
		}
		defer watcher.Close()
		// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
		filename, err := v.getConfigFile()
		if err != nil {
			v.logger.Error(fmt.Sprintf("get config file: %s", err))
			initWG.Done()
			return
		}

		configFile := filepath.Clean(filename)
		configDir, _ := filepath.Split(configFile)
		realConfigFile, _ := filepath.EvalSymlinks(filename)

		eventsWG := sync.WaitGroup{}
		eventsWG.Add(1)
		go func() {
			for {
				select {
				case event, ok := <-watcher.Events:
					if !ok { // 'Events' channel is closed
						eventsWG.Done()
						return
					}
					currentConfigFile, _ := filepath.EvalSymlinks(filename)
					// we only care about the config file with the following cases:
					// 1 - if the config file was modified or created
					// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
					if (filepath.Clean(event.Name) == configFile &&
						(event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) ||
						(currentConfigFile != "" && currentConfigFile != realConfigFile) {
						realConfigFile = currentConfigFile
						err := v.ReadInConfig()
						if err != nil {
							v.logger.Error(fmt.Sprintf("read config file: %s", err))
						}
						if v.onConfigChange != nil {
							v.onConfigChange(event)
						}
					} else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) {
						eventsWG.Done()
						return
					}

				case err, ok := <-watcher.Errors:
					if ok { // 'Errors' channel is not closed
						v.logger.Error(fmt.Sprintf("watcher error: %s", err))
					}
					eventsWG.Done()
					return
				}
			}
		}()
		err = watcher.Add(configDir)
		if err != nil {
			v.logger.Error(fmt.Sprintf("failed to add watcher: %s", err))
			initWG.Done()
			return
		}
		initWG.Done()   // done initializing the watch in this go routine, so the parent routine can move on...
		eventsWG.Wait() // now, wait for event loop to end in this go-routine...
	}()
	initWG.Wait() // make sure that the go routine above fully ended before returning
}

// SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths.
func SetConfigFile(in string) { v.SetConfigFile(in) }

// SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths.
func (v *Viper) SetConfigFile(in string) {
	if in != "" {
		v.configFile = in
	}
}

// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use.
// E.g. if your prefix is "spf", the env registry will look for env
// variables that start with "SPF_".
func SetEnvPrefix(in string) { v.SetEnvPrefix(in) }

// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use.
// E.g. if your prefix is "spf", the env registry will look for env
// variables that start with "SPF_".
func (v *Viper) SetEnvPrefix(in string) {
	if in != "" {
		v.envPrefix = in
	}
}

// GetEnvPrefix returns the environment variable prefix.
func GetEnvPrefix() string { return v.GetEnvPrefix() }

// GetEnvPrefix returns the environment variable prefix.
func (v *Viper) GetEnvPrefix() string {
	return v.envPrefix
}

func (v *Viper) mergeWithEnvPrefix(in string) string {
	if v.envPrefix != "" {
		return strings.ToUpper(v.envPrefix + "_" + in)
	}

	return strings.ToUpper(in)
}

// AllowEmptyEnv tells Viper to consider set,
// but empty environment variables as valid values instead of falling back.
// For backward compatibility reasons this is false by default.
func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) }

// AllowEmptyEnv tells Viper to consider set,
// but empty environment variables as valid values instead of falling back.
// For backward compatibility reasons this is false by default.
func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) {
	v.allowEmptyEnv = allowEmptyEnv
}

// TODO: should getEnv logic be moved into find(). Can generalize the use of
// rewriting keys many things, Ex: Get('someKey') -> some_key
// (camel case to snake case for JSON keys perhaps)

// getEnv is a wrapper around os.Getenv which replaces characters in the original
// key. This allows env vars which have different keys than the config object
// keys.
func (v *Viper) getEnv(key string) (string, bool) {
	if v.envKeyReplacer != nil {
		key = v.envKeyReplacer.Replace(key)
	}

	val, ok := os.LookupEnv(key)

	return val, ok && (v.allowEmptyEnv || val != "")
}

// ConfigFileUsed returns the file used to populate the config registry.
func ConfigFileUsed() string { return v.ConfigFileUsed() }

// ConfigFileUsed returns the file used to populate the config registry.
func (v *Viper) ConfigFileUsed() string { return v.configFile }

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func AddConfigPath(in string) { v.AddConfigPath(in) }

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func (v *Viper) AddConfigPath(in string) {
	if v.finder != nil {
		v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "AddConfigPath"))
	}

	if in != "" {
		absin := absPathify(v.logger, in)

		v.logger.Info("adding path to search paths", "path", absin)
		if !slices.Contains(v.configPaths, absin) {
			v.configPaths = append(v.configPaths, absin)
		}
	}
}

// searchMap recursively searches for a value for path in source map.
// Returns nil if not found.
// Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchMap(source map[string]any, path []string) any {
	if len(path) == 0 {
		return source
	}

	next, ok := source[path[0]]
	if ok {
		// Fast path
		if len(path) == 1 {
			return next
		}

		// Nested case
		switch next := next.(type) {
		case map[any]any:
			return v.searchMap(cast.ToStringMap(next), path[1:])
		case map[string]any:
			// Type assertion is safe here since it is only reached
			// if the type of `next` is the same as the type being asserted
			return v.searchMap(next, path[1:])
		default:
			// got a value but nested key expected, return "nil" for not found
			return nil
		}
	}
	return nil
}

// searchIndexableWithPathPrefixes recursively searches for a value for path in source map/slice.
//
// While searchMap() considers each path element as a single map key or slice index, this
// function searches for, and prioritizes, merged path elements.
// e.g., if in the source, "foo" is defined with a sub-key "bar", and "foo.bar"
// is also defined, this latter value is returned for path ["foo", "bar"].
//
// This should be useful only at config level (other maps may not contain dots
// in their keys).
//
// Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchIndexableWithPathPrefixes(source any, path []string) any {
	if len(path) == 0 {
		return source
	}

	// search for path prefixes, starting from the longest one
	for i := len(path); i > 0; i-- {
		prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim))

		var val any
		switch sourceIndexable := source.(type) {
		case []any:
			val = v.searchSliceWithPathPrefixes(sourceIndexable, prefixKey, i, path)
		case map[string]any:
			val = v.searchMapWithPathPrefixes(sourceIndexable, prefixKey, i, path)
		}
		if val != nil {
			return val
		}
	}

	// not found
	return nil
}

// searchSliceWithPathPrefixes searches for a value for path in sourceSlice
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchSliceWithPathPrefixes(
	sourceSlice []any,
	prefixKey string,
	pathIndex int,
	path []string,
) any {
	// if the prefixKey is not a number or it is out of bounds of the slice
	index, err := strconv.Atoi(prefixKey)
	if err != nil || len(sourceSlice) <= index {
		return nil
	}

	next := sourceSlice[index]

	// Fast path
	if pathIndex == len(path) {
		return next
	}

	switch n := next.(type) {
	case map[any]any:
		return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
	case map[string]any, []any:
		return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
	default:
		// got a value but nested key expected, do nothing and look for next prefix
	}

	// not found
	return nil
}

// searchMapWithPathPrefixes searches for a value for path in sourceMap
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchMapWithPathPrefixes(
	sourceMap map[string]any,
	prefixKey string,
	pathIndex int,
	path []string,
) any {
	next, ok := sourceMap[prefixKey]
	if !ok {
		return nil
	}

	// Fast path
	if pathIndex == len(path) {
		return next
	}

	// Nested case
	switch n := next.(type) {
	case map[any]any:
		return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
	case map[string]any, []any:
		return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
	default:
		// got a value but nested key expected, do nothing and look for next prefix
	}

	// not found
	return nil
}

// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
// on its path in the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]any) string {
	var parentVal any
	for i := 1; i < len(path); i++ {
		parentVal = v.searchMap(m, path[0:i])
		if parentVal == nil {
			// not found, no need to add more path elements
			return ""
		}
		switch parentVal.(type) {
		case map[any]any:
			continue
		case map[string]any:
			continue
		default:
			// parentVal is a regular value which shadows "path"
			return strings.Join(path[0:i], v.keyDelim)
		}
	}
	return ""
}

// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
// in a sub-path of the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInFlatMap(path []string, mi any) string {
	// unify input map
	var m map[string]interface{}
	switch miv := mi.(type) {
	case map[string]string:
		m = castMapStringToMapInterface(miv)
	case map[string]FlagValue:
		m = castMapFlagToMapInterface(miv)
	default:
		return ""
	}

	// scan paths
	var parentKey string
	for i := 1; i < len(path); i++ {
		parentKey = strings.Join(path[0:i], v.keyDelim)
		if _, ok := m[parentKey]; ok {
			return parentKey
		}
	}
	return ""
}

// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
// in the environment, when automatic env is on.
// e.g., if "foo.bar" has a value in the environment, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
	var parentKey string
	for i := 1; i < len(path); i++ {
		parentKey = strings.Join(path[0:i], v.keyDelim)
		if _, ok := v.getEnv(v.mergeWithEnvPrefix(parentKey)); ok {
			return parentKey
		}
	}
	return ""
}

// SetTypeByDefaultValue enables or disables the inference of a key value's
// type when the Get function is used based upon a key's default value as
// opposed to the value returned based on the normal fetch logic.
//
// For example, if a key has a default value of []string{} and the same key
// is set via an environment variable to "a b c", a call to the Get function
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
//	[]string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
//	"a b c"
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }

// SetTypeByDefaultValue enables or disables the inference of a key value's
// type when the Get function is used based upon a key's default value as
// opposed to the value returned based on the normal fetch logic.
//
// For example, if a key has a default value of []string{} and the same key
// is set via an environment variable to "a b c", a call to the Get function
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
//	[]string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
//	"a b c"
func (v *Viper) SetTypeByDefaultValue(enable bool) {
	v.typeByDefValue = enable
}

// GetViper gets the global Viper instance.
func GetViper() *Viper {
	return v
}

// Get can retrieve any value given the key to use.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func Get(key string) any { return v.Get(key) }

// Get retrieves the value associated with the key.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func (v *Viper) Get(key string) any {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, true)
	if val == nil {
		return nil
	}

	if v.typeByDefValue {
		// TODO(bep) this branch isn't covered by a single test.
		valType := val
		path := strings.Split(lcaseKey, v.keyDelim)
		defVal := v.searchMap(v.defaults, path)
		if defVal != nil {
			valType = defVal
		}

		switch valType.(type) {
		case bool:
			return cast.ToBool(val)
		case string:
			return cast.ToString(val)
		case int32, int16, int8, int:
			return cast.ToInt(val)
		case uint:
			return cast.ToUint(val)
		case uint32:
			return cast.ToUint32(val)
		case uint64:
			return cast.ToUint64(val)
		case int64:
			return cast.ToInt64(val)
		case float64, float32:
			return cast.ToFloat64(val)
		case time.Time:
			return cast.ToTime(val)
		case time.Duration:
			return cast.ToDuration(val)
		case []string:
			return cast.ToStringSlice(val)
		case []int:
			return cast.ToIntSlice(val)
		case []time.Duration:
			return cast.ToDurationSlice(val)
		}
	}

	return val
}

// Sub returns new Viper instance representing a sub tree of this instance.
// Sub is case-insensitive for a key.
func Sub(key string) *Viper { return v.Sub(key) }

// Sub returns a new Viper instance representing a sub tree of this instance.
// Sub is case-insensitive for a key.
func (v *Viper) Sub(key string) *Viper {
	subv := New()
	data := v.Get(key)
	if data == nil {
		return nil
	}

	if reflect.TypeOf(data).Kind() == reflect.Map {
		subv.parents = append([]string(nil), v.parents...)
		subv.parents = append(subv.parents, strings.ToLower(key))
		subv.automaticEnvApplied = v.automaticEnvApplied
		subv.envPrefix = v.envPrefix
		subv.envKeyReplacer = v.envKeyReplacer
		subv.keyDelim = v.keyDelim
		subv.config = cast.ToStringMap(data)
		return subv
	}
	return nil
}

// GetString returns the value associated with the key as a string.
func GetString(key string) string { return v.GetString(key) }

// GetString returns the value associated with the key as a string.
func (v *Viper) GetString(key string) string {
	return cast.ToString(v.Get(key))
}

// GetBool returns the value associated with the key as a boolean.
func GetBool(key string) bool { return v.GetBool(key) }

// GetBool returns the value associated with the key as a boolean.
func (v *Viper) GetBool(key string) bool {
	return cast.ToBool(v.Get(key))
}

// GetInt returns the value associated with the key as an integer.
func GetInt(key string) int { return v.GetInt(key) }

// GetInt returns the value associated with the key as an integer.
func (v *Viper) GetInt(key string) int {
	return cast.ToInt(v.Get(key))
}

// GetInt32 returns the value associated with the key as an integer.
func GetInt32(key string) int32 { return v.GetInt32(key) }

// GetInt32 returns the value associated with the key as an integer.
func (v *Viper) GetInt32(key string) int32 {
	return cast.ToInt32(v.Get(key))
}

// GetInt64 returns the value associated with the key as an integer.
func GetInt64(key string) int64 { return v.GetInt64(key) }

// GetInt64 returns the value associated with the key as an integer.
func (v *Viper) GetInt64(key string) int64 {
	return cast.ToInt64(v.Get(key))
}

// GetUint8 returns the value associated with the key as an unsigned integer.
func GetUint8(key string) uint8 { return v.GetUint8(key) }

// GetUint8 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint8(key string) uint8 {
	return cast.ToUint8(v.Get(key))
}

// GetUint returns the value associated with the key as an unsigned integer.
func GetUint(key string) uint { return v.GetUint(key) }

// GetUint returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint(key string) uint {
	return cast.ToUint(v.Get(key))
}

// GetUint16 returns the value associated with the key as an unsigned integer.
func GetUint16(key string) uint16 { return v.GetUint16(key) }

// GetUint16 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint16(key string) uint16 {
	return cast.ToUint16(v.Get(key))
}

// GetUint32 returns the value associated with the key as an unsigned integer.
func GetUint32(key string) uint32 { return v.GetUint32(key) }

// GetUint32 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint32(key string) uint32 {
	return cast.ToUint32(v.Get(key))
}

// GetUint64 returns the value associated with the key as an unsigned integer.
func GetUint64(key string) uint64 { return v.GetUint64(key) }

// GetUint64 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint64(key string) uint64 {
	return cast.ToUint64(v.Get(key))
}

// GetFloat64 returns the value associated with the key as a float64.
func GetFloat64(key string) float64 { return v.GetFloat64(key) }

// GetFloat64 returns the value associated with the key as a float64.
func (v *Viper) GetFloat64(key string) float64 {
	return cast.ToFloat64(v.Get(key))
}

// GetTime returns the value associated with the key as time.
func GetTime(key string) time.Time { return v.GetTime(key) }

// GetTime returns the value associated with the key as time.
func (v *Viper) GetTime(key string) time.Time {
	return cast.ToTime(v.Get(key))
}

// GetDuration returns the value associated with the key as a duration.
func GetDuration(key string) time.Duration { return v.GetDuration(key) }

// GetDuration returns the value associated with the key as a duration.
func (v *Viper) GetDuration(key string) time.Duration {
	return cast.ToDuration(v.Get(key))
}

// GetIntSlice returns the value associated with the key as a slice of int values.
func GetIntSlice(key string) []int { return v.GetIntSlice(key) }

// GetIntSlice returns the value associated with the key as a slice of int values.
func (v *Viper) GetIntSlice(key string) []int {
	return cast.ToIntSlice(v.Get(key))
}

// GetStringSlice returns the value associated with the key as a slice of strings.
func GetStringSlice(key string) []string { return v.GetStringSlice(key) }

// GetStringSlice returns the value associated with the key as a slice of strings.
func (v *Viper) GetStringSlice(key string) []string {
	return cast.ToStringSlice(v.Get(key))
}

// GetStringMap returns the value associated with the key as a map of interfaces.
func GetStringMap(key string) map[string]any { return v.GetStringMap(key) }

// GetStringMap returns the value associated with the key as a map of interfaces.
func (v *Viper) GetStringMap(key string) map[string]any {
	return cast.ToStringMap(v.Get(key))
}

// GetStringMapString returns the value associated with the key as a map of strings.
func GetStringMapString(key string) map[string]string { return v.GetStringMapString(key) }

// GetStringMapString returns the value associated with the key as a map of strings.
func (v *Viper) GetStringMapString(key string) map[string]string {
	return cast.ToStringMapString(v.Get(key))
}

// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func GetStringMapStringSlice(key string) map[string][]string { return v.GetStringMapStringSlice(key) }

// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (v *Viper) GetStringMapStringSlice(key string) map[string][]string {
	return cast.ToStringMapStringSlice(v.Get(key))
}

// GetSizeInBytes returns the size of the value associated with the given key
// in bytes.
func GetSizeInBytes(key string) uint { return v.GetSizeInBytes(key) }

// GetSizeInBytes returns the size of the value associated with the given key
// in bytes.
func (v *Viper) GetSizeInBytes(key string) uint {
	sizeStr := cast.ToString(v.Get(key))
	return parseSizeInBytes(sizeStr)
}

// UnmarshalKey takes a single key and unmarshals it into a Struct.
func UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
	return v.UnmarshalKey(key, rawVal, opts...)
}

// UnmarshalKey takes a single key and unmarshals it into a Struct.
func (v *Viper) UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
	return decode(v.Get(key), v.defaultDecoderConfig(rawVal, opts...))
}

// Unmarshal unmarshals the config into a Struct. Make sure that the tags
// on the fields of the structure are properly set.
func Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
	return v.Unmarshal(rawVal, opts...)
}

// Unmarshal unmarshals the config into a Struct. Make sure that the tags
// on the fields of the structure are properly set.
func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
	keys := v.AllKeys()

	if v.experimentalBindStruct {
		// TODO: make this optional?
		structKeys, err := v.decodeStructKeys(rawVal, opts...)
		if err != nil {
			return err
		}

		keys = append(keys, structKeys...)
	}

	// TODO: struct keys should be enough?
	return decode(v.getSettings(keys), v.defaultDecoderConfig(rawVal, opts...))
}

func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]string, error) {
	var structKeyMap map[string]any

	err := decode(input, v.defaultDecoderConfig(&structKeyMap, opts...))
	if err != nil {
		return nil, err
	}

	flattenedStructKeyMap := v.flattenAndMergeMap(map[string]bool{}, structKeyMap, "")

	r := make([]string, 0, len(flattenedStructKeyMap))
	for v := range flattenedStructKeyMap {
		r = append(r, v)
	}

	return r, nil
}

// defaultDecoderConfig returns default mapstructure.DecoderConfig with support
// of time.Duration values & string slices.
func (v *Viper) defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
	decodeHook := v.decodeHook
	if decodeHook == nil {
		decodeHook = mapstructure.ComposeDecodeHookFunc(
			mapstructure.StringToTimeDurationHookFunc(),
			// mapstructure.StringToSliceHookFunc(","),
			stringToWeakSliceHookFunc(","),
		)
	}

	c := &mapstructure.DecoderConfig{
		Metadata:         nil,
		WeaklyTypedInput: true,
		DecodeHook:       decodeHook,
	}

	for _, opt := range opts {
		opt(c)
	}

	// Do not allow overwriting the output
	c.Result = output

	return c
}

// As of mapstructure v2.0.0 StringToSliceHookFunc checks if the return type is a string slice.
// This function removes that check.
// TODO: implement a function that checks if the value can be converted to the return type and use it instead.
func stringToWeakSliceHookFunc(sep string) mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{},
	) (interface{}, error) {
		if f.Kind() != reflect.String || t.Kind() != reflect.Slice {
			return data, nil
		}

		raw := data.(string)
		if raw == "" {
			return []string{}, nil
		}

		return strings.Split(raw, sep), nil
	}
}

// decode is a wrapper around mapstructure.Decode that mimics the WeakDecode functionality.
func decode(input any, config *mapstructure.DecoderConfig) error {
	decoder, err := mapstructure.NewDecoder(config)
	if err != nil {
		return err
	}
	return decoder.Decode(input)
}

// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent
// in the destination struct.
func UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
	return v.UnmarshalExact(rawVal, opts...)
}

// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent
// in the destination struct.
func (v *Viper) UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
	config := v.defaultDecoderConfig(rawVal, opts...)
	config.ErrorUnused = true

	keys := v.AllKeys()

	if v.experimentalBindStruct {
		// TODO: make this optional?
		structKeys, err := v.decodeStructKeys(rawVal, opts...)
		if err != nil {
			return err
		}

		keys = append(keys, structKeys...)
	}

	// TODO: struct keys should be enough?
	return decode(v.getSettings(keys), config)
}

// BindPFlags binds a full flag set to the configuration, using each flag's long
// name as the config key.
func BindPFlags(flags *pflag.FlagSet) error { return v.BindPFlags(flags) }

// BindPFlags binds a full flag set to the configuration, using each flag's long
// name as the config key.
func (v *Viper) BindPFlags(flags *pflag.FlagSet) error {
	return v.BindFlagValues(pflagValueSet{flags})
}

// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
//	serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
//	Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) }

// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
//	serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
//	Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error {
	if flag == nil {
		return fmt.Errorf("flag for %q is nil", key)
	}
	return v.BindFlagValue(key, pflagValue{flag})
}

// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long
// name as the config key.
func BindFlagValues(flags FlagValueSet) error { return v.BindFlagValues(flags) }

// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long
// name as the config key.
func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) {
	flags.VisitAll(func(flag FlagValue) {
		if err = v.BindFlagValue(flag.Name(), flag); err != nil {
			return
		}
	})
	return nil
}

// BindFlagValue binds a specific key to a FlagValue.
func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) }

// BindFlagValue binds a specific key to a FlagValue.
func (v *Viper) BindFlagValue(key string, flag FlagValue) error {
	if flag == nil {
		return fmt.Errorf("flag for %q is nil", key)
	}
	v.pflags[strings.ToLower(key)] = flag
	return nil
}

// BindEnv binds a Viper key to a ENV variable.
// ENV variables are case sensitive.
// If only a key is provided, it will use the env key matching the key, uppercased.
// If more arguments are provided, they will represent the env variable names that
// should bind to this key and will be taken in the specified order.
// EnvPrefix will be used when set when env name is not provided.
func BindEnv(input ...string) error { return v.BindEnv(input...) }

// BindEnv binds a Viper key to a ENV variable.
// ENV variables are case sensitive.
// If only a key is provided, it will use the env key matching the key, uppercased.
// If more arguments are provided, they will represent the env variable names that
// should bind to this key and will be taken in the specified order.
// EnvPrefix will be used when set when env name is not provided.
func (v *Viper) BindEnv(input ...string) error {
	if len(input) == 0 {
		return fmt.Errorf("missing key to bind to")
	}

	key := strings.ToLower(input[0])

	if len(input) == 1 {
		v.env[key] = append(v.env[key], v.mergeWithEnvPrefix(key))
	} else {
		v.env[key] = append(v.env[key], input[1:]...)
	}

	return nil
}

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
func MustBindEnv(input ...string) { v.MustBindEnv(input...) }

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
func (v *Viper) MustBindEnv(input ...string) {
	if err := v.BindEnv(input...); err != nil {
		panic(fmt.Sprintf("error while binding environment variable: %v", err))
	}
}

// Given a key, find the value.
//
// Viper will check to see if an alias exists first.
// Viper will then check in the following order:
// flag, env, config file, key/value store.
// Lastly, if no value was found and flagDefault is true, and if the key
// corresponds to a flag, the flag's default value is returned.
//
// Note: this assumes a lower-cased key given.
func (v *Viper) find(lcaseKey string, flagDefault bool) any {
	var (
		val    any
		exists bool
		path   = strings.Split(lcaseKey, v.keyDelim)
		nested = len(path) > 1
	)

	// compute the path through the nested maps to the nested value
	if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
		return nil
	}

	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path = strings.Split(lcaseKey, v.keyDelim)
	nested = len(path) > 1

	// Set() override first
	val = v.searchMap(v.override, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
		return nil
	}

	// PFlag override next
	flag, exists := v.pflags[lcaseKey]
	if exists && flag.HasChanged() {
		switch flag.ValueType() {
		case "int", "int8", "int16", "int32", "int64":
			return cast.ToInt(flag.ValueString())
		case "bool":
			return cast.ToBool(flag.ValueString())
		case "stringSlice", "stringArray":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return res
		case "boolSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToBoolSlice(res)
		case "intSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToIntSlice(res)
		case "uintSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToUintSlice(res)
		case "float64Slice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToFloat64Slice(res)
		case "durationSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			slice := strings.Split(s, ",")
			return cast.ToDurationSlice(slice)
		case "stringToString":
			return stringToStringConv(flag.ValueString())
		case "stringToInt":
			return stringToIntConv(flag.ValueString())
		default:
			return flag.ValueString()
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
		return nil
	}

	// Env override next
	if v.automaticEnvApplied {
		envKey := strings.Join(append(v.parents, lcaseKey), ".")
		// even if it hasn't been registered, if automaticEnv is used,
		// check any Get request
		if val, ok := v.getEnv(v.mergeWithEnvPrefix(envKey)); ok {
			return val
		}
		if nested && v.isPathShadowedInAutoEnv(path) != "" {
			return nil
		}
	}
	envkeys, exists := v.env[lcaseKey]
	if exists {
		for _, envkey := range envkeys {
			if val, ok := v.getEnv(envkey); ok {
				return val
			}
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
		return nil
	}

	// Config file next
	val = v.searchIndexableWithPathPrefixes(v.config, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
		return nil
	}

	// K/V store next
	val = v.searchMap(v.kvstore, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
		return nil
	}

	// Default next
	val = v.searchMap(v.defaults, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
		return nil
	}

	if flagDefault {
		// last chance: if no value is found and a flag does exist for the key,
		// get the flag's default value even if the flag's value has not been set.
		if flag, exists := v.pflags[lcaseKey]; exists {
			switch flag.ValueType() {
			case "int", "int8", "int16", "int32", "int64":
				return cast.ToInt(flag.ValueString())
			case "bool":
				return cast.ToBool(flag.ValueString())
			case "stringSlice", "stringArray":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return res
			case "boolSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToBoolSlice(res)
			case "intSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToIntSlice(res)
			case "uintSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToUintSlice(res)
			case "float64Slice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToFloat64Slice(res)
			case "stringToString":
				return stringToStringConv(flag.ValueString())
			case "stringToInt":
				return stringToIntConv(flag.ValueString())
			case "durationSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				slice := strings.Split(s, ",")
				return cast.ToDurationSlice(slice)
			default:
				return flag.ValueString()
			}
		}
		// last item, no need to check shadowing
	}

	return nil
}

func readAsCSV(val string) ([]string, error) {
	if val == "" {
		return []string{}, nil
	}
	stringReader := strings.NewReader(val)
	csvReader := csv.NewReader(stringReader)
	return csvReader.Read()
}

// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/master/string_to_string.go#L79
// alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap.
func stringToStringConv(val string) any {
	val = strings.Trim(val, "[]")
	// An empty string would cause an empty map
	if val == "" {
		return map[string]any{}
	}
	r := csv.NewReader(strings.NewReader(val))
	ss, err := r.Read()
	if err != nil {
		return nil
	}
	out := make(map[string]any, len(ss))
	for _, pair := range ss {
		k, vv, found := strings.Cut(pair, "=")
		if !found {
			return nil
		}
		out[k] = vv
	}
	return out
}

// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/d5e0c0615acee7028e1e2740a11102313be88de1/string_to_int.go#L68
// alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap.
func stringToIntConv(val string) any {
	val = strings.Trim(val, "[]")
	// An empty string would cause an empty map
	if val == "" {
		return map[string]any{}
	}
	ss := strings.Split(val, ",")
	out := make(map[string]any, len(ss))
	for _, pair := range ss {
		k, vv, found := strings.Cut(pair, "=")
		if !found {
			return nil
		}
		var err error
		out[k], err = strconv.Atoi(vv)
		if err != nil {
			return nil
		}
	}
	return out
}

// IsSet checks to see if the key has been set in any of the data locations.
// IsSet is case-insensitive for a key.
func IsSet(key string) bool { return v.IsSet(key) }

// IsSet checks to see if the key has been set in any of the data locations.
// IsSet is case-insensitive for a key.
func (v *Viper) IsSet(key string) bool {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, false)
	return val != nil
}

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func AutomaticEnv() { v.AutomaticEnv() }

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func (v *Viper) AutomaticEnv() {
	v.automaticEnvApplied = true
}

// SetEnvKeyReplacer sets the strings.Replacer on the viper object
// Useful for mapping an environmental variable to a key that does
// not match it.
func SetEnvKeyReplacer(r *strings.Replacer) { v.SetEnvKeyReplacer(r) }

// SetEnvKeyReplacer sets the strings.Replacer on the viper object
// Useful for mapping an environmental variable to a key that does
// not match it.
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) {
	v.envKeyReplacer = r
}

// RegisterAlias creates an alias that provides another accessor for the same key.
// This enables one to change a name without breaking the application.
func RegisterAlias(alias, key string) { v.RegisterAlias(alias, key) }

// RegisterAlias creates an alias that provides another accessor for the same key.
// This enables one to change a name without breaking the application.
func (v *Viper) RegisterAlias(alias, key string) {
	v.registerAlias(alias, strings.ToLower(key))
}

func (v *Viper) registerAlias(alias, key string) {
	alias = strings.ToLower(alias)
	if alias != key && alias != v.realKey(key) {
		_, exists := v.aliases[alias]

		if !exists {
			// if we alias something that exists in one of the maps to another
			// name, we'll never be able to get that value using the original
			// name, so move the config value to the new realkey.
			if val, ok := v.config[alias]; ok {
				delete(v.config, alias)
				v.config[key] = val
			}
			if val, ok := v.kvstore[alias]; ok {
				delete(v.kvstore, alias)
				v.kvstore[key] = val
			}
			if val, ok := v.defaults[alias]; ok {
				delete(v.defaults, alias)
				v.defaults[key] = val
			}
			if val, ok := v.override[alias]; ok {
				delete(v.override, alias)
				v.override[key] = val
			}
			v.aliases[alias] = key
		}
	} else {
		v.logger.Warn("creating circular reference alias", "alias", alias, "key", key, "real_key", v.realKey(key))
	}
}

func (v *Viper) realKey(key string) string {
	newkey, exists := v.aliases[key]
	if exists {
		v.logger.Debug("key is an alias", "alias", key, "to", newkey)

		return v.realKey(newkey)
	}
	return key
}

// InConfig checks to see if the given key (or an alias) is in the config file.
func InConfig(key string) bool { return v.InConfig(key) }

// InConfig checks to see if the given key (or an alias) is in the config file.
func (v *Viper) InConfig(key string) bool {
	lcaseKey := strings.ToLower(key)

	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path := strings.Split(lcaseKey, v.keyDelim)

	return v.searchIndexableWithPathPrefixes(v.config, path) != nil
}

// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func SetDefault(key string, value any) { v.SetDefault(key, value) }

// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func (v *Viper) SetDefault(key string, value any) {
	// If alias passed in, then set the proper default
	key = v.realKey(strings.ToLower(key))
	value = toCaseInsensitiveValue(value)

	path := strings.Split(key, v.keyDelim)
	lastKey := strings.ToLower(path[len(path)-1])
	deepestMap := deepSearch(v.defaults, path[0:len(path)-1])

	// set innermost value
	deepestMap[lastKey] = value
}

// Set sets the value for the key in the override register.
// Set is case-insensitive for a key.
// Will be used instead of values obtained via
// flags, config file, ENV, default, or key/value store.
func Set(key string, value any) { v.Set(key, value) }

// Set sets the value for the key in the override register.
// Set is case-insensitive for a key.
// Will be used instead of values obtained via
// flags, config file, ENV, default, or key/value store.
func (v *Viper) Set(key string, value any) {
	// If alias passed in, then set the proper override
	key = v.realKey(strings.ToLower(key))
	value = toCaseInsensitiveValue(value)

	path := strings.Split(key, v.keyDelim)
	lastKey := strings.ToLower(path[len(path)-1])
	deepestMap := deepSearch(v.override, path[0:len(path)-1])

	// set innermost value
	deepestMap[lastKey] = value
}

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func ReadInConfig() error { return v.ReadInConfig() }

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func (v *Viper) ReadInConfig() error {
	v.logger.Info("attempting to read in config file")
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}

	if !slices.Contains(SupportedExts, v.getConfigType()) {
		return UnsupportedConfigError(v.getConfigType())
	}

	v.logger.Debug("reading file", "file", filename)

	file, err := afero.ReadFile(v.fs, filename)
	if errors.Is(err, fs.ErrNotExist) {
		// The specified config file is missing
		return FileNotFoundError{err: err, path: filename}
	} else if err != nil {
		// We hit some other error from the filesystem that isn't a missing file
		return err
	}

	config := make(map[string]any)

	err = v.unmarshalReader(bytes.NewReader(file), config)
	if err != nil {
		return err
	}

	v.config = config
	return nil
}

// MergeInConfig merges a new configuration with an existing config.
func MergeInConfig() error { return v.MergeInConfig() }

// MergeInConfig merges a new configuration with an existing config.
func (v *Viper) MergeInConfig() error {
	v.logger.Info("attempting to merge in config file")
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}

	if !slices.Contains(SupportedExts, v.getConfigType()) {
		return UnsupportedConfigError(v.getConfigType())
	}

	file, err := afero.ReadFile(v.fs, filename)
	if err != nil {
		return err
	}

	return v.MergeConfig(bytes.NewReader(file))
}

// ReadConfig will read a configuration file, setting existing keys to nil if the
// key does not exist in the file.
func ReadConfig(in io.Reader) error { return v.ReadConfig(in) }

// ReadConfig reads a configuration file, setting existing keys to nil if the
// key does not exist in the file.
func (v *Viper) ReadConfig(in io.Reader) error {
	config := make(map[string]any)

	err := v.unmarshalReader(in, config)
	if err != nil {
		return err
	}

	v.config = config

	return nil
}

// MergeConfig merges a new configuration with an existing config.
func MergeConfig(in io.Reader) error { return v.MergeConfig(in) }

// MergeConfig merges a new configuration with an existing config.
func (v *Viper) MergeConfig(in io.Reader) error {
	config := make(map[string]any)

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

	return v.MergeConfigMap(config)
}

// MergeConfigMap merges the configuration from the map given with an existing config.
// Note that the map given may be modified.
func MergeConfigMap(cfg map[string]any) error { return v.MergeConfigMap(cfg) }

// MergeConfigMap merges the configuration from the map given with an existing config.
// Note that the map given may be modified.
func (v *Viper) MergeConfigMap(cfg map[string]any) error {
	if v.config == nil {
		v.config = make(map[string]any)
	}
	insensitiviseMap(cfg)
	mergeMaps(cfg, v.config, nil)
	return nil
}

// WriteConfig writes the current configuration to a file.
func WriteConfig() error { return v.WriteConfig() }

// WriteConfig writes the current configuration to a file.
func (v *Viper) WriteConfig() error {
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}
	return v.writeConfig(filename, true)
}

// SafeWriteConfig writes current configuration to file only if the file does not exist.
func SafeWriteConfig() error { return v.SafeWriteConfig() }

// SafeWriteConfig writes current configuration to file only if the file does not exist.
func (v *Viper) SafeWriteConfig() error {
	if len(v.configPaths) < 1 {
		return errors.New("missing configuration for 'configPath'")
	}
	return v.SafeWriteConfigAs(filepath.Join(v.configPaths[0], v.configName+"."+v.configType))
}

// WriteConfigAs writes current configuration to a given filename.
func WriteConfigAs(filename string) error { return v.WriteConfigAs(filename) }

// WriteConfigAs writes current configuration to a given filename.
func (v *Viper) WriteConfigAs(filename string) error {
	return v.writeConfig(filename, true)
}

// WriteConfigTo writes current configuration to an [io.Writer].
func WriteConfigTo(w io.Writer) error { return v.WriteConfigTo(w) }

// WriteConfigTo writes current configuration to an [io.Writer].
func (v *Viper) WriteConfigTo(w io.Writer) error {
	format := strings.ToLower(v.getConfigType())

	if !slices.Contains(SupportedExts, format) {
		return UnsupportedConfigError(format)
	}

	return v.marshalWriter(w, format)
}

// SafeWriteConfigAs writes current configuration to a given filename if it does not exist.
func SafeWriteConfigAs(filename string) error { return v.SafeWriteConfigAs(filename) }

// SafeWriteConfigAs writes current configuration to a given filename if it does not exist.
func (v *Viper) SafeWriteConfigAs(filename string) error {
	alreadyExists, err := afero.Exists(v.fs, filename)
	if alreadyExists && err == nil {
		return ConfigFileAlreadyExistsError(filename)
	}
	return v.writeConfig(filename, false)
}

func (v *Viper) writeConfig(filename string, force bool) error {
	v.logger.Info("attempting to write configuration to file")

	var configType string

	ext := filepath.Ext(filename)
	if ext != "" && ext != filepath.Base(filename) {
		configType = ext[1:]
	} else {
		configType = v.configType
	}
	if configType == "" {
		return fmt.Errorf("config type could not be determined for %s", filename)
	}

	if !slices.Contains(SupportedExts, configType) {
		return UnsupportedConfigError(configType)
	}
	if v.config == nil {
		v.config = make(map[string]any)
	}
	flags := os.O_CREATE | os.O_TRUNC | os.O_WRONLY
	if !force {
		flags |= os.O_EXCL
	}
	f, err := v.fs.OpenFile(filename, flags, v.configPermissions)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := v.marshalWriter(f, configType); err != nil {
		return err
	}

	return f.Sync()
}

func (v *Viper) unmarshalReader(in io.Reader, c map[string]any) error {
	format := strings.ToLower(v.getConfigType())
	if format == "" {
		return errors.New("cannot decode configuration: unable to determine config type")
	}

	buf := new(bytes.Buffer)
	_, err := buf.ReadFrom(in)
	if err != nil {
		return fmt.Errorf("failed to read configuration from input: %w", err)
	}

	// TODO: remove this once SupportedExts is deprecated/removed
	if !slices.Contains(SupportedExts, format) {
		return UnsupportedConfigError(format)
	}

	// TODO: return [UnsupportedConfigError] if the registry does not contain the format
	// TODO: consider deprecating this error type
	decoder, err := v.decoderRegistry.Decoder(format)
	if err != nil {
		return ConfigParseError{err}
	}

	err = decoder.Decode(buf.Bytes(), c)
	if err != nil {
		return ConfigParseError{err}
	}

	insensitiviseMap(c)
	return nil
}

// Marshal a map into Writer.
func (v *Viper) marshalWriter(w io.Writer, configType string) error {
	c := v.AllSettings()

	encoder, err := v.encoderRegistry.Encoder(configType)
	if err != nil {
		return ConfigMarshalError{err}
	}

	b, err := encoder.Encode(c)
	if err != nil {
		return ConfigMarshalError{err}
	}

	_, err = w.Write(b)
	if err != nil {
		return ConfigMarshalError{err}
	}

	return nil
}

func keyExists(k string, m map[string]any) string {
	lk := strings.ToLower(k)
	for mk := range m {
		lmk := strings.ToLower(mk)
		if lmk == lk {
			return mk
		}
	}
	return ""
}

func castToMapStringInterface(
	src map[any]any,
) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[fmt.Sprintf("%v", k)] = v
	}
	return tgt
}

func castMapStringSliceToMapInterface(src map[string][]string) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

func castMapStringToMapInterface(src map[string]string) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

func castMapFlagToMapInterface(src map[string]FlagValue) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

// mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's
// insistence on parsing nested structures as `map[any]any`
// instead of using a `string` as the key for nest structures beyond one level
// deep. Both map types are supported as there is a go-yaml fork that uses
// `map[string]any` instead.
func mergeMaps(src, tgt map[string]any, itgt map[any]any) {
	for sk, sv := range src {
		tk := keyExists(sk, tgt)
		if tk == "" {
			v.logger.Debug("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
			tgt[sk] = sv
			if itgt != nil {
				itgt[sk] = sv
			}
			continue
		}

		tv, ok := tgt[tk]
		if !ok {
			v.logger.Debug("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
			tgt[sk] = sv
			if itgt != nil {
				itgt[sk] = sv
			}
			continue
		}

		svType := reflect.TypeOf(sv)
		tvType := reflect.TypeOf(tv)

		v.logger.Debug(
			"processing",
			"key", sk,
			"st", svType,
			"tt", tvType,
			"sv", sv,
			"tv", tv,
		)

		switch ttv := tv.(type) {
		case map[any]any:
			v.logger.Debug("merging maps (must convert)")
			tsv, ok := sv.(map[any]any)
			if !ok {
				v.logger.Error(
					"Could not cast sv to map[any]any",
					"key", sk,
					"st", svType,
					"tt", tvType,
					"sv", sv,
					"tv", tv,
				)
				continue
			}

			ssv := castToMapStringInterface(tsv)
			stv := castToMapStringInterface(ttv)
			mergeMaps(ssv, stv, ttv)
		case map[string]any:
			v.logger.Debug("merging maps")
			tsv, ok := sv.(map[string]any)
			if !ok {
				v.logger.Error(
					"Could not cast sv to map[string]any",
					"key", sk,
					"st", svType,
					"tt", tvType,
					"sv", sv,
					"tv", tv,
				)
				continue
			}
			mergeMaps(tsv, ttv, nil)
		default:
			v.logger.Debug("setting value")
			tgt[tk] = sv
			if itgt != nil {
				itgt[tk] = sv
			}
		}
	}
}

// AllKeys returns all keys holding a value, regardless of where they are set.
// Nested keys are returned with a v.keyDelim separator.
func AllKeys() []string { return v.AllKeys() }

// AllKeys returns all keys holding a value, regardless of where they are set.
// Nested keys are returned with a v.keyDelim separator.
func (v *Viper) AllKeys() []string {
	m := map[string]bool{}
	// add all paths, by order of descending priority to ensure correct shadowing
	m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "")
	m = v.flattenAndMergeMap(m, v.override, "")
	m = v.mergeFlatMap(m, castMapFlagToMapInterface(v.pflags))
	m = v.mergeFlatMap(m, castMapStringSliceToMapInterface(v.env))
	m = v.flattenAndMergeMap(m, v.config, "")
	m = v.flattenAndMergeMap(m, v.kvstore, "")
	m = v.flattenAndMergeMap(m, v.defaults, "")

	// convert set of paths to list
	a := make([]string, 0, len(m))
	for x := range m {
		a = append(a, x)
	}
	return a
}

// flattenAndMergeMap recursively flattens the given map into a map[string]bool
// of key paths (used as a set, easier to manipulate than a []string):
//   - each path is merged into a single key string, delimited with v.keyDelim
//   - if a path is shadowed by an earlier value in the initial shadow map,
//     it is skipped.
//
// The resulting set of paths is merged to the given shadow set at the same time.
func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]any, prefix string) map[string]bool {
	if shadow != nil && prefix != "" && shadow[prefix] {
		// prefix is shadowed => nothing more to flatten
		return shadow
	}
	if shadow == nil {
		shadow = make(map[string]bool)
	}

	var m2 map[string]any
	if prefix != "" {
		prefix += v.keyDelim
	}
	for k, val := range m {
		fullKey := prefix + k
		switch val := val.(type) {
		case map[string]any:
			m2 = val
		case map[any]any:
			m2 = cast.ToStringMap(val)
		default:
			// immediate value
			shadow[strings.ToLower(fullKey)] = true
			continue
		}
		// recursively merge to shadow map
		shadow = v.flattenAndMergeMap(shadow, m2, fullKey)
	}
	return shadow
}

// mergeFlatMap merges the given maps, excluding values of the second map
// shadowed by values from the first map.
func (v *Viper) mergeFlatMap(shadow map[string]bool, m map[string]any) map[string]bool {
	// scan keys
outer:
	for k := range m {
		path := strings.Split(k, v.keyDelim)
		// scan intermediate paths
		var parentKey string
		for i := 1; i < len(path); i++ {
			parentKey = strings.Join(path[0:i], v.keyDelim)
			if shadow[parentKey] {
				// path is shadowed, continue
				continue outer
			}
		}
		// add key
		shadow[strings.ToLower(k)] = true
	}
	return shadow
}

// AllSettings merges all settings and returns them as a map[string]any.
func AllSettings() map[string]any { return v.AllSettings() }

// AllSettings merges all settings and returns them as a map[string]any.
func (v *Viper) AllSettings() map[string]any {
	return v.getSettings(v.AllKeys())
}

func (v *Viper) getSettings(keys []string) map[string]any {
	m := map[string]any{}
	// start from the list of keys, and construct the map one value at a time
	for _, k := range keys {
		value := v.Get(k)
		if value == nil {
			// should not happen, since AllKeys() returns only keys holding a value,
			// check just in case anything changes
			continue
		}
		path := strings.Split(k, v.keyDelim)
		lastKey := strings.ToLower(path[len(path)-1])
		deepestMap := deepSearch(m, path[0:len(path)-1])
		// set innermost value
		deepestMap[lastKey] = value
	}
	return m
}

// SetFs sets the filesystem to use to read configuration.
func SetFs(fs afero.Fs) { v.SetFs(fs) }

// SetFs sets the filesystem to use to read configuration.
func (v *Viper) SetFs(fs afero.Fs) {
	v.fs = fs
}

// SetConfigName sets name for the config file.
// Does not include extension.
func SetConfigName(in string) { v.SetConfigName(in) }

// SetConfigName sets the name for the config file.
// Does not include extension.
func (v *Viper) SetConfigName(in string) {
	if v.finder != nil {
		v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "SetConfigName"))
	}

	if in != "" {
		v.configName = in
		v.configFile = ""
	}
}

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func SetConfigType(in string) { v.SetConfigType(in) }

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func (v *Viper) SetConfigType(in string) {
	if in != "" {
		v.configType = in
	}
}

// SetConfigPermissions sets the permissions for the config file.
func SetConfigPermissions(perm os.FileMode) { v.SetConfigPermissions(perm) }

// SetConfigPermissions sets the permissions for the config file.
func (v *Viper) SetConfigPermissions(perm os.FileMode) {
	v.configPermissions = perm.Perm()
}

func (v *Viper) getConfigType() string {
	if v.configType != "" {
		return v.configType
	}

	cf, err := v.getConfigFile()
	if err != nil {
		return ""
	}

	ext := filepath.Ext(cf)

	if len(ext) > 1 {
		return ext[1:]
	}

	return ""
}

func (v *Viper) getConfigFile() (string, error) {
	if v.configFile == "" {
		cf, err := v.findConfigFile()
		if err != nil {
			return "", err
		}
		v.configFile = cf
	}
	return v.configFile, nil
}

// Debug prints all configuration registries for debugging
// purposes.
func Debug() { v.Debug() }

// DebugTo writes all configuration registries to the given [io.Writer].
func DebugTo(w io.Writer) { v.DebugTo(w) }

// Debug prints all configuration registries for debugging purposes.
func (v *Viper) Debug() { v.DebugTo(os.Stdout) }

// DebugTo writes all configurati
[truncated at 64000 of 64403 bytes]
[evidence record sha256:a9524f7bb566e76ee239d3879e895779ea4b3b9524ac6505758b1c3ccc0ed121 kind tool-call:read]
tool read <- {"path":"go.mod"}
tool read ok: module github.com/spf13/viper

go 1.23.0

require (
	github.com/fsnotify/fsnotify v1.9.0
	github.com/go-viper/mapstructure/v2 v2.4.0
	github.com/pelletier/go-toml/v2 v2.2.4
	github.com/sagikazarmark/locafero v0.12.0
	github.com/spf13/afero v1.15.0
	github.com/spf13/cast v1.10.0
	github.com/spf13/pflag v1.0.10
	github.com/stretchr/testify v1.11.1
	github.com/subosito/gotenv v1.6.0
	go.yaml.in/yaml/v3 v3.0.4
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/google/go-cmp v0.6.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	golang.org/x/sys v0.29.0 // indirect
	golang.org/x/text v0.28.0 // indirect
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

[evidence record sha256:41d0976eef27e805039c11bbcd7b0e7964f59793d50564e657f4e15076f96eb7 kind tool-call:read]
step 5: calling local:qwen3.6:35b-a3b
stopped: empty-response after 5 steps, 88048 tokens
gate typecheck passed: the command exited 0 [evidence record sha256:678a9a8f66b03253e2d8f0edf3fe5b03c9539919513354f39808b9c3d3affde5]
gate lint passed: the command exited 0 [evidence record sha256:3e20dae0bf0f3fbcf7dc32e61bafcdb86e4e8a55940af60c1dcf8d0dac7a0d94]
gate format failed: the command listed 29 offending file(s) [evidence record sha256:f0410f5a87decacab4b9458985dcdb804c6396ba8db83af89404d41d448f69ac]
gate tests failed: the command exited 1 [evidence record sha256:4d425c0eede7e7b6d35cb7bb0f730aeaf4c528390336aae45721f7db9d351677]
gate file-set failed: 1 file(s) changed but no file set was declared before editing. Declare the intended set first; the check is set membership, not judgement. [evidence record sha256:795737928382be80d8394c946c9f92f06ce596649cdcbe75eabe8e45b72ee5d8]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:6ab4a60c77fbee0a6e8337589bb53775332ffb0cc3347a678c83b90c1c1257c8]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:0d1bddaaa0335ae01c8ded7b8c8df77a28e4d5968164123819f75b8963b3a344]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:e4544917a11298f6a754745d2738fe0f6523c4e67aad3f27582878f46f5e3ebd]
gate diff-budget passed (advisory): within budget: 1 file(s) and 15 added line(s) [evidence record sha256:c3674f2e9f16424c09b9e87a25ea40ab540e766ed78af3238b099e0aa713245b]
ratchet accepted attempt 1: the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage) [evidence record sha256:523568645f9e34ea067d95cd12f9e2a8d1fc2692cd7062ea981a8d35b63a4612]
auto-resolve attempt 2 of 2
step 1: calling local:qwen3.6:35b-a3b
plan: I'll start by exploring the workspace structure and running the tests to understand the failures.
tool list <- {"path":"."}
tool list ok: .campaign/
.editorconfig
.envrc
.git/
.github/
.gitignore
.golangci.yaml
.yamlignore
.yamllint.yaml
LICENSE
Makefile
README.md
TROUBLESHOOTING.md
UPGRADE.md
encoding.go
encoding_test.go
errors.go
experimental.go
file.go
finder.go
finder_example_test.go
finder_test.go
flags.go
flags_test.go
flake.lock
flake.nix
go.mod
go.sum
internal/
logger.go
overrides_test.go
remote.go
remote/
util.go
util_test.go
viper.go
viper_test.go
viper_yaml_test.go
workplan.txt
[evidence record sha256:1bc699607b13d5c9223127f8b665bd35c450efb96095cc2441e3fea48ede3e9b kind tool-call:list]
tool shell <- {"command":"go test ./... 2>&1 | head -100","timeoutMs":60000}
tool shell failed: denied: "go test ./... 2>&1 | head -100" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:1dca7cc7e29f09570bcfecbb24550961fcb1ebceb72c1a6b8e487c7d15c9275b kind tool-call:shell]
step 2: calling local:qwen3.6:35b-a3b
tool read <- {"path":"finder.go"}
tool read ok: package viper

import (
	"errors"

	"github.com/spf13/afero"
)

// WithFinder sets a custom [Finder].
func WithFinder(f Finder) Option {
	return optionFunc(func(v *Viper) {
		if f != nil {
			return
		}

		v.finder = f
	})
}

// Finder looks for files and directories in an [afero.Fs] filesystem.
type Finder interface {
	Find(fsys afero.Fs) ([]string, error)
}

// Finders combines multiple finders into one.
func Finders(finders ...Finder) Finder {
	return &combinedFinder{finders: finders}
}

// combinedFinder is a Finder that combines multiple finders.
type combinedFinder struct {
	finders []Finder
}

// Find implements the [Finder] interface.
func (c *combinedFinder) Find(fsys afero.Fs) ([]string, error) {
	var results []string
	var errs []error

	for _, finder := range c.finders {
		if finder == nil {
			continue
		}

		r, err := finder.Find(fsys)
		if err != nil {
			errs = append(errs, err)
			continue
		}

		results = append(results, r...)
	}

	return results, errors.Join(errs...)
}

[evidence record sha256:8378f8c4c505d0812fb38fe55adc83f90e7c86a45bed30e4560fef9ea08db0bb kind tool-call:read]
step 3: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper.go"}
tool read ok: // 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.

// Viper is an application configuration system.
// It believes that applications can be configured a variety of ways
// via flags, ENVIRONMENT variables, configuration files retrieved
// from the file system, or a remote key/value store.

// Each item takes precedence over the item below it:

// overrides
// flag
// env
// config
// key/value store
// default

package viper

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	fs "io/fs"
	"log/slog"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/go-viper/mapstructure/v2"
	"github.com/spf13/afero"
	"github.com/spf13/cast"
	"github.com/spf13/pflag"

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

var v *Viper

func init() {
	v = New()
}

// A DecoderConfigOption can be passed to viper.Unmarshal to configure
// mapstructure.DecoderConfig options.
type DecoderConfigOption func(*mapstructure.DecoderConfig)

// DecodeHook returns a DecoderConfigOption which overrides the default
// DecoderConfig.DecodeHook value, the default is:
//
//	 mapstructure.ComposeDecodeHookFunc(
//			mapstructure.StringToTimeDurationHookFunc(),
//			mapstructure.StringToSliceHookFunc(","),
//		)
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
	return func(c *mapstructure.DecoderConfig) {
		c.DecodeHook = hook
	}
}

// Viper is a prioritized configuration registry. It
// maintains a set of configuration sources, fetches
// values to populate those, and provides them according
// to the source's priority.
// The priority of the sources is the following:
// 1. overrides
// 2. flags
// 3. env. variables
// 4. config file
// 5. key/value store
// 6. defaults
//
// For example, if values from the following sources were loaded:
//
//	Defaults : {
//		"secret": "",
//		"user": "default",
//		"endpoint": "https://localhost"
//	}
//	Config : {
//		"user": "root"
//		"secret": "defaultsecret"
//	}
//	Env : {
//		"secret": "somesecretkey"
//	}
//
// The resulting config will have the following values:
//
//	{
//		"secret": "somesecretkey",
//		"user": "root",
//		"endpoint": "https://localhost"
//	}
//
// Note: Vipers are not safe for concurrent Get() and Set() operations.
type Viper struct {
	// Delimiter that separates a list of keys
	// used to access a nested value in one go
	keyDelim string

	// A set of paths to look for the config file in
	configPaths []string

	// The filesystem to read config from.
	fs afero.Fs

	finder Finder

	// A set of remote providers to search for the configuration
	remoteProviders []*defaultRemoteProvider

	// Name of file to look for inside the path
	configName        string
	configFile        string
	configType        string
	configPermissions os.FileMode
	envPrefix         string

	automaticEnvApplied bool
	envKeyReplacer      StringReplacer
	allowEmptyEnv       bool

	parents        []string
	config         map[string]any
	override       map[string]any
	defaults       map[string]any
	kvstore        map[string]any
	pflags         map[string]FlagValue
	env            map[string][]string
	aliases        map[string]string
	typeByDefValue bool

	onConfigChange func(fsnotify.Event)

	logger *slog.Logger

	encoderRegistry EncoderRegistry
	decoderRegistry DecoderRegistry

	decodeHook mapstructure.DecodeHookFunc

	experimentalFinder     bool
	experimentalBindStruct bool
}

// New returns an initialized Viper instance.
func New() *Viper {
	v := new(Viper)
	v.keyDelim = "."
	v.configName = "config"
	v.configPermissions = os.FileMode(0o644)
	v.fs = afero.NewOsFs()
	v.config = make(map[string]any)
	v.parents = []string{}
	v.override = make(map[string]any)
	v.defaults = make(map[string]any)
	v.kvstore = make(map[string]any)
	v.pflags = make(map[string]FlagValue)
	v.env = make(map[string][]string)
	v.aliases = make(map[string]string)
	v.typeByDefValue = false
	v.logger = slog.New(&discardHandler{})

	codecRegistry := NewCodecRegistry()

	v.encoderRegistry = codecRegistry
	v.decoderRegistry = codecRegistry

	v.experimentalFinder = features.Finder
	v.experimentalBindStruct = features.BindStruct

	return v
}

// Option configures Viper using the functional options paradigm popularized by Rob Pike and Dave Cheney.
// If you're unfamiliar with this style,
// see https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html and
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis.
type Option interface {
	apply(v *Viper)
}

type optionFunc func(v *Viper)

func (fn optionFunc) apply(v *Viper) {
	fn(v)
}

// KeyDelimiter sets the delimiter used for determining key parts.
// By default it's value is ".".
func KeyDelimiter(d string) Option {
	return optionFunc(func(v *Viper) {
		v.keyDelim = d
	})
}

// StringReplacer applies a set of replacements to a string.
type StringReplacer interface {
	// Replace returns a copy of s with all replacements performed.
	Replace(s string) string
}

// EnvKeyReplacer sets a replacer used for mapping environment variables to internal keys.
func EnvKeyReplacer(r StringReplacer) Option {
	return optionFunc(func(v *Viper) {
		if r == nil {
			return
		}

		v.envKeyReplacer = r
	})
}

// WithDecodeHook sets a default decode hook for mapstructure.
func WithDecodeHook(h mapstructure.DecodeHookFunc) Option {
	return optionFunc(func(v *Viper) {
		if h == nil {
			return
		}

		v.decodeHook = h
	})
}

// NewWithOptions creates a new Viper instance.
func NewWithOptions(opts ...Option) *Viper {
	v := New()

	for _, opt := range opts {
		opt.apply(v)
	}

	return v
}

// SetOptions sets the options on the global Viper instance.
//
// Be careful when using this function: subsequent calls may override options you set.
// It's always better to use a local Viper instance.
func SetOptions(opts ...Option) {
	for _, opt := range opts {
		opt.apply(v)
	}
}

// Reset is intended for testing, will reset all to default settings.
// In the public interface for the viper package so applications
// can use it in their testing as well.
func Reset() {
	v = New()
	SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

	resetRemote()
}

// SupportedExts are universally supported extensions.
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

// OnConfigChange sets the event handler that is called when a config file changes.
func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) }

// OnConfigChange sets the event handler that is called when a config file changes.
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
	v.onConfigChange = run
}

// WatchConfig starts watching a config file for changes.
func WatchConfig() { v.WatchConfig() }

// WatchConfig starts watching a config file for changes.
func (v *Viper) WatchConfig() {
	initWG := sync.WaitGroup{}
	initWG.Add(1)
	go func() {
		watcher, err := fsnotify.NewWatcher()
		if err != nil {
			v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err))
			os.Exit(1)
		}
		defer watcher.Close()
		// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
		filename, err := v.getConfigFile()
		if err != nil {
			v.logger.Error(fmt.Sprintf("get config file: %s", err))
			initWG.Done()
			return
		}

		configFile := filepath.Clean(filename)
		configDir, _ := filepath.Split(configFile)
		realConfigFile, _ := filepath.EvalSymlinks(filename)

		eventsWG := sync.WaitGroup{}
		eventsWG.Add(1)
		go func() {
			for {
				select {
				case event, ok := <-watcher.Events:
					if !ok { // 'Events' channel is closed
						eventsWG.Done()
						return
					}
					currentConfigFile, _ := filepath.EvalSymlinks(filename)
					// we only care about the config file with the following cases:
					// 1 - if the config file was modified or created
					// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
					if (filepath.Clean(event.Name) == configFile &&
						(event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) ||
						(currentConfigFile != "" && currentConfigFile != realConfigFile) {
						realConfigFile = currentConfigFile
						err := v.ReadInConfig()
						if err != nil {
							v.logger.Error(fmt.Sprintf("read config file: %s", err))
						}
						if v.onConfigChange != nil {
							v.onConfigChange(event)
						}
					} else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) {
						eventsWG.Done()
						return
					}

				case err, ok := <-watcher.Errors:
					if ok { // 'Errors' channel is not closed
						v.logger.Error(fmt.Sprintf("watcher error: %s", err))
					}
					eventsWG.Done()
					return
				}
			}
		}()
		err = watcher.Add(configDir)
		if err != nil {
			v.logger.Error(fmt.Sprintf("failed to add watcher: %s", err))
			initWG.Done()
			return
		}
		initWG.Done()   // done initializing the watch in this go routine, so the parent routine can move on...
		eventsWG.Wait() // now, wait for event loop to end in this go-routine...
	}()
	initWG.Wait() // make sure that the go routine above fully ended before returning
}

// SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths.
func SetConfigFile(in string) { v.SetConfigFile(in) }

// SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths.
func (v *Viper) SetConfigFile(in string) {
	if in != "" {
		v.configFile = in
	}
}

// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use.
// E.g. if your prefix is "spf", the env registry will look for env
// variables that start with "SPF_".
func SetEnvPrefix(in string) { v.SetEnvPrefix(in) }

// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use.
// E.g. if your prefix is "spf", the env registry will look for env
// variables that start with "SPF_".
func (v *Viper) SetEnvPrefix(in string) {
	if in != "" {
		v.envPrefix = in
	}
}

// GetEnvPrefix returns the environment variable prefix.
func GetEnvPrefix() string { return v.GetEnvPrefix() }

// GetEnvPrefix returns the environment variable prefix.
func (v *Viper) GetEnvPrefix() string {
	return v.envPrefix
}

func (v *Viper) mergeWithEnvPrefix(in string) string {
	if v.envPrefix != "" {
		return strings.ToUpper(v.envPrefix + "_" + in)
	}

	return strings.ToUpper(in)
}

// AllowEmptyEnv tells Viper to consider set,
// but empty environment variables as valid values instead of falling back.
// For backward compatibility reasons this is false by default.
func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) }

// AllowEmptyEnv tells Viper to consider set,
// but empty environment variables as valid values instead of falling back.
// For backward compatibility reasons this is false by default.
func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) {
	v.allowEmptyEnv = allowEmptyEnv
}

// TODO: should getEnv logic be moved into find(). Can generalize the use of
// rewriting keys many things, Ex: Get('someKey') -> some_key
// (camel case to snake case for JSON keys perhaps)

// getEnv is a wrapper around os.Getenv which replaces characters in the original
// key. This allows env vars which have different keys than the config object
// keys.
func (v *Viper) getEnv(key string) (string, bool) {
	if v.envKeyReplacer != nil {
		key = v.envKeyReplacer.Replace(key)
	}

	val, ok := os.LookupEnv(key)

	return val, ok && (v.allowEmptyEnv || val != "")
}

// ConfigFileUsed returns the file used to populate the config registry.
func ConfigFileUsed() string { return v.ConfigFileUsed() }

// ConfigFileUsed returns the file used to populate the config registry.
func (v *Viper) ConfigFileUsed() string { return v.configFile }

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func AddConfigPath(in string) { v.AddConfigPath(in) }

// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func (v *Viper) AddConfigPath(in string) {
	if v.finder != nil {
		v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "AddConfigPath"))
	}

	if in != "" {
		absin := absPathify(v.logger, in)

		v.logger.Info("adding path to search paths", "path", absin)
		if !slices.Contains(v.configPaths, absin) {
			v.configPaths = append(v.configPaths, absin)
		}
	}
}

// searchMap recursively searches for a value for path in source map.
// Returns nil if not found.
// Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchMap(source map[string]any, path []string) any {
	if len(path) == 0 {
		return source
	}

	next, ok := source[path[0]]
	if ok {
		// Fast path
		if len(path) == 1 {
			return next
		}

		// Nested case
		switch next := next.(type) {
		case map[any]any:
			return v.searchMap(cast.ToStringMap(next), path[1:])
		case map[string]any:
			// Type assertion is safe here since it is only reached
			// if the type of `next` is the same as the type being asserted
			return v.searchMap(next, path[1:])
		default:
			// got a value but nested key expected, return "nil" for not found
			return nil
		}
	}
	return nil
}

// searchIndexableWithPathPrefixes recursively searches for a value for path in source map/slice.
//
// While searchMap() considers each path element as a single map key or slice index, this
// function searches for, and prioritizes, merged path elements.
// e.g., if in the source, "foo" is defined with a sub-key "bar", and "foo.bar"
// is also defined, this latter value is returned for path ["foo", "bar"].
//
// This should be useful only at config level (other maps may not contain dots
// in their keys).
//
// Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchIndexableWithPathPrefixes(source any, path []string) any {
	if len(path) == 0 {
		return source
	}

	// search for path prefixes, starting from the longest one
	for i := len(path); i > 0; i-- {
		prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim))

		var val any
		switch sourceIndexable := source.(type) {
		case []any:
			val = v.searchSliceWithPathPrefixes(sourceIndexable, prefixKey, i, path)
		case map[string]any:
			val = v.searchMapWithPathPrefixes(sourceIndexable, prefixKey, i, path)
		}
		if val != nil {
			return val
		}
	}

	// not found
	return nil
}

// searchSliceWithPathPrefixes searches for a value for path in sourceSlice
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchSliceWithPathPrefixes(
	sourceSlice []any,
	prefixKey string,
	pathIndex int,
	path []string,
) any {
	// if the prefixKey is not a number or it is out of bounds of the slice
	index, err := strconv.Atoi(prefixKey)
	if err != nil || len(sourceSlice) <= index {
		return nil
	}

	next := sourceSlice[index]

	// Fast path
	if pathIndex == len(path) {
		return next
	}

	switch n := next.(type) {
	case map[any]any:
		return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
	case map[string]any, []any:
		return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
	default:
		// got a value but nested key expected, do nothing and look for next prefix
	}

	// not found
	return nil
}

// searchMapWithPathPrefixes searches for a value for path in sourceMap
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchMapWithPathPrefixes(
	sourceMap map[string]any,
	prefixKey string,
	pathIndex int,
	path []string,
) any {
	next, ok := sourceMap[prefixKey]
	if !ok {
		return nil
	}

	// Fast path
	if pathIndex == len(path) {
		return next
	}

	// Nested case
	switch n := next.(type) {
	case map[any]any:
		return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
	case map[string]any, []any:
		return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
	default:
		// got a value but nested key expected, do nothing and look for next prefix
	}

	// not found
	return nil
}

// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
// on its path in the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]any) string {
	var parentVal any
	for i := 1; i < len(path); i++ {
		parentVal = v.searchMap(m, path[0:i])
		if parentVal == nil {
			// not found, no need to add more path elements
			return ""
		}
		switch parentVal.(type) {
		case map[any]any:
			continue
		case map[string]any:
			continue
		default:
			// parentVal is a regular value which shadows "path"
			return strings.Join(path[0:i], v.keyDelim)
		}
	}
	return ""
}

// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
// in a sub-path of the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInFlatMap(path []string, mi any) string {
	// unify input map
	var m map[string]interface{}
	switch miv := mi.(type) {
	case map[string]string:
		m = castMapStringToMapInterface(miv)
	case map[string]FlagValue:
		m = castMapFlagToMapInterface(miv)
	default:
		return ""
	}

	// scan paths
	var parentKey string
	for i := 1; i < len(path); i++ {
		parentKey = strings.Join(path[0:i], v.keyDelim)
		if _, ok := m[parentKey]; ok {
			return parentKey
		}
	}
	return ""
}

// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
// in the environment, when automatic env is on.
// e.g., if "foo.bar" has a value in the environment, it “shadows”
//
//	"foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
	var parentKey string
	for i := 1; i < len(path); i++ {
		parentKey = strings.Join(path[0:i], v.keyDelim)
		if _, ok := v.getEnv(v.mergeWithEnvPrefix(parentKey)); ok {
			return parentKey
		}
	}
	return ""
}

// SetTypeByDefaultValue enables or disables the inference of a key value's
// type when the Get function is used based upon a key's default value as
// opposed to the value returned based on the normal fetch logic.
//
// For example, if a key has a default value of []string{} and the same key
// is set via an environment variable to "a b c", a call to the Get function
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
//	[]string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
//	"a b c"
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }

// SetTypeByDefaultValue enables or disables the inference of a key value's
// type when the Get function is used based upon a key's default value as
// opposed to the value returned based on the normal fetch logic.
//
// For example, if a key has a default value of []string{} and the same key
// is set via an environment variable to "a b c", a call to the Get function
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
//	[]string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
//	"a b c"
func (v *Viper) SetTypeByDefaultValue(enable bool) {
	v.typeByDefValue = enable
}

// GetViper gets the global Viper instance.
func GetViper() *Viper {
	return v
}

// Get can retrieve any value given the key to use.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func Get(key string) any { return v.Get(key) }

// Get retrieves the value associated with the key.
// Get is case-insensitive for a key.
// Get has the behavior of returning the value associated with the first
// place from where it is set. Viper will check in the following order:
// override, flag, env, config file, key/value store, default
//
// Get returns an interface. For a specific value use one of the Get____ methods.
func (v *Viper) Get(key string) any {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, true)
	if val == nil {
		return nil
	}

	if v.typeByDefValue {
		// TODO(bep) this branch isn't covered by a single test.
		valType := val
		path := strings.Split(lcaseKey, v.keyDelim)
		defVal := v.searchMap(v.defaults, path)
		if defVal != nil {
			valType = defVal
		}

		switch valType.(type) {
		case bool:
			return cast.ToBool(val)
		case string:
			return cast.ToString(val)
		case int32, int16, int8, int:
			return cast.ToInt(val)
		case uint:
			return cast.ToUint(val)
		case uint32:
			return cast.ToUint32(val)
		case uint64:
			return cast.ToUint64(val)
		case int64:
			return cast.ToInt64(val)
		case float64, float32:
			return cast.ToFloat64(val)
		case time.Time:
			return cast.ToTime(val)
		case time.Duration:
			return cast.ToDuration(val)
		case []string:
			return cast.ToStringSlice(val)
		case []int:
			return cast.ToIntSlice(val)
		case []time.Duration:
			return cast.ToDurationSlice(val)
		}
	}

	return val
}

// Sub returns new Viper instance representing a sub tree of this instance.
// Sub is case-insensitive for a key.
func Sub(key string) *Viper { return v.Sub(key) }

// Sub returns a new Viper instance representing a sub tree of this instance.
// Sub is case-insensitive for a key.
func (v *Viper) Sub(key string) *Viper {
	subv := New()
	data := v.Get(key)
	if data == nil {
		return nil
	}

	if reflect.TypeOf(data).Kind() == reflect.Map {
		subv.parents = append([]string(nil), v.parents...)
		subv.parents = append(subv.parents, strings.ToLower(key))
		subv.automaticEnvApplied = v.automaticEnvApplied
		subv.envPrefix = v.envPrefix
		subv.envKeyReplacer = v.envKeyReplacer
		subv.keyDelim = v.keyDelim
		subv.config = cast.ToStringMap(data)
		return subv
	}
	return nil
}

// GetString returns the value associated with the key as a string.
func GetString(key string) string { return v.GetString(key) }

// GetString returns the value associated with the key as a string.
func (v *Viper) GetString(key string) string {
	return cast.ToString(v.Get(key))
}

// GetBool returns the value associated with the key as a boolean.
func GetBool(key string) bool { return v.GetBool(key) }

// GetBool returns the value associated with the key as a boolean.
func (v *Viper) GetBool(key string) bool {
	return cast.ToBool(v.Get(key))
}

// GetInt returns the value associated with the key as an integer.
func GetInt(key string) int { return v.GetInt(key) }

// GetInt returns the value associated with the key as an integer.
func (v *Viper) GetInt(key string) int {
	return cast.ToInt(v.Get(key))
}

// GetInt32 returns the value associated with the key as an integer.
func GetInt32(key string) int32 { return v.GetInt32(key) }

// GetInt32 returns the value associated with the key as an integer.
func (v *Viper) GetInt32(key string) int32 {
	return cast.ToInt32(v.Get(key))
}

// GetInt64 returns the value associated with the key as an integer.
func GetInt64(key string) int64 { return v.GetInt64(key) }

// GetInt64 returns the value associated with the key as an integer.
func (v *Viper) GetInt64(key string) int64 {
	return cast.ToInt64(v.Get(key))
}

// GetUint8 returns the value associated with the key as an unsigned integer.
func GetUint8(key string) uint8 { return v.GetUint8(key) }

// GetUint8 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint8(key string) uint8 {
	return cast.ToUint8(v.Get(key))
}

// GetUint returns the value associated with the key as an unsigned integer.
func GetUint(key string) uint { return v.GetUint(key) }

// GetUint returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint(key string) uint {
	return cast.ToUint(v.Get(key))
}

// GetUint16 returns the value associated with the key as an unsigned integer.
func GetUint16(key string) uint16 { return v.GetUint16(key) }

// GetUint16 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint16(key string) uint16 {
	return cast.ToUint16(v.Get(key))
}

// GetUint32 returns the value associated with the key as an unsigned integer.
func GetUint32(key string) uint32 { return v.GetUint32(key) }

// GetUint32 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint32(key string) uint32 {
	return cast.ToUint32(v.Get(key))
}

// GetUint64 returns the value associated with the key as an unsigned integer.
func GetUint64(key string) uint64 { return v.GetUint64(key) }

// GetUint64 returns the value associated with the key as an unsigned integer.
func (v *Viper) GetUint64(key string) uint64 {
	return cast.ToUint64(v.Get(key))
}

// GetFloat64 returns the value associated with the key as a float64.
func GetFloat64(key string) float64 { return v.GetFloat64(key) }

// GetFloat64 returns the value associated with the key as a float64.
func (v *Viper) GetFloat64(key string) float64 {
	return cast.ToFloat64(v.Get(key))
}

// GetTime returns the value associated with the key as time.
func GetTime(key string) time.Time { return v.GetTime(key) }

// GetTime returns the value associated with the key as time.
func (v *Viper) GetTime(key string) time.Time {
	return cast.ToTime(v.Get(key))
}

// GetDuration returns the value associated with the key as a duration.
func GetDuration(key string) time.Duration { return v.GetDuration(key) }

// GetDuration returns the value associated with the key as a duration.
func (v *Viper) GetDuration(key string) time.Duration {
	return cast.ToDuration(v.Get(key))
}

// GetIntSlice returns the value associated with the key as a slice of int values.
func GetIntSlice(key string) []int { return v.GetIntSlice(key) }

// GetIntSlice returns the value associated with the key as a slice of int values.
func (v *Viper) GetIntSlice(key string) []int {
	return cast.ToIntSlice(v.Get(key))
}

// GetStringSlice returns the value associated with the key as a slice of strings.
func GetStringSlice(key string) []string { return v.GetStringSlice(key) }

// GetStringSlice returns the value associated with the key as a slice of strings.
func (v *Viper) GetStringSlice(key string) []string {
	return cast.ToStringSlice(v.Get(key))
}

// GetStringMap returns the value associated with the key as a map of interfaces.
func GetStringMap(key string) map[string]any { return v.GetStringMap(key) }

// GetStringMap returns the value associated with the key as a map of interfaces.
func (v *Viper) GetStringMap(key string) map[string]any {
	return cast.ToStringMap(v.Get(key))
}

// GetStringMapString returns the value associated with the key as a map of strings.
func GetStringMapString(key string) map[string]string { return v.GetStringMapString(key) }

// GetStringMapString returns the value associated with the key as a map of strings.
func (v *Viper) GetStringMapString(key string) map[string]string {
	return cast.ToStringMapString(v.Get(key))
}

// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func GetStringMapStringSlice(key string) map[string][]string { return v.GetStringMapStringSlice(key) }

// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (v *Viper) GetStringMapStringSlice(key string) map[string][]string {
	return cast.ToStringMapStringSlice(v.Get(key))
}

// GetSizeInBytes returns the size of the value associated with the given key
// in bytes.
func GetSizeInBytes(key string) uint { return v.GetSizeInBytes(key) }

// GetSizeInBytes returns the size of the value associated with the given key
// in bytes.
func (v *Viper) GetSizeInBytes(key string) uint {
	sizeStr := cast.ToString(v.Get(key))
	return parseSizeInBytes(sizeStr)
}

// UnmarshalKey takes a single key and unmarshals it into a Struct.
func UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
	return v.UnmarshalKey(key, rawVal, opts...)
}

// UnmarshalKey takes a single key and unmarshals it into a Struct.
func (v *Viper) UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
	return decode(v.Get(key), v.defaultDecoderConfig(rawVal, opts...))
}

// Unmarshal unmarshals the config into a Struct. Make sure that the tags
// on the fields of the structure are properly set.
func Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
	return v.Unmarshal(rawVal, opts...)
}

// Unmarshal unmarshals the config into a Struct. Make sure that the tags
// on the fields of the structure are properly set.
func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
	keys := v.AllKeys()

	if v.experimentalBindStruct {
		// TODO: make this optional?
		structKeys, err := v.decodeStructKeys(rawVal, opts...)
		if err != nil {
			return err
		}

		keys = append(keys, structKeys...)
	}

	// TODO: struct keys should be enough?
	return decode(v.getSettings(keys), v.defaultDecoderConfig(rawVal, opts...))
}

func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]string, error) {
	var structKeyMap map[string]any

	err := decode(input, v.defaultDecoderConfig(&structKeyMap, opts...))
	if err != nil {
		return nil, err
	}

	flattenedStructKeyMap := v.flattenAndMergeMap(map[string]bool{}, structKeyMap, "")

	r := make([]string, 0, len(flattenedStructKeyMap))
	for v := range flattenedStructKeyMap {
		r = append(r, v)
	}

	return r, nil
}

// defaultDecoderConfig returns default mapstructure.DecoderConfig with support
// of time.Duration values & string slices.
func (v *Viper) defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
	decodeHook := v.decodeHook
	if decodeHook == nil {
		decodeHook = mapstructure.ComposeDecodeHookFunc(
			mapstructure.StringToTimeDurationHookFunc(),
			// mapstructure.StringToSliceHookFunc(","),
			stringToWeakSliceHookFunc(","),
		)
	}

	c := &mapstructure.DecoderConfig{
		Metadata:         nil,
		WeaklyTypedInput: true,
		DecodeHook:       decodeHook,
	}

	for _, opt := range opts {
		opt(c)
	}

	// Do not allow overwriting the output
	c.Result = output

	return c
}

// As of mapstructure v2.0.0 StringToSliceHookFunc checks if the return type is a string slice.
// This function removes that check.
// TODO: implement a function that checks if the value can be converted to the return type and use it instead.
func stringToWeakSliceHookFunc(sep string) mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{},
	) (interface{}, error) {
		if f.Kind() != reflect.String || t.Kind() != reflect.Slice {
			return data, nil
		}

		raw := data.(string)
		if raw == "" {
			return []string{}, nil
		}

		return strings.Split(raw, sep), nil
	}
}

// decode is a wrapper around mapstructure.Decode that mimics the WeakDecode functionality.
func decode(input any, config *mapstructure.DecoderConfig) error {
	decoder, err := mapstructure.NewDecoder(config)
	if err != nil {
		return err
	}
	return decoder.Decode(input)
}

// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent
// in the destination struct.
func UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
	return v.UnmarshalExact(rawVal, opts...)
}

// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent
// in the destination struct.
func (v *Viper) UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
	config := v.defaultDecoderConfig(rawVal, opts...)
	config.ErrorUnused = true

	keys := v.AllKeys()

	if v.experimentalBindStruct {
		// TODO: make this optional?
		structKeys, err := v.decodeStructKeys(rawVal, opts...)
		if err != nil {
			return err
		}

		keys = append(keys, structKeys...)
	}

	// TODO: struct keys should be enough?
	return decode(v.getSettings(keys), config)
}

// BindPFlags binds a full flag set to the configuration, using each flag's long
// name as the config key.
func BindPFlags(flags *pflag.FlagSet) error { return v.BindPFlags(flags) }

// BindPFlags binds a full flag set to the configuration, using each flag's long
// name as the config key.
func (v *Viper) BindPFlags(flags *pflag.FlagSet) error {
	return v.BindFlagValues(pflagValueSet{flags})
}

// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
//	serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
//	Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) }

// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
//	serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
//	Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error {
	if flag == nil {
		return fmt.Errorf("flag for %q is nil", key)
	}
	return v.BindFlagValue(key, pflagValue{flag})
}

// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long
// name as the config key.
func BindFlagValues(flags FlagValueSet) error { return v.BindFlagValues(flags) }

// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long
// name as the config key.
func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) {
	flags.VisitAll(func(flag FlagValue) {
		if err = v.BindFlagValue(flag.Name(), flag); err != nil {
			return
		}
	})
	return nil
}

// BindFlagValue binds a specific key to a FlagValue.
func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) }

// BindFlagValue binds a specific key to a FlagValue.
func (v *Viper) BindFlagValue(key string, flag FlagValue) error {
	if flag == nil {
		return fmt.Errorf("flag for %q is nil", key)
	}
	v.pflags[strings.ToLower(key)] = flag
	return nil
}

// BindEnv binds a Viper key to a ENV variable.
// ENV variables are case sensitive.
// If only a key is provided, it will use the env key matching the key, uppercased.
// If more arguments are provided, they will represent the env variable names that
// should bind to this key and will be taken in the specified order.
// EnvPrefix will be used when set when env name is not provided.
func BindEnv(input ...string) error { return v.BindEnv(input...) }

// BindEnv binds a Viper key to a ENV variable.
// ENV variables are case sensitive.
// If only a key is provided, it will use the env key matching the key, uppercased.
// If more arguments are provided, they will represent the env variable names that
// should bind to this key and will be taken in the specified order.
// EnvPrefix will be used when set when env name is not provided.
func (v *Viper) BindEnv(input ...string) error {
	if len(input) == 0 {
		return fmt.Errorf("missing key to bind to")
	}

	key := strings.ToLower(input[0])

	if len(input) == 1 {
		v.env[key] = append(v.env[key], v.mergeWithEnvPrefix(key))
	} else {
		v.env[key] = append(v.env[key], input[1:]...)
	}

	return nil
}

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
func MustBindEnv(input ...string) { v.MustBindEnv(input...) }

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
func (v *Viper) MustBindEnv(input ...string) {
	if err := v.BindEnv(input...); err != nil {
		panic(fmt.Sprintf("error while binding environment variable: %v", err))
	}
}

// Given a key, find the value.
//
// Viper will check to see if an alias exists first.
// Viper will then check in the following order:
// flag, env, config file, key/value store.
// Lastly, if no value was found and flagDefault is true, and if the key
// corresponds to a flag, the flag's default value is returned.
//
// Note: this assumes a lower-cased key given.
func (v *Viper) find(lcaseKey string, flagDefault bool) any {
	var (
		val    any
		exists bool
		path   = strings.Split(lcaseKey, v.keyDelim)
		nested = len(path) > 1
	)

	// compute the path through the nested maps to the nested value
	if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
		return nil
	}

	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path = strings.Split(lcaseKey, v.keyDelim)
	nested = len(path) > 1

	// Set() override first
	val = v.searchMap(v.override, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
		return nil
	}

	// PFlag override next
	flag, exists := v.pflags[lcaseKey]
	if exists && flag.HasChanged() {
		switch flag.ValueType() {
		case "int", "int8", "int16", "int32", "int64":
			return cast.ToInt(flag.ValueString())
		case "bool":
			return cast.ToBool(flag.ValueString())
		case "stringSlice", "stringArray":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return res
		case "boolSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToBoolSlice(res)
		case "intSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToIntSlice(res)
		case "uintSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToUintSlice(res)
		case "float64Slice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToFloat64Slice(res)
		case "durationSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			slice := strings.Split(s, ",")
			return cast.ToDurationSlice(slice)
		case "stringToString":
			return stringToStringConv(flag.ValueString())
		case "stringToInt":
			return stringToIntConv(flag.ValueString())
		default:
			return flag.ValueString()
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
		return nil
	}

	// Env override next
	if v.automaticEnvApplied {
		envKey := strings.Join(append(v.parents, lcaseKey), ".")
		// even if it hasn't been registered, if automaticEnv is used,
		// check any Get request
		if val, ok := v.getEnv(v.mergeWithEnvPrefix(envKey)); ok {
			return val
		}
		if nested && v.isPathShadowedInAutoEnv(path) != "" {
			return nil
		}
	}
	envkeys, exists := v.env[lcaseKey]
	if exists {
		for _, envkey := range envkeys {
			if val, ok := v.getEnv(envkey); ok {
				return val
			}
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
		return nil
	}

	// Config file next
	val = v.searchIndexableWithPathPrefixes(v.config, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
		return nil
	}

	// K/V store next
	val = v.searchMap(v.kvstore, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
		return nil
	}

	// Default next
	val = v.searchMap(v.defaults, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
		return nil
	}

	if flagDefault {
		// last chance: if no value is found and a flag does exist for the key,
		// get the flag's default value even if the flag's value has not been set.
		if flag, exists := v.pflags[lcaseKey]; exists {
			switch flag.ValueType() {
			case "int", "int8", "int16", "int32", "int64":
				return cast.ToInt(flag.ValueString())
			case "bool":
				return cast.ToBool(flag.ValueString())
			case "stringSlice", "stringArray":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return res
			case "boolSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToBoolSlice(res)
			case "intSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToIntSlice(res)
			case "uintSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToUintSlice(res)
			case "float64Slice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToFloat64Slice(res)
			case "stringToString":
				return stringToStringConv(flag.ValueString())
			case "stringToInt":
				return stringToIntConv(flag.ValueString())
			case "durationSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				slice := strings.Split(s, ",")
				return cast.ToDurationSlice(slice)
			default:
				return flag.ValueString()
			}
		}
		// last item, no need to check shadowing
	}

	return nil
}

func readAsCSV(val string) ([]string, error) {
	if val == "" {
		return []string{}, nil
	}
	stringReader := strings.NewReader(val)
	csvReader := csv.NewReader(stringReader)
	return csvReader.Read()
}

// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/master/string_to_string.go#L79
// alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap.
func stringToStringConv(val string) any {
	val = strings.Trim(val, "[]")
	// An empty string would cause an empty map
	if val == "" {
		return map[string]any{}
	}
	r := csv.NewReader(strings.NewReader(val))
	ss, err := r.Read()
	if err != nil {
		return nil
	}
	out := make(map[string]any, len(ss))
	for _, pair := range ss {
		k, vv, found := strings.Cut(pair, "=")
		if !found {
			return nil
		}
		out[k] = vv
	}
	return out
}

// mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/d5e0c0615acee7028e1e2740a11102313be88de1/string_to_int.go#L68
// alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap.
func stringToIntConv(val string) any {
	val = strings.Trim(val, "[]")
	// An empty string would cause an empty map
	if val == "" {
		return map[string]any{}
	}
	ss := strings.Split(val, ",")
	out := make(map[string]any, len(ss))
	for _, pair := range ss {
		k, vv, found := strings.Cut(pair, "=")
		if !found {
			return nil
		}
		var err error
		out[k], err = strconv.Atoi(vv)
		if err != nil {
			return nil
		}
	}
	return out
}

// IsSet checks to see if the key has been set in any of the data locations.
// IsSet is case-insensitive for a key.
func IsSet(key string) bool { return v.IsSet(key) }

// IsSet checks to see if the key has been set in any of the data locations.
// IsSet is case-insensitive for a key.
func (v *Viper) IsSet(key string) bool {
	lcaseKey := strings.ToLower(key)
	val := v.find(lcaseKey, false)
	return val != nil
}

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func AutomaticEnv() { v.AutomaticEnv() }

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func (v *Viper) AutomaticEnv() {
	v.automaticEnvApplied = true
}

// SetEnvKeyReplacer sets the strings.Replacer on the viper object
// Useful for mapping an environmental variable to a key that does
// not match it.
func SetEnvKeyReplacer(r *strings.Replacer) { v.SetEnvKeyReplacer(r) }

// SetEnvKeyReplacer sets the strings.Replacer on the viper object
// Useful for mapping an environmental variable to a key that does
// not match it.
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) {
	v.envKeyReplacer = r
}

// RegisterAlias creates an alias that provides another accessor for the same key.
// This enables one to change a name without breaking the application.
func RegisterAlias(alias, key string) { v.RegisterAlias(alias, key) }

// RegisterAlias creates an alias that provides another accessor for the same key.
// This enables one to change a name without breaking the application.
func (v *Viper) RegisterAlias(alias, key string) {
	v.registerAlias(alias, strings.ToLower(key))
}

func (v *Viper) registerAlias(alias, key string) {
	alias = strings.ToLower(alias)
	if alias != key && alias != v.realKey(key) {
		_, exists := v.aliases[alias]

		if !exists {
			// if we alias something that exists in one of the maps to another
			// name, we'll never be able to get that value using the original
			// name, so move the config value to the new realkey.
			if val, ok := v.config[alias]; ok {
				delete(v.config, alias)
				v.config[key] = val
			}
			if val, ok := v.kvstore[alias]; ok {
				delete(v.kvstore, alias)
				v.kvstore[key] = val
			}
			if val, ok := v.defaults[alias]; ok {
				delete(v.defaults, alias)
				v.defaults[key] = val
			}
			if val, ok := v.override[alias]; ok {
				delete(v.override, alias)
				v.override[key] = val
			}
			v.aliases[alias] = key
		}
	} else {
		v.logger.Warn("creating circular reference alias", "alias", alias, "key", key, "real_key", v.realKey(key))
	}
}

func (v *Viper) realKey(key string) string {
	newkey, exists := v.aliases[key]
	if exists {
		v.logger.Debug("key is an alias", "alias", key, "to", newkey)

		return v.realKey(newkey)
	}
	return key
}

// InConfig checks to see if the given key (or an alias) is in the config file.
func InConfig(key string) bool { return v.InConfig(key) }

// InConfig checks to see if the given key (or an alias) is in the config file.
func (v *Viper) InConfig(key string) bool {
	lcaseKey := strings.ToLower(key)

	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path := strings.Split(lcaseKey, v.keyDelim)

	return v.searchIndexableWithPathPrefixes(v.config, path) != nil
}

// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func SetDefault(key string, value any) { v.SetDefault(key, value) }

// SetDefault sets the default value for this key.
// SetDefault is case-insensitive for a key.
// Default only used when no value is provided by the user via flag, config or ENV.
func (v *Viper) SetDefault(key string, value any) {
	// If alias passed in, then set the proper default
	key = v.realKey(strings.ToLower(key))
	value = toCaseInsensitiveValue(value)

	path := strings.Split(key, v.keyDelim)
	lastKey := strings.ToLower(path[len(path)-1])
	deepestMap := deepSearch(v.defaults, path[0:len(path)-1])

	// set innermost value
	deepestMap[lastKey] = value
}

// Set sets the value for the key in the override register.
// Set is case-insensitive for a key.
// Will be used instead of values obtained via
// flags, config file, ENV, default, or key/value store.
func Set(key string, value any) { v.Set(key, value) }

// Set sets the value for the key in the override register.
// Set is case-insensitive for a key.
// Will be used instead of values obtained via
// flags, config file, ENV, default, or key/value store.
func (v *Viper) Set(key string, value any) {
	// If alias passed in, then set the proper override
	key = v.realKey(strings.ToLower(key))
	value = toCaseInsensitiveValue(value)

	path := strings.Split(key, v.keyDelim)
	lastKey := strings.ToLower(path[len(path)-1])
	deepestMap := deepSearch(v.override, path[0:len(path)-1])

	// set innermost value
	deepestMap[lastKey] = value
}

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func ReadInConfig() error { return v.ReadInConfig() }

// ReadInConfig will discover and load the configuration file from disk
// and key/value stores, searching in one of the defined paths.
func (v *Viper) ReadInConfig() error {
	v.logger.Info("attempting to read in config file")
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}

	if !slices.Contains(SupportedExts, v.getConfigType()) {
		return UnsupportedConfigError(v.getConfigType())
	}

	v.logger.Debug("reading file", "file", filename)

	file, err := afero.ReadFile(v.fs, filename)
	if errors.Is(err, fs.ErrNotExist) {
		// The specified config file is missing
		return FileNotFoundError{err: err, path: filename}
	} else if err != nil {
		// We hit some other error from the filesystem that isn't a missing file
		return err
	}

	config := make(map[string]any)

	err = v.unmarshalReader(bytes.NewReader(file), config)
	if err != nil {
		return err
	}

	v.config = config
	return nil
}

// MergeInConfig merges a new configuration with an existing config.
func MergeInConfig() error { return v.MergeInConfig() }

// MergeInConfig merges a new configuration with an existing config.
func (v *Viper) MergeInConfig() error {
	v.logger.Info("attempting to merge in config file")
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}

	if !slices.Contains(SupportedExts, v.getConfigType()) {
		return UnsupportedConfigError(v.getConfigType())
	}

	file, err := afero.ReadFile(v.fs, filename)
	if err != nil {
		return err
	}

	return v.MergeConfig(bytes.NewReader(file))
}

// ReadConfig will read a configuration file, setting existing keys to nil if the
// key does not exist in the file.
func ReadConfig(in io.Reader) error { return v.ReadConfig(in) }

// ReadConfig reads a configuration file, setting existing keys to nil if the
// key does not exist in the file.
func (v *Viper) ReadConfig(in io.Reader) error {
	config := make(map[string]any)

	err := v.unmarshalReader(in, config)
	if err != nil {
		return err
	}

	v.config = config

	return nil
}

// MergeConfig merges a new configuration with an existing config.
func MergeConfig(in io.Reader) error { return v.MergeConfig(in) }

// MergeConfig merges a new configuration with an existing config.
func (v *Viper) MergeConfig(in io.Reader) error {
	config := make(map[string]any)

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

	return v.MergeConfigMap(config)
}

// MergeConfigMap merges the configuration from the map given with an existing config.
// Note that the map given may be modified.
func MergeConfigMap(cfg map[string]any) error { return v.MergeConfigMap(cfg) }

// MergeConfigMap merges the configuration from the map given with an existing config.
// Note that the map given may be modified.
func (v *Viper) MergeConfigMap(cfg map[string]any) error {
	if v.config == nil {
		v.config = make(map[string]any)
	}
	insensitiviseMap(cfg)
	mergeMaps(cfg, v.config, nil)
	return nil
}

// WriteConfig writes the current configuration to a file.
func WriteConfig() error { return v.WriteConfig() }

// WriteConfig writes the current configuration to a file.
func (v *Viper) WriteConfig() error {
	filename, err := v.getConfigFile()
	if err != nil {
		return err
	}
	return v.writeConfig(filename, true)
}

// SafeWriteConfig writes current configuration to file only if the file does not exist.
func SafeWriteConfig() error { return v.SafeWriteConfig() }

// SafeWriteConfig writes current configuration to file only if the file does not exist.
func (v *Viper) SafeWriteConfig() error {
	if len(v.configPaths) < 1 {
		return errors.New("missing configuration for 'configPath'")
	}
	return v.SafeWriteConfigAs(filepath.Join(v.configPaths[0], v.configName+"."+v.configType))
}

// WriteConfigAs writes current configuration to a given filename.
func WriteConfigAs(filename string) error { return v.WriteConfigAs(filename) }

// WriteConfigAs writes current configuration to a given filename.
func (v *Viper) WriteConfigAs(filename string) error {
	return v.writeConfig(filename, true)
}

// WriteConfigTo writes current configuration to an [io.Writer].
func WriteConfigTo(w io.Writer) error { return v.WriteConfigTo(w) }

// WriteConfigTo writes current configuration to an [io.Writer].
func (v *Viper) WriteConfigTo(w io.Writer) error {
	format := strings.ToLower(v.getConfigType())

	if !slices.Contains(SupportedExts, format) {
		return UnsupportedConfigError(format)
	}

	return v.marshalWriter(w, format)
}

// SafeWriteConfigAs writes current configuration to a given filename if it does not exist.
func SafeWriteConfigAs(filename string) error { return v.SafeWriteConfigAs(filename) }

// SafeWriteConfigAs writes current configuration to a given filename if it does not exist.
func (v *Viper) SafeWriteConfigAs(filename string) error {
	alreadyExists, err := afero.Exists(v.fs, filename)
	if alreadyExists && err == nil {
		return ConfigFileAlreadyExistsError(filename)
	}
	return v.writeConfig(filename, false)
}

func (v *Viper) writeConfig(filename string, force bool) error {
	v.logger.Info("attempting to write configuration to file")

	var configType string

	ext := filepath.Ext(filename)
	if ext != "" && ext != filepath.Base(filename) {
		configType = ext[1:]
	} else {
		configType = v.configType
	}
	if configType == "" {
		return fmt.Errorf("config type could not be determined for %s", filename)
	}

	if !slices.Contains(SupportedExts, configType) {
		return UnsupportedConfigError(configType)
	}
	if v.config == nil {
		v.config = make(map[string]any)
	}
	flags := os.O_CREATE | os.O_TRUNC | os.O_WRONLY
	if !force {
		flags |= os.O_EXCL
	}
	f, err := v.fs.OpenFile(filename, flags, v.configPermissions)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := v.marshalWriter(f, configType); err != nil {
		return err
	}

	return f.Sync()
}

func (v *Viper) unmarshalReader(in io.Reader, c map[string]any) error {
	format := strings.ToLower(v.getConfigType())
	if format == "" {
		return errors.New("cannot decode configuration: unable to determine config type")
	}

	buf := new(bytes.Buffer)
	_, err := buf.ReadFrom(in)
	if err != nil {
		return fmt.Errorf("failed to read configuration from input: %w", err)
	}

	// TODO: remove this once SupportedExts is deprecated/removed
	if !slices.Contains(SupportedExts, format) {
		return UnsupportedConfigError(format)
	}

	// TODO: return [UnsupportedConfigError] if the registry does not contain the format
	// TODO: consider deprecating this error type
	decoder, err := v.decoderRegistry.Decoder(format)
	if err != nil {
		return ConfigParseError{err}
	}

	err = decoder.Decode(buf.Bytes(), c)
	if err != nil {
		return ConfigParseError{err}
	}

	insensitiviseMap(c)
	return nil
}

// Marshal a map into Writer.
func (v *Viper) marshalWriter(w io.Writer, configType string) error {
	c := v.AllSettings()

	encoder, err := v.encoderRegistry.Encoder(configType)
	if err != nil {
		return ConfigMarshalError{err}
	}

	b, err := encoder.Encode(c)
	if err != nil {
		return ConfigMarshalError{err}
	}

	_, err = w.Write(b)
	if err != nil {
		return ConfigMarshalError{err}
	}

	return nil
}

func keyExists(k string, m map[string]any) string {
	lk := strings.ToLower(k)
	for mk := range m {
		lmk := strings.ToLower(mk)
		if lmk == lk {
			return mk
		}
	}
	return ""
}

func castToMapStringInterface(
	src map[any]any,
) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[fmt.Sprintf("%v", k)] = v
	}
	return tgt
}

func castMapStringSliceToMapInterface(src map[string][]string) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

func castMapStringToMapInterface(src map[string]string) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

func castMapFlagToMapInterface(src map[string]FlagValue) map[string]any {
	tgt := map[string]any{}
	for k, v := range src {
		tgt[k] = v
	}
	return tgt
}

// mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's
// insistence on parsing nested structures as `map[any]any`
// instead of using a `string` as the key for nest structures beyond one level
// deep. Both map types are supported as there is a go-yaml fork that uses
// `map[string]any` instead.
func mergeMaps(src, tgt map[string]any, itgt map[any]any) {
	for sk, sv := range src {
		tk := keyExists(sk, tgt)
		if tk == "" {
			v.logger.Debug("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
			tgt[sk] = sv
			if itgt != nil {
				itgt[sk] = sv
			}
			continue
		}

		tv, ok := tgt[tk]
		if !ok {
			v.logger.Debug("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
			tgt[sk] = sv
			if itgt != nil {
				itgt[sk] = sv
			}
			continue
		}

		svType := reflect.TypeOf(sv)
		tvType := reflect.TypeOf(tv)

		v.logger.Debug(
			"processing",
			"key", sk,
			"st", svType,
			"tt", tvType,
			"sv", sv,
			"tv", tv,
		)

		switch ttv := tv.(type) {
		case map[any]any:
			v.logger.Debug("merging maps (must convert)")
			tsv, ok := sv.(map[any]any)
			if !ok {
				v.logger.Error(
					"Could not cast sv to map[any]any",
					"key", sk,
					"st", svType,
					"tt", tvType,
					"sv", sv,
					"tv", tv,
				)
				continue
			}

			ssv := castToMapStringInterface(tsv)
			stv := castToMapStringInterface(ttv)
			mergeMaps(ssv, stv, ttv)
		case map[string]any:
			v.logger.Debug("merging maps")
			tsv, ok := sv.(map[string]any)
			if !ok {
				v.logger.Error(
					"Could not cast sv to map[string]any",
					"key", sk,
					"st", svType,
					"tt", tvType,
					"sv", sv,
					"tv", tv,
				)
				continue
			}
			mergeMaps(tsv, ttv, nil)
		default:
			v.logger.Debug("setting value")
			tgt[tk] = sv
			if itgt != nil {
				itgt[tk] = sv
			}
		}
	}
}

// AllKeys returns all keys holding a value, regardless of where they are set.
// Nested keys are returned with a v.keyDelim separator.
func AllKeys() []string { return v.AllKeys() }

// AllKeys returns all keys holding a value, regardless of where they are set.
// Nested keys are returned with a v.keyDelim separator.
func (v *Viper) AllKeys() []string {
	m := map[string]bool{}
	// add all paths, by order of descending priority to ensure correct shadowing
	m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "")
	m = v.flattenAndMergeMap(m, v.override, "")
	m = v.mergeFlatMap(m, castMapFlagToMapInterface(v.pflags))
	m = v.mergeFlatMap(m, castMapStringSliceToMapInterface(v.env))
	m = v.flattenAndMergeMap(m, v.config, "")
	m = v.flattenAndMergeMap(m, v.kvstore, "")
	m = v.flattenAndMergeMap(m, v.defaults, "")

	// convert set of paths to list
	a := make([]string, 0, len(m))
	for x := range m {
		a = append(a, x)
	}
	return a
}

// flattenAndMergeMap recursively flattens the given map into a map[string]bool
// of key paths (used as a set, easier to manipulate than a []string):
//   - each path is merged into a single key string, delimited with v.keyDelim
//   - if a path is shadowed by an earlier value in the initial shadow map,
//     it is skipped.
//
// The resulting set of paths is merged to the given shadow set at the same time.
func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]any, prefix string) map[string]bool {
	if shadow != nil && prefix != "" && shadow[prefix] {
		// prefix is shadowed => nothing more to flatten
		return shadow
	}
	if shadow == nil {
		shadow = make(map[string]bool)
	}

	var m2 map[string]any
	if prefix != "" {
		prefix += v.keyDelim
	}
	for k, val := range m {
		fullKey := prefix + k
		switch val := val.(type) {
		case map[string]any:
			m2 = val
		case map[any]any:
			m2 = cast.ToStringMap(val)
		default:
			// immediate value
			shadow[strings.ToLower(fullKey)] = true
			continue
		}
		// recursively merge to shadow map
		shadow = v.flattenAndMergeMap(shadow, m2, fullKey)
	}
	return shadow
}

// mergeFlatMap merges the given maps, excluding values of the second map
// shadowed by values from the first map.
func (v *Viper) mergeFlatMap(shadow map[string]bool, m map[string]any) map[string]bool {
	// scan keys
outer:
	for k := range m {
		path := strings.Split(k, v.keyDelim)
		// scan intermediate paths
		var parentKey string
		for i := 1; i < len(path); i++ {
			parentKey = strings.Join(path[0:i], v.keyDelim)
			if shadow[parentKey] {
				// path is shadowed, continue
				continue outer
			}
		}
		// add key
		shadow[strings.ToLower(k)] = true
	}
	return shadow
}

// AllSettings merges all settings and returns them as a map[string]any.
func AllSettings() map[string]any { return v.AllSettings() }

// AllSettings merges all settings and returns them as a map[string]any.
func (v *Viper) AllSettings() map[string]any {
	return v.getSettings(v.AllKeys())
}

func (v *Viper) getSettings(keys []string) map[string]any {
	m := map[string]any{}
	// start from the list of keys, and construct the map one value at a time
	for _, k := range keys {
		value := v.Get(k)
		if value == nil {
			// should not happen, since AllKeys() returns only keys holding a value,
			// check just in case anything changes
			continue
		}
		path := strings.Split(k, v.keyDelim)
		lastKey := strings.ToLower(path[len(path)-1])
		deepestMap := deepSearch(m, path[0:len(path)-1])
		// set innermost value
		deepestMap[lastKey] = value
	}
	return m
}

// SetFs sets the filesystem to use to read configuration.
func SetFs(fs afero.Fs) { v.SetFs(fs) }

// SetFs sets the filesystem to use to read configuration.
func (v *Viper) SetFs(fs afero.Fs) {
	v.fs = fs
}

// SetConfigName sets name for the config file.
// Does not include extension.
func SetConfigName(in string) { v.SetConfigName(in) }

// SetConfigName sets the name for the config file.
// Does not include extension.
func (v *Viper) SetConfigName(in string) {
	if v.finder != nil {
		v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "SetConfigName"))
	}

	if in != "" {
		v.configName = in
		v.configFile = ""
	}
}

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func SetConfigType(in string) { v.SetConfigType(in) }

// SetConfigType sets the type of the configuration returned by the
// remote source, e.g. "json".
func (v *Viper) SetConfigType(in string) {
	if in != "" {
		v.configType = in
	}
}

// SetConfigPermissions sets the permissions for the config file.
func SetConfigPermissions(perm os.FileMode) { v.SetConfigPermissions(perm) }

// SetConfigPermissions sets the permissions for the config file.
func (v *Viper) SetConfigPermissions(perm os.FileMode) {
	v.configPermissions = perm.Perm()
}

func (v *Viper) getConfigType() string {
	if v.configType != "" {
		return v.configType
	}

	cf, err := v.getConfigFile()
	if err != nil {
		return ""
	}

	ext := filepath.Ext(cf)

	if len(ext) > 1 {
		return ext[1:]
	}

	return ""
}

func (v *Viper) getConfigFile() (string, error) {
	if v.configFile == "" {
		cf, err := v.findConfigFile()
		if err != nil {
			return "", err
		}
		v.configFile = cf
	}
	return v.configFile, nil
}

// Debug prints all configuration registries for debugging
// purposes.
func Debug() { v.Debug() }

// DebugTo writes all configuration registries to the given [io.Writer].
func DebugTo(w io.Writer) { v.DebugTo(w) }

// Debug prints all configuration registries for debugging purposes.
func (v *Viper) Debug() { v.DebugTo(os.Stdout) }

// DebugTo writes all configurati
[truncated at 64000 of 64403 bytes]
[evidence record sha256:86fd559aa1a7e6b28c31c808d014d9d6c67b6f90934950214952b9631a0d11d8 kind tool-call:read]
tool read <- {"path":"finder_test.go"}
tool read ok: package viper

import (
	"testing"

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

type finderStub struct {
	results []string
}

func (f finderStub) Find(_ afero.Fs) ([]string, error) {
	return f.results, nil
}

func TestFinders(t *testing.T) {
	finder := Finders(
		finderStub{
			results: []string{
				"/home/user/.viper.yaml",
			},
		},
		finderStub{
			results: []string{
				"/etc/viper/config.yaml",
			},
		},
	)

	results, err := finder.Find(afero.NewMemMapFs())
	require.NoError(t, err)

	expected := []string{
		"/home/user/.viper.yaml",
		"/etc/viper/config.yaml",
	}

	assert.Equal(t, expected, results)
}

[evidence record sha256:1d0ee30896acf39f454ed238e7d618962e61e51f7ab5583bef0b234f94a74de5 kind tool-call:read]
step 4: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"grep -n \"WithFinder\\|withFinder\" /work/finder_test.go"}
tool shell failed: denied: "grep -n "WithFinder\|withFinder" /work/finder_test.go" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:09719556c4e035f8ecfaf1b91db856fdd7c7f7a8ee3d98c766619f8a42031f59 kind tool-call:shell]
step 5: calling local:qwen3.6:35b-a3b
tool declare_file_set <- {"files":["finder.go"]}
tool declare_file_set ok: declared 1 file(s): finder.go
[evidence record sha256:0b5d7c37f6ed92e9e1905aaf092747753c874ba61aec9efd19763e2e6459e663 kind tool-call:declare_file_set]
step 6: calling local:qwen3.6:35b-a3b
tool edit <- {"path":"finder.go","find":"func WithFinder(f Finder) Option {\n\treturn optionFunc(func(v *Viper) {\n\t\tif f != nil {\n\t\t\treturn\n\t\t}\n\n\t\tv.finder = f\n\t})\n}","replace":"func WithFinder(f Finder) Option {\n\treturn optionFunc(func(v *Viper) {\n\t\tif f == nil {\n\t\t\treturn\n\t\t}\n\n\t\tv.finder = f\n\t})\n}"}
tool edit ok: replaced 1 occurrence(s) in finder.go
[evidence record sha256:3c46b5fd2395e0628d6c05b8af3c715c2c8c2fe6ffc99f56c997e7fb290afad3 kind tool-call:edit]
step 7: calling local:qwen3.6:35b-a3b
tool read <- {"path":".golangci.yaml"}
tool read ok: version: "2"

run:
  timeout: 5m

linters:
  enable:
    - bodyclose
    - dogsled
    - dupl
    - durationcheck
    - exhaustive
    - gocritic
    - godot
    - gomoddirectives
    - goprintffuncname
    - govet
    - importas
    - ineffassign
    - makezero
    - misspell
    - nakedret
    - nilerr
    - noctx
    - nolintlint
    - prealloc
    - predeclared
    - revive
    - rowserrcheck
    - sqlclosecheck
    - staticcheck
    - tparallel
    - unconvert
    - unparam
    - unused
    - wastedassign
    - whitespace

    # fixme
    # - cyclop
    # - errcheck
    # - errorlint
    # - exhaustivestruct
    # - forbidigo
    # - forcetypeassert
    # - gochecknoglobals
    # - gochecknoinits
    # - gocognit
    # - goconst
    # - gocyclo
    # - gosec
    # - gosimple
    # - ifshort
    # - lll
    # - nlreturn
    # - paralleltest
    # - scopelint
    # - thelper
    # - wrapcheck

    # unused
    # - depguard
    # - goheader
    # - gomodguard

    # don't enable:
    # - asciicheck
    # - funlen
    # - godox
    # - goerr113
    # - gomnd
    # - interfacer
    # - maligned
    # - nestif
    # - testpackage
    # - wsl

  exclusions:
    rules:
      - linters:
        - errcheck
        - noctx
        path: _test.go
    presets:
      - std-error-handling

  settings:
    revive:
      rules:
        - name: blank-imports
        - name: context-as-argument
          arguments:
            - allowTypesBefore: "*testing.T"
        - name: context-keys-type
        - name: dot-imports
        - name: empty-block
        - name: error-naming
        - name: error-return
        - name: error-strings
        - name: errorf
        - name: exported
          arguments:
            - "sayRepetitiveInsteadOfStutters"  # make error messages clearer
        - name: increment-decrement
        - name: indent-error-flow
        - name: package-comments
          disabled: true # disable package comments rule for now
        - name: range
        - name: redefines-builtin-id
        - name: superfluous-else
        - name: time-naming
        - name: unexported-return
        - name: unreachable-code
        - name: unused-parameter
        - name: var-declaration
        - name: var-naming

        # TODO: add more revive rules

    misspell:
      locale: US
    nolintlint:
      allow-unused: false # report any unused nolint directives
      require-specific: false # don't require nolint directives to be specific about which linter is being skipped
    gocritic:
      # Enable multiple checks by tags. See "Tags" section in https://github.com/go-critic/go-critic#usage.
      enabled-tags:
        - diagnostic
        - experimental
        - opinionated
        - style
      disabled-checks:
        - importShadow
        - unnamedResult

formatters:
  enable:
    - gci
    - gofmt
    - gofumpt
    - goimports
    # - golines

  settings:
    gci:
      sections:
        - standard
        - default
        - localmodule

[evidence record sha256:657298efc4971996a6b99de142d321309048db95df4931ed42699f5f812b809c kind tool-call:read]
step 8: calling local:qwen3.6:35b-a3b
tool read <- {"path":"Makefile"}
tool read ok: # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html

OS = $(shell uname | tr A-Z a-z)
export PATH := $(abspath bin/):${PATH}

# Build variables
BUILD_DIR ?= build
export CGO_ENABLED ?= 0
export GOOS = $(shell go env GOOS)
ifeq (${VERBOSE}, 1)
ifeq ($(filter -v,${GOARGS}),)
	GOARGS += -v
endif
TEST_FORMAT = short-verbose
endif

# Dependency versions
GOTESTSUM_VERSION = 1.9.0
GOLANGCI_VERSION = 1.53.3

# Add the ability to override some variables
# Use with care
-include override.mk

.PHONY: clear
clear: ## Clear the working area and the project
	rm -rf bin/

.PHONY: check
check: test lint ## Run tests and linters


TEST_PKGS ?= ./...
.PHONY: test
test: TEST_FORMAT ?= short
test: SHELL = /bin/bash
test: export CGO_ENABLED=1
test: bin/gotestsum ## Run tests
	@mkdir -p ${BUILD_DIR}
	bin/gotestsum --no-summary=skipped --junitfile ${BUILD_DIR}/coverage.xml --format ${TEST_FORMAT} -- -race -coverprofile=${BUILD_DIR}/coverage.txt -covermode=atomic $(filter-out -v,${GOARGS}) $(if ${TEST_PKGS},${TEST_PKGS},./...)

.PHONY: lint
lint: lint-go lint-yaml
lint: ## Run linters

.PHONY: lint-go
lint-go:
	golangci-lint run $(if ${CI},--out-format github-actions,)

.PHONY: lint-yaml
lint-yaml:
	yamllint $(if ${CI},-f github,) --no-warnings .

.PHONY: fmt
fmt: ## Format code
	golangci-lint run --fix

deps: bin/golangci-lint bin/gotestsum yamllint
deps: ## Install dependencies

bin/gotestsum:
	@mkdir -p bin
	curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_${OS}_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum && chmod +x ./bin/gotestsum

bin/golangci-lint:
	@mkdir -p bin
	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- v${GOLANGCI_VERSION}

.PHONY: yamllint
yamllint:
	pip3 install --user yamllint

# Add custom targets here
-include custom.mk

.PHONY: list
list: ## List all make targets
	@${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort

.PHONY: help
.DEFAULT_GOAL := help
help:
	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

# Variable outputting/exporting rules
var-%: ; @echo $($*)
varexport-%: ; @echo $*=$($*)

[evidence record sha256:fe063cc121969c732945f00484b6465047e0476d76761e84acd9080f604e5349 kind tool-call:read]
step 9: calling local:qwen3.6:35b-a3b
tool read <- {"path":".gitignore"}
tool read ok: /.devenv/
/.direnv/
/.idea/
/.pre-commit-config.yaml
/bin/
/build/
/var/
/vendor/

[evidence record sha256:2c8a97d0527dd2a8aa639c5c8a7598fa05e067d82cc1fe158c63704791c0cf42 kind tool-call:read]
step 10: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"ls .campaign/ 2>/dev/null | head -20"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:ab85959029b50b28ac8f001aac45ff2593168d465ca0ab362c974fcca87cf69d kind tool-call:shell]
step 11: calling local:qwen3.6:35b-a3b
tool read <- {"path":"finder_example_test.go"}
tool read ok: package viper_test

import (
	"fmt"

	"github.com/sagikazarmark/locafero"
	"github.com/spf13/afero"

	"github.com/spf13/viper"
)

func ExampleFinder() {
	fs := afero.NewMemMapFs()

	fs.Mkdir("/home/user", 0o777)

	f, _ := fs.Create("/home/user/myapp.yaml")
	f.WriteString("foo: bar")
	f.Close()

	// HCL will have a "lower" priority in the search order
	fs.Create("/home/user/myapp.hcl")

	finder := locafero.Finder{
		Paths: []string{"/home/user"},
		Names: locafero.NameWithExtensions("myapp", viper.SupportedExts...),
		Type:  locafero.FileTypeFile, // This is important!
	}

	v := viper.NewWithOptions(viper.WithFinder(finder))
	v.SetFs(fs)
	v.ReadInConfig()

	fmt.Println(v.GetString("foo"))

	// Output:
	// bar
}

func ExampleFinders() {
	fs := afero.NewMemMapFs()

	fs.Mkdir("/home/user", 0o777)
	f, _ := fs.Create("/home/user/myapp.yaml")
	f.WriteString("foo: bar")
	f.Close()

	fs.Mkdir("/etc/myapp", 0o777)
	fs.Create("/etc/myapp/config.yaml")

	// Combine multiple finders to search for files in multiple locations with different criteria
	finder := viper.Finders(
		locafero.Finder{
			Paths: []string{"/home/user"},
			Names: locafero.NameWithExtensions("myapp", viper.SupportedExts...),
			Type:  locafero.FileTypeFile, // This is important!
		},
		locafero.Finder{
			Paths: []string{"/etc/myapp"},
			Names: []string{"config.yaml"}, // Only accept YAML files in the system config directory
			Type:  locafero.FileTypeFile,   // This is important!
		},
	)

	v := viper.NewWithOptions(viper.WithFinder(finder))
	v.SetFs(fs)
	v.ReadInConfig()

	fmt.Println(v.GetString("foo"))

	// Output:
	// bar
}

[evidence record sha256:ba8965a1b79c845018bdb713de0b45b5686882e28a5cac739e90f0e40c6635db kind tool-call:read]
step 12: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cd /work && gofmt -l ./*.go 2>/dev/null"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:91721b161dc2a4228c89d83a866961b144234358090ef71654faea726ac1fae8 kind tool-call:shell]
step 13: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cd /work && gofmt -l ./viper.go ./finder.go ./encoding.go > /tmp/fmtout 2>&1 && echo \"Format check passed\" || (cat /tmp/fmtout && exit 1)"}
tool shell failed: denied: "cd /work && gofmt -l ./viper.go ./finder.go ./encoding.go > /tmp/fmtout 2>&1 && echo "Format check passed" || (cat /tmp/fmtout && exit 1)" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:d31f828fda03d54512dfcb7f722caf587456f318530862ef61e916e3589c29a4 kind tool-call:shell]
step 14: calling local:qwen3.6:35b-a3b
tool read <- {"path":".campaign/gomod/github.com/davecgh/go-spew@v1.1.1/spew/bypass.go"}
tool read ok: // Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line.  The "disableunsafe"
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4

package spew

import (
	"reflect"
	"unsafe"
)

const (
	// UnsafeDisabled is a build-time constant which specifies whether or
	// not access to the unsafe package is available.
	UnsafeDisabled = false

	// ptrSize is the size of a pointer on the current arch.
	ptrSize = unsafe.Sizeof((*byte)(nil))
)

type flag uintptr

var (
	// flagRO indicates whether the value field of a reflect.Value
	// is read-only.
	flagRO flag

	// flagAddr indicates whether the address of the reflect.Value's
	// value may be taken.
	flagAddr flag
)

// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)

// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
	ro, addr flag
}{{
	// From Go 1.4 to 1.5
	ro:   1 << 5,
	addr: 1 << 7,
}, {
	// Up to Go tip.
	ro:   1<<5 | 1<<6,
	addr: 1 << 8,
}}

var flagValOffset = func() uintptr {
	field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
	if !ok {
		panic("reflect.Value has no flag field")
	}
	return field.Offset
}()

// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
	return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}

// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data.  It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) reflect.Value {
	if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
		return v
	}
	flagFieldPtr := flagField(&v)
	*flagFieldPtr &^= flagRO
	*flagFieldPtr |= flagAddr
	return v
}

// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
	field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
	if !ok {
		panic("reflect.Value has no flag field")
	}
	if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
		panic("reflect.Value flag field has changed kind")
	}
	type t0 int
	var t struct {
		A t0
		// t0 will have flagEmbedRO set.
		t0
		// a will have flagStickyRO set
		a t0
	}
	vA := reflect.ValueOf(t).FieldByName("A")
	va := reflect.ValueOf(t).FieldByName("a")
	vt0 := reflect.ValueOf(t).FieldByName("t0")

	// Infer flagRO from the difference between the flags
	// for the (otherwise identical) fields in t.
	flagPublic := *flagField(&vA)
	flagWithRO := *flagField(&va) | *flagField(&vt0)
	flagRO = flagPublic ^ flagWithRO

	// Infer flagAddr from the difference between a value
	// taken from a pointer and not.
	vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
	flagNoPtr := *flagField(&vA)
	flagPtr := *flagField(&vPtrA)
	flagAddr = flagNoPtr ^ flagPtr

	// Check that the inferred flags tally with one of the known versions.
	for _, f := range okFlags {
		if flagRO == f.ro && flagAddr == f.addr {
			return
		}
	}
	panic("reflect.Value read-only flag has changed semantics")
}

[evidence record sha256:7a04e786a15742cba39f84367807b0e4a122f5440b402eb1560c0d22fa3b1b61 kind tool-call:read]
step 15: calling local:qwen3.6:35b-a3b
tool read <- {"path":"viper_test.go","maxBytes":400}
tool read ok: // 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.c
[truncated at 400 of 66541 bytes]
[evidence record sha256:e5153ede6a7587facc52e85b7e38e60d04399e270063d06d0a4db5e4a9bbae81 kind tool-call:read]
step 16: calling local:qwen3.6:35b-a3b
stopped: empty-response after 16 steps, 374073 tokens
gate typecheck passed: the command exited 0 [evidence record sha256:fbe80d2b5fb677ff644f5db6a6a63dc38936c175390869d048ed04e1dc45d1ef]
gate lint passed: the command exited 0 [evidence record sha256:5065969302214ba6425d41a5a0027699b8955f9bb08c75d21ef606b8e52d1ebd]
gate format failed: the command listed 29 offending file(s) [evidence record sha256:7a16d7c4059b764c4cf1659765bc7c0902e8d7762e0640476d11b4758bfb8a0b]
gate tests passed: the command exited 0 [evidence record sha256:d7c629d4333c8772b17c6e4dd4bb78cf1400b9dc6a4ee8ad217dcce2c21244fb]
gate file-set failed: 1 file(s) outside the declared set: workplan.txt. 1 file(s) were edited before anything declared them: workplan.txt. A declaration written after the edit describes what was done, not what was intended. Record an amendment to widen the set, which puts the widening in front of a reviewer. [evidence record sha256:675bab62d76d1848e0679ff5a019c446ba81dd4c0b2f95fd8bb74c2b755a54d3]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:80a69ddf538b2edff3530ed1afc787650dc7d826a04eded035e165077cbe6257]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:48401cbfcc12987dfae6c002c84fc54a4e94fc5d6e443f68ed699bb2fc400bbf]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:d10ec5b4c8a1d40b28d094707e71408003a41b576e152d86d5eeb98612fa9caa]
gate diff-budget passed (advisory): within budget: 2 file(s) and 16 added line(s) [evidence record sha256:8e93cf571ec429f612a0f5f6176d5dc01607f47c82f02c737aedbfb790f4ea05]
ratchet accepted attempt 2: the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage) [evidence record sha256:4b349eb50923a1bf1b5925b29b97a69e38873b82e3f382519855cee6d1ea11a1]
escalated after 2 attempt(s) at gate format: the command listed 29 offending file(s)

gates:
  passed   typecheck: the command exited 0
  passed   lint: the command exited 0
  failed   format: the command listed 29 offending file(s)
  passed   tests: the command exited 0
  failed   file-set: 1 file(s) outside the declared set: workplan.txt. 1 file(s) were edited before anything declared them: workplan.txt. A declaration written after the edit describes what was done, not what was intended. Record an amendment to widen the set, which puts the widening in front of a reviewer.
  passed   placeholder: no placeholder marker was introduced by this change
  passed   secret-scan: no known credential pattern appears in the added lines
  passed   behaviour-probe: 0 changed function(s) still answer to their inputs.
  passed   diff-budget (advisory): within budget: 2 file(s) and 16 added line(s)
attempt 1: accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
attempt 2: accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)

Escalating after 2 of 2 attempts.

Gate: format (format (gofmt -l))
Why: the command listed 29 offending file(s)
Its last run is ledger record sha256:7a16d7c4059b764c4cf1659765bc7c0902e8d7762e0640476d11b4758bfb8a0b.

Attempts:
  1. accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
     still failing: format, tests, file-set
  2. accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
     still failing: format, file-set

routing reward: 0.000 (the run escalated, so the gates never went green)
[signing] the Secret Service keyring would not take a new key (secret-tool store failed: ), so the bundle is signed with a per-run key

evidence bundle: /out/bundle
verify it anywhere: node /out/bundle/verify.mjs /out/bundle
review it: open /out/bundle/review.html
what this run produced

  the page a person reads: /out/bundle/review.html
  the bundle a stranger verifies: /out/bundle
  its own verifier, needing nothing installed: node /out/bundle/verify.mjs /out/bundle
  the chain every record is on: /out/bundle/ledger.jsonl

  193 records. The harness verified 1 claim(s) and refused 0.
  bundle verified in this run: verify.mjs exited 0
[chokepoint] refusing shell without a terminal to confirm on: "go test ./... 2>&1 | head -200" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "make test TEST_PKGS=./... 2>&1 | tail -300" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go version" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go test ./... -v 2>&1 -count=1" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "echo hello" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "git log --oneline -20 2>&1 || echo "git not available"" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go test ./... 2>&1" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go test ./... 2>&1 | head -100" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "grep -n "WithFinder\|withFinder" /work/finder_test.go" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "cd /work && gofmt -l ./viper.go ./finder.go ./encoding.go > /tmp/fmtout 2>&1 && echo "Format check passed" || (cat /tmp/fmtout && exit 1)" is not on the shell allowlist.
