// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package taskenv

import (
	"fmt"
	"testing"

	"github.com/hashicorp/nomad/ci"
	"github.com/stretchr/testify/require"
	"github.com/zclconf/go-cty/cty"
)

// TestAddNestedKey_Ok asserts test cases that succeed when passed to
// addNestedKey.
func TestAddNestedKey_Ok(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		// M will be initialized if unset
		M map[string]interface{}
		K string
		// Value is always "x"
		Result map[string]interface{}
	}{
		{
			K: "foo",
			Result: map[string]interface{}{
				"foo": "x",
			},
		},
		{
			K: "foo.bar",
			Result: map[string]interface{}{
				"foo": map[string]interface{}{
					"bar": "x",
				},
			},
		},
		{
			K: "foo.bar.quux",
			Result: map[string]interface{}{
				"foo": map[string]interface{}{
					"bar": map[string]interface{}{
						"quux": "x",
					},
				},
			},
		},
		{
			K: "a.b.c",
			Result: map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": "x",
					},
				},
			},
		},
		{
			// Nested object b should take precedence over values
			M: map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": "c",
					},
				},
			},
			K: "a.b",
			Result: map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": "c",
					},
				},
			},
		},
		{
			M: map[string]interface{}{
				"a": map[string]interface{}{
					"x": "x",
				},
				"z": "z",
			},
			K: "a.b.c",
			Result: map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": "x",
					},
					"x": "x",
				},
				"z": "z",
			},
		},
		{
			M: map[string]interface{}{
				"foo": map[string]interface{}{
					"bar": map[string]interface{}{
						"a":    "z",
						"quux": "z",
					},
				},
			},
			K: "foo.bar.quux",
			Result: map[string]interface{}{
				"foo": map[string]interface{}{
					"bar": map[string]interface{}{
						"a":    "z",
						"quux": "x",
					},
				},
			},
		},
		{
			M: map[string]interface{}{
				"foo":  "1",
				"bar":  "2",
				"quux": "3",
			},
			K: "a.bbbbbb.c",
			Result: map[string]interface{}{
				"foo":  "1",
				"bar":  "2",
				"quux": "3",
				"a": map[string]interface{}{
					"bbbbbb": map[string]interface{}{
						"c": "x",
					},
				},
			},
		},
		// Regardless of whether attr.driver.qemu = "1" is added first
		// or second, attr.driver.qemu.version = "..." should take
		// precedence (nested maps take precedence over values)
		{
			M: map[string]interface{}{
				"attr": map[string]interface{}{
					"driver": map[string]interface{}{
						"qemu": "1",
					},
				},
			},
			K: "attr.driver.qemu.version",
			Result: map[string]interface{}{
				"attr": map[string]interface{}{
					"driver": map[string]interface{}{
						"qemu": map[string]interface{}{
							"version": "x",
						},
					},
				},
			},
		},
		{
			M: map[string]interface{}{
				"attr": map[string]interface{}{
					"driver": map[string]interface{}{
						"qemu": map[string]interface{}{
							"version": "1.2.3",
						},
					},
				},
			},
			K: "attr.driver.qemu",
			Result: map[string]interface{}{
				"attr": map[string]interface{}{
					"driver": map[string]interface{}{
						"qemu": map[string]interface{}{
							"version": "1.2.3",
						},
					},
				},
			},
		},
		{
			M: map[string]interface{}{
				"a": "a",
			},
			K: "a.b",
			Result: map[string]interface{}{
				"a": map[string]interface{}{
					"b": "x",
				},
			},
		},
		{
			M: map[string]interface{}{
				"a": "a",
				"foo": map[string]interface{}{
					"b":   "b",
					"bar": "quux",
				},
				"c": map[string]interface{}{},
			},
			K: "foo.bar.quux",
			Result: map[string]interface{}{
				"a": "a",
				"foo": map[string]interface{}{
					"b": "b",
					"bar": map[string]interface{}{
						"quux": "x",
					},
				},
				"c": map[string]interface{}{},
			},
		},
	}

	for i := range cases {
		tc := cases[i]
		name := tc.K
		if len(tc.M) > 0 {
			name = fmt.Sprintf("%s-%d", name, len(tc.M))
		}
		t.Run(name, func(t *testing.T) {
			ci.Parallel(t)
			if tc.M == nil {
				tc.M = map[string]interface{}{}
			}
			require.NoError(t, addNestedKey(tc.M, tc.K, "x"))
			require.Equal(t, tc.Result, tc.M)
		})
	}
}

// TestAddNestedKey_Bad asserts test cases return an error when passed to
// addNestedKey.
func TestAddNestedKey_Bad(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		// M will be initialized if unset
		M func() map[string]interface{}
		K string
		// Value is always "x"
		// Result is compared by Error() string equality
		Result error
	}{
		{
			K:      ".",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      ".foo",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      "foo.",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      ".a.",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      "foo..bar",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      "foo...bar",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      "foo.bar..quux",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      "foo..bar.quux",
			Result: ErrInvalidObjectPath,
		},
		{
			K:      "foo.bar.quux.",
			Result: ErrInvalidObjectPath,
		},
		{
			M: func() map[string]interface{} {
				return map[string]interface{}{
					"a": "a",
					"foo": map[string]interface{}{
						"b": "b",
						"bar": map[string]interface{}{
							"c": "c",
						},
					},
				}
			},
			K:      "foo.bar.quux.",
			Result: ErrInvalidObjectPath,
		},
		{
			M: func() map[string]interface{} {
				return map[string]interface{}{
					"a": "a",
					"foo": map[string]interface{}{
						"b": "b",
						"bar": map[string]interface{}{
							"c": "c",
						},
					},
				}
			},
			K:      "foo.bar..quux",
			Result: ErrInvalidObjectPath,
		},
		{
			M: func() map[string]interface{} {
				return map[string]interface{}{
					"a": "a",
					"foo": map[string]interface{}{
						"b": "b",
						"bar": map[string]interface{}{
							"c": "c",
						},
					},
				}
			},
			K:      "foo.bar..quux",
			Result: ErrInvalidObjectPath,
		},
	}

	for i := range cases {
		tc := cases[i]
		name := tc.K
		if tc.M != nil {
			name += "-cleanup"
		}
		t.Run(name, func(t *testing.T) {
			ci.Parallel(t)

			// Copy original M value to ensure it doesn't get altered
			if tc.M == nil {
				tc.M = func() map[string]interface{} {
					return map[string]interface{}{}
				}
			}

			// Call func and assert error
			m := tc.M()
			err := addNestedKey(m, tc.K, "x")
			require.EqualError(t, err, tc.Result.Error())

			// Ensure M wasn't altered
			require.Equal(t, tc.M(), m)
		})
	}
}

func TestCtyify_Ok(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name string
		In   map[string]interface{}
		Out  map[string]cty.Value
	}{
		{
			Name: "OneVal",
			In: map[string]interface{}{
				"a": "b",
			},
			Out: map[string]cty.Value{
				"a": cty.StringVal("b"),
			},
		},
		{
			Name: "MultiVal",
			In: map[string]interface{}{
				"a":   "b",
				"foo": "bar",
			},
			Out: map[string]cty.Value{
				"a":   cty.StringVal("b"),
				"foo": cty.StringVal("bar"),
			},
		},
		{
			Name: "NestedVals",
			In: map[string]interface{}{
				"a": "b",
				"foo": map[string]interface{}{
					"c": "d",
					"bar": map[string]interface{}{
						"quux": "z",
					},
				},
				"123": map[string]interface{}{
					"bar": map[string]interface{}{
						"456": "789",
					},
				},
			},
			Out: map[string]cty.Value{
				"a": cty.StringVal("b"),
				"foo": cty.ObjectVal(map[string]cty.Value{
					"c": cty.StringVal("d"),
					"bar": cty.ObjectVal(map[string]cty.Value{
						"quux": cty.StringVal("z"),
					}),
				}),
				"123": cty.ObjectVal(map[string]cty.Value{
					"bar": cty.ObjectVal(map[string]cty.Value{
						"456": cty.StringVal("789"),
					}),
				}),
			},
		},
	}

	for i := range cases {
		tc := cases[i]
		t.Run(tc.Name, func(t *testing.T) {
			ci.Parallel(t)

			// ctiyif and check for errors
			result, err := ctyify(tc.In)
			require.NoError(t, err)

			// convert results to ObjectVals and compare with RawEquals
			resultObj := cty.ObjectVal(result)
			OutObj := cty.ObjectVal(tc.Out)
			require.True(t, OutObj.RawEquals(resultObj))
		})
	}
}

func TestCtyify_Bad(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name string
		In   map[string]interface{}
		Out  map[string]cty.Value
	}{
		{
			Name: "NonStringVal",
			In: map[string]interface{}{
				"a": 1,
			},
		},
		{
			Name: "NestedNonString",
			In: map[string]interface{}{
				"foo": map[string]interface{}{
					"c": 1,
				},
			},
		},
	}

	for i := range cases {
		tc := cases[i]
		t.Run(tc.Name, func(t *testing.T) {
			ci.Parallel(t)

			// ctiyif and check for errors
			result, err := ctyify(tc.In)
			require.Error(t, err)
			require.Nil(t, result)
		})
	}
}
