package values

import (
	"errors"
	"fmt"
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v3"
	"gopkg.in/ini.v1"

	"github.com/grafana/grafana/pkg/setting"
)

func TestValues(t *testing.T) {
	t.Run("Values", func(t *testing.T) {
		t.Setenv("INT", "1")
		t.Setenv("STRING", "test")
		t.Setenv("EMPTYSTRING", "")
		t.Setenv("BOOL", "true")

		t.Run("IntValue", func(t *testing.T) {
			type Data struct {
				Val IntValue `yaml:"val"`
			}
			t.Run("Should unmarshal simple number", func(t *testing.T) {
				d := &Data{}

				unmarshalingTest(t, `val: 1`, d)
				require.Equal(t, d.Val.Value(), 1)
				require.Equal(t, d.Val.Raw, "1")
			})

			t.Run("Should unmarshal env var", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: $INT`, d)
				require.Equal(t, d.Val.Value(), 1)
				require.Equal(t, d.Val.Raw, "$INT")
			})

			t.Run("Should ignore empty value", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: `, d)
				require.Equal(t, d.Val.Value(), 0)
				require.Equal(t, d.Val.Raw, "")
			})
		})

		t.Run("StringValue", func(t *testing.T) {
			type Data struct {
				Val StringValue `yaml:"val"`
			}
			t.Run("Should unmarshal simple string", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: test`, d)
				require.Equal(t, d.Val.Value(), "test")
				require.Equal(t, d.Val.Raw, "test")
			})

			t.Run("Should unmarshal env var", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: $STRING`, d)
				require.Equal(t, d.Val.Value(), "test")
				require.Equal(t, d.Val.Raw, "$STRING")
			})

			t.Run("Should ignore empty value", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: `, d)
				require.Equal(t, d.Val.Value(), "")
				require.Equal(t, d.Val.Raw, "")
			})

			t.Run("empty var should have empty value", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: $EMPTYSTRING`, d)
				require.Equal(t, d.Val.Value(), "")
				require.Equal(t, d.Val.Raw, "$EMPTYSTRING")
			})

			t.Run("$$ should be a literal $", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: $$`, d)
				require.Equal(t, d.Val.Value(), "$")
				require.Equal(t, d.Val.Raw, "$$")
			})

			t.Run("$$ should be a literal $ and not expanded within a string", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: mY,Passwo$$rd`, d)
				require.Equal(t, d.Val.Value(), "mY,Passwo$rd")
				require.Equal(t, d.Val.Raw, "mY,Passwo$$rd")
			})
		})

		t.Run("BoolValue", func(t *testing.T) {
			type Data struct {
				Val BoolValue `yaml:"val"`
			}
			t.Run("Should unmarshal bool value", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: true`, d)
				require.True(t, d.Val.Value())
				require.Equal(t, d.Val.Raw, "true")
			})

			t.Run("Should unmarshal explicit string", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: "true"`, d)
				require.True(t, d.Val.Value())
				require.Equal(t, d.Val.Raw, "true")
			})

			t.Run("Should unmarshal env var", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: $BOOL`, d)
				require.True(t, d.Val.Value())
				require.Equal(t, d.Val.Raw, "$BOOL")
			})

			t.Run("Should ignore empty value", func(t *testing.T) {
				d := &Data{}
				unmarshalingTest(t, `val: `, d)
				require.False(t, d.Val.Value())
				require.Equal(t, d.Val.Raw, "")
			})
		})

		t.Run("JSONValue", func(t *testing.T) {
			type Data struct {
				Val JSONValue `yaml:"val"`
			}
			d := &Data{}

			t.Run("Should unmarshal variable nesting", func(t *testing.T) {
				doc := `
                 val:
                   one: 1
                   two: $STRING
                   three:
                     - 1
                     - two
                     - three:
                         inside: $STRING
                     - six:
                         empty:
                   four:
                     nested:
                       onemore: $INT
                   multiline: >
                     Some text with $STRING
                   anchor: &label $INT
                   anchored: *label
                   boolval: $BOOL
               `
				unmarshalingTest(t, doc, d)

				type stringMap = map[string]any
				require.Equal(t, d.Val.Value(), stringMap{
					"one": 1,
					"two": "test",
					"three": []any{
						1,
						"two",
						stringMap{
							"three": stringMap{
								"inside": "test",
							},
						},
						stringMap{
							"six": stringMap{
								"empty": any(nil),
							},
						},
					},
					"four": stringMap{
						"nested": stringMap{
							"onemore": int64(1),
						},
					},
					"multiline": "Some text with test\n",
					"anchor":    int64(1),
					"anchored":  int64(1),
					"boolval":   true,
				})

				require.Equal(t, d.Val.Raw, stringMap{
					"one": 1,
					"two": "$STRING",
					"three": []any{
						1,
						"two",
						stringMap{
							"three": stringMap{
								"inside": "$STRING",
							},
						},
						stringMap{
							"six": stringMap{
								"empty": any(nil),
							},
						},
					},
					"four": stringMap{
						"nested": stringMap{
							"onemore": "$INT",
						},
					},
					"multiline": "Some text with $STRING\n",
					"anchor":    "$INT",
					"anchored":  "$INT",
					"boolval":   "$BOOL",
				})
			})
		})

		t.Run("JSONSliceValue", func(t *testing.T) {
			type Data struct {
				Val JSONSliceValue `yaml:"val"`
			}
			d := &Data{}

			t.Run("Should unmarshal top-level slices and nested structures", func(t *testing.T) {
				doc := `
                 val:
                   - interpolatedString: $STRING
                     interpolatedInt: $INT
                     string: "just a string"
                   - interpolatedString: $STRING
                     interpolatedInt: $INT
                     string: "just a string"
               `
				unmarshalingTest(t, doc, d)

				type stringMap = map[string]any

				require.Equal(t, []stringMap{
					{
						"interpolatedString": "test",
						"interpolatedInt":    int64(1),
						"string":             "just a string",
					},
					{
						"interpolatedString": "test",
						"interpolatedInt":    int64(1),
						"string":             "just a string",
					},
				}, d.Val.Value())

				require.Equal(t, []stringMap{
					{
						"interpolatedString": "$STRING",
						"interpolatedInt":    "$INT",
						"string":             "just a string",
					},
					{
						"interpolatedString": "$STRING",
						"interpolatedInt":    "$INT",
						"string":             "just a string",
					},
				}, d.Val.Raw)
			})
		})

		t.Run("StringMapValue", func(t *testing.T) {
			type Data struct {
				Val StringMapValue `yaml:"val"`
			}
			d := &Data{}

			t.Run("Should unmarshal mapping", func(t *testing.T) {
				doc := `
                 val:
                   one: 1
                   two: "test string"
                   three: $STRING
                   four: true
               `
				unmarshalingTest(t, doc, d)
				require.Equal(t, d.Val.Value(), map[string]string{
					"one":   "1",
					"two":   "test string",
					"three": "test",
					"four":  "true",
				})

				require.Equal(t, d.Val.Raw, map[string]string{
					"one":   "1",
					"two":   "test string",
					"three": "$STRING",
					"four":  "true",
				})
			})
		})
	})
}

func unmarshalingTest(t *testing.T, document string, out any) {
	err := yaml.Unmarshal([]byte(document), out)
	require.NoError(t, err)
}

func TestValues_readFile(t *testing.T) {
	type Data struct {
		Val StringValue `yaml:"val"`
	}

	f, err := os.CreateTemp(os.TempDir(), "file expansion *")
	require.NoError(t, err)
	file := f.Name()

	defer func() {
		require.NoError(t, os.Remove(file))
	}()

	const expected = "hello, world"
	_, err = f.WriteString(expected)
	require.NoError(t, err)
	require.NoError(t, f.Close())

	data := &Data{}
	err = yaml.Unmarshal([]byte(fmt.Sprintf("val: $__file{%s}", file)), data)
	require.NoError(t, err)
	assert.Equal(t, expected, data.Val.Value())
}

func TestValues_expanderError(t *testing.T) {
	type Data struct {
		Top JSONValue `yaml:"top"`
	}

	setting.AddExpander("fail", 0, failExpander{})

	data := &Data{}
	err := yaml.Unmarshal([]byte("top:\n  val: $__fail{val}"), data)
	require.Error(t, err)
	require.Truef(t, errors.Is(err, errExpand), "expected error to wrap: %v\ngot: %v", errExpand, err)
	assert.Empty(t, data)
}

var errExpand = errors.New("test error: bad expander")

type failExpander struct{}

func (f failExpander) SetupExpander(file *ini.File) error {
	return nil
}

func (f failExpander) Expand(s string) (string, error) {
	return "", errExpand
}
