// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package confmap // import "go.opentelemetry.io/collector/confmap"

import (
	"errors"
	"testing"

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

type configChildStruct struct {
	Child    errValidateConfig
	ChildPtr *errValidateConfig
}

type configChildSlice struct {
	Child    []errValidateConfig
	ChildPtr []*errValidateConfig
}

type configChildMapValue struct {
	Child    map[string]errValidateConfig
	ChildPtr map[string]*errValidateConfig
}

type configChildMapKey struct {
	Child    map[errType]string
	ChildPtr map[*errType]string
}

type configChildTypeDef struct {
	Child    errType
	ChildPtr *errType
}

type config any

type configChildInterface struct {
	Child config
}

type errValidateConfig struct {
	err error
}

func (e *errValidateConfig) Validate() error {
	return e.err
}

type errType string

func (e errType) Validate() error {
	if e == "" {
		return nil
	}
	return errors.New(string(e))
}

func newErrType(etStr string) *errType {
	et := errType(etStr)
	return &et
}

type errMapType map[string]string

func (e errMapType) Validate() error {
	return errors.New(e["err"])
}

type structKey struct {
	k string
	e error
}

func (s structKey) String() string {
	return s.k
}

func (s structKey) Validate() error {
	return s.e
}

type configChildMapCustomKey struct {
	Child map[structKey]errValidateConfig
}

func newErrMapType() *errMapType {
	et := errMapType(nil)
	return &et
}

type configMapstructure struct {
	Valid  *errValidateConfig `mapstructure:"validtag,omitempty"`
	NoData *errValidateConfig `mapstructure:""`
	NoName *errValidateConfig `mapstructure:",remain"`
}

type configDeeplyNested struct {
	MapKeyChild   map[configChildStruct]string
	MapValueChild map[string]configChildStruct
	SliceChild    []configChildSlice
	MapIntKey     map[int]errValidateConfig
	MapFloatKey   map[float64]errValidateConfig
}

type sliceTypeAlias []configChildSlice

func (sliceTypeAlias) Validate() error {
	return errors.New("sliceTypeAlias error")
}

type configWithArray struct {
	Child [2]errValidateConfig
}

type configWithNilPtr struct {
	Child *errValidateConfig
}

type configWithUnexported struct {
	unexported errValidateConfig
}

type configWithEmbedded struct {
	errValidateConfig
}

func TestValidateConfig(t *testing.T) {
	tests := []struct {
		name     string
		cfg      any
		expected error
	}{
		{
			name:     "struct",
			cfg:      errValidateConfig{err: errors.New("struct")},
			expected: errors.New("struct"),
		},
		{
			name:     "pointer struct",
			cfg:      &errValidateConfig{err: errors.New("pointer struct")},
			expected: errors.New("pointer struct"),
		},
		{
			name:     "type",
			cfg:      errType("type"),
			expected: errors.New("type"),
		},
		{
			name:     "pointer child",
			cfg:      newErrType("pointer type"),
			expected: errors.New("pointer type"),
		},
		{
			name:     "child interface with nil",
			cfg:      configChildInterface{},
			expected: nil,
		},
		{
			name:     "pointer to child interface with nil",
			cfg:      &configChildInterface{},
			expected: nil,
		},
		{
			name:     "nil",
			cfg:      nil,
			expected: nil,
		},
		{
			name:     "nil map type",
			cfg:      errMapType(nil),
			expected: errors.New(""),
		},
		{
			name:     "nil pointer map type",
			cfg:      newErrMapType(),
			expected: errors.New(""),
		},
		{
			name:     "child struct",
			cfg:      configChildStruct{Child: errValidateConfig{err: errors.New("child struct")}},
			expected: errors.New("child: child struct"),
		},
		{
			name:     "pointer child struct",
			cfg:      &configChildStruct{Child: errValidateConfig{err: errors.New("pointer child struct")}},
			expected: errors.New("child: pointer child struct"),
		},
		{
			name:     "child struct pointer",
			cfg:      &configChildStruct{ChildPtr: &errValidateConfig{err: errors.New("child struct pointer")}},
			expected: errors.New("childptr: child struct pointer"),
		},
		{
			name:     "child interface",
			cfg:      configChildInterface{Child: errValidateConfig{err: errors.New("child interface")}},
			expected: errors.New("child: child interface"),
		},
		{
			name:     "pointer to child interface",
			cfg:      &configChildInterface{Child: errValidateConfig{err: errors.New("pointer to child interface")}},
			expected: errors.New("child: pointer to child interface"),
		},
		{
			name:     "child interface with pointer",
			cfg:      configChildInterface{Child: &errValidateConfig{err: errors.New("child interface with pointer")}},
			expected: errors.New("child: child interface with pointer"),
		},
		{
			name:     "pointer to child interface with pointer",
			cfg:      &configChildInterface{Child: &errValidateConfig{err: errors.New("pointer to child interface with pointer")}},
			expected: errors.New("child: pointer to child interface with pointer"),
		},
		{
			name:     "child slice",
			cfg:      configChildSlice{Child: []errValidateConfig{{}, {err: errors.New("child slice")}}},
			expected: errors.New("child::1: child slice"),
		},
		{
			name:     "pointer child slice",
			cfg:      &configChildSlice{Child: []errValidateConfig{{}, {err: errors.New("pointer child slice")}}},
			expected: errors.New("child::1: pointer child slice"),
		},
		{
			name:     "child slice pointer",
			cfg:      &configChildSlice{ChildPtr: []*errValidateConfig{{}, {err: errors.New("child slice pointer")}}},
			expected: errors.New("childptr::1: child slice pointer"),
		},
		{
			name:     "child map value",
			cfg:      configChildMapValue{Child: map[string]errValidateConfig{"test": {err: errors.New("child map")}}},
			expected: errors.New("child::test: child map"),
		},
		{
			name:     "pointer child map value",
			cfg:      &configChildMapValue{Child: map[string]errValidateConfig{"test": {err: errors.New("pointer child map")}}},
			expected: errors.New("child::test: pointer child map"),
		},
		{
			name:     "child map value pointer",
			cfg:      &configChildMapValue{ChildPtr: map[string]*errValidateConfig{"test": {err: errors.New("child map pointer")}}},
			expected: errors.New("childptr::test: child map pointer"),
		},
		{
			name:     "child map key",
			cfg:      configChildMapKey{Child: map[errType]string{"child_map_key": ""}},
			expected: errors.New("child::child_map_key: child_map_key"),
		},
		{
			name:     "pointer child map key",
			cfg:      &configChildMapKey{Child: map[errType]string{"pointer_child_map_key": ""}},
			expected: errors.New("child::pointer_child_map_key: pointer_child_map_key"),
		},
		{
			name:     "child map key pointer",
			cfg:      &configChildMapKey{ChildPtr: map[*errType]string{newErrType("child map key pointer"): ""}},
			expected: errors.New("childptr::[*confmap.errType key]: child map key pointer"),
		},
		{
			name:     "map with stringified non-string key type",
			cfg:      &configChildMapCustomKey{Child: map[structKey]errValidateConfig{{k: "struct_key", e: errors.New("custom key error")}: {err: errors.New("value error")}}},
			expected: errors.New("child::struct_key: custom key error\nchild::struct_key: value error"),
		},
		{
			name:     "child type",
			cfg:      configChildTypeDef{Child: "child type"},
			expected: errors.New("child: child type"),
		},
		{
			name:     "pointer child type",
			cfg:      &configChildTypeDef{Child: "pointer child type"},
			expected: errors.New("child: pointer child type"),
		},
		{
			name:     "child type pointer",
			cfg:      &configChildTypeDef{ChildPtr: newErrType("child type pointer")},
			expected: errors.New("childptr: child type pointer"),
		},
		{
			name:     "valid mapstructure tag",
			cfg:      configMapstructure{Valid: &errValidateConfig{errors.New("test")}},
			expected: errors.New("validtag: test"),
		},
		{
			name:     "zero-length mapstructure tag",
			cfg:      configMapstructure{NoData: &errValidateConfig{errors.New("test")}},
			expected: errors.New("nodata: test"),
		},
		{
			name:     "no field name in mapstructure tag",
			cfg:      configMapstructure{NoName: &errValidateConfig{errors.New("test")}},
			expected: errors.New("noname: test"),
		},
		{
			name:     "nested map key error",
			cfg:      configDeeplyNested{MapKeyChild: map[configChildStruct]string{{Child: errValidateConfig{err: errors.New("child key error")}}: "val"}},
			expected: errors.New("mapkeychild::[confmap.configChildStruct key]::child: child key error"),
		},
		{
			name:     "nested map value error",
			cfg:      configDeeplyNested{MapValueChild: map[string]configChildStruct{"key": {Child: errValidateConfig{err: errors.New("child key error")}}}},
			expected: errors.New("mapvaluechild::key::child: child key error"),
		},
		{
			name:     "nested slice value error",
			cfg:      configDeeplyNested{SliceChild: []configChildSlice{{Child: []errValidateConfig{{err: errors.New("child key error")}}}}},
			expected: errors.New("slicechild::0::child::0: child key error"),
		},
		{
			name:     "nested map with int key",
			cfg:      configDeeplyNested{MapIntKey: map[int]errValidateConfig{1: {err: errors.New("int key error")}}},
			expected: errors.New("mapintkey::1: int key error"),
		},
		{
			name:     "nested map with float key",
			cfg:      configDeeplyNested{MapFloatKey: map[float64]errValidateConfig{1.2: {err: errors.New("float key error")}}},
			expected: errors.New("mapfloatkey::1.2: float key error"),
		},
		{
			name:     "slice type alias",
			cfg:      sliceTypeAlias{},
			expected: errors.New("sliceTypeAlias error"),
		},
		{
			name:     "fixed-size array element error",
			cfg:      configWithArray{Child: [2]errValidateConfig{{}, {err: errors.New("array error")}}},
			expected: errors.New("child::1: array error"),
		},
		{
			name:     "nil pointer field",
			cfg:      configWithNilPtr{},
			expected: nil,
		},
		{
			name:     "unexported field skipped",
			cfg:      configWithUnexported{unexported: errValidateConfig{err: errors.New("should be skipped")}},
			expected: nil,
		},
		{
			// errValidateConfig is an unexported type, so the embedded field is
			// skipped by the IsExported() check in field traversal. The error is
			// still surfaced because *configWithEmbedded implements Validator via
			// the promoted method, so it is caught at the struct level with no
			// path prefix.
			name:     "embedded unexported struct field error",
			cfg:      configWithEmbedded{errValidateConfig: errValidateConfig{err: errors.New("embedded error")}},
			expected: errors.New("embedded error"),
		},
		{
			name: "multiple fields failing",
			cfg: configChildStruct{
				Child:    errValidateConfig{err: errors.New("child error")},
				ChildPtr: &errValidateConfig{err: errors.New("childptr error")},
			},
			expected: errors.New("child: child error\nchildptr: childptr error"),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := Validate(tt.cfg)

			if tt.expected != nil {
				assert.EqualError(t, err, tt.expected.Error())
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestPathErrorIsSingle(t *testing.T) {
	sentinel := errors.New("sentinel error")
	pe := pathError{err: sentinel, path: []string{"field"}}
	require.ErrorIs(t, pe, sentinel)
}

func TestValidateErrorIsMultiple(t *testing.T) {
	err1 := errors.New("first error")
	err2 := errors.New("second error")
	cfg := configChildStruct{
		Child:    errValidateConfig{err: err1},
		ChildPtr: &errValidateConfig{err: err2},
	}
	err := Validate(cfg)
	require.ErrorIs(t, err, err1)
	require.ErrorIs(t, err, err2)
}
