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

package hcl2shim

import (
	"fmt"
	"testing"

	"github.com/go-test/deep"

	"github.com/zclconf/go-cty/cty"
)

func TestFlatmapValueFromHCL2(t *testing.T) {
	tests := []struct {
		Value cty.Value
		Want  map[string]string
	}{
		{
			cty.EmptyObjectVal,
			map[string]string{},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("hello"),
			}),
			map[string]string{
				"foo": "hello",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.Bool),
			}),
			map[string]string{
				"foo": UnknownVariableValue,
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NumberIntVal(12),
			}),
			map[string]string{
				"foo": "12",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
				"bar": cty.False,
			}),
			map[string]string{
				"foo": "true",
				"bar": "false",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("hello"),
				"bar": cty.StringVal("world"),
				"baz": cty.StringVal("whelp"),
			}),
			map[string]string{
				"foo": "hello",
				"bar": "world",
				"baz": "whelp",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListValEmpty(cty.String),
			}),
			map[string]string{
				"foo.#": "0",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.List(cty.String)),
			}),
			map[string]string{
				"foo.#": UnknownVariableValue,
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
				}),
			}),
			map[string]string{
				"foo.#": "1",
				"foo.0": "hello",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("world"),
				}),
			}),
			map[string]string{
				"foo.#": "2",
				"foo.0": "hello",
				"foo.1": "world",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"hello":       cty.NumberIntVal(12),
					"hello.world": cty.NumberIntVal(10),
				}),
			}),
			map[string]string{
				"foo.%":           "2",
				"foo.hello":       "12",
				"foo.hello.world": "10",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.Map(cty.String)),
			}),
			map[string]string{
				"foo.%": UnknownVariableValue,
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"hello":       cty.NumberIntVal(12),
					"hello.world": cty.NumberIntVal(10),
				}),
			}),
			map[string]string{
				"foo.%":           "2",
				"foo.hello":       "12",
				"foo.hello.world": "10",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("world"),
				}),
			}),
			map[string]string{
				"foo.#": "2",
				"foo.0": "hello",
				"foo.1": "world",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.Set(cty.Number)),
			}),
			map[string]string{
				"foo.#": UnknownVariableValue,
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("hello"),
						"baz": cty.StringVal("world"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("bloo"),
						"baz": cty.StringVal("blaa"),
					}),
				}),
			}),
			map[string]string{
				"foo.#":     "2",
				"foo.0.bar": "hello",
				"foo.0.baz": "world",
				"foo.1.bar": "bloo",
				"foo.1.baz": "blaa",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("hello"),
						"baz": cty.ListVal([]cty.Value{
							cty.True,
							cty.True,
						}),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("bloo"),
						"baz": cty.ListVal([]cty.Value{
							cty.False,
							cty.True,
						}),
					}),
				}),
			}),
			map[string]string{
				"foo.#":       "2",
				"foo.0.bar":   "hello",
				"foo.0.baz.#": "2",
				"foo.0.baz.0": "true",
				"foo.0.baz.1": "true",
				"foo.1.bar":   "bloo",
				"foo.1.baz.#": "2",
				"foo.1.baz.0": "false",
				"foo.1.baz.1": "true",
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.UnknownVal(cty.Object(map[string]cty.Type{
						"bar": cty.String,
						"baz": cty.List(cty.Bool),
						"bap": cty.Map(cty.Number),
					})),
				}),
			}),
			map[string]string{
				"foo.#":       "1",
				"foo.0.bar":   UnknownVariableValue,
				"foo.0.baz.#": UnknownVariableValue,
				"foo.0.bap.%": UnknownVariableValue,
			},
		},
		{
			cty.NullVal(cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Object(map[string]cty.Type{
					"bar": cty.String,
				})),
			})),
			nil,
		},
	}

	for _, test := range tests {
		t.Run(test.Value.GoString(), func(t *testing.T) {
			got := FlatmapValueFromHCL2(test.Value)

			for _, problem := range deep.Equal(got, test.Want) {
				t.Error(problem)
			}
		})
	}
}

func TestFlatmapValueFromHCL2FromFlatmap(t *testing.T) {
	tests := []struct {
		Name string
		Map  map[string]string
		Type cty.Type
	}{
		{
			"empty flatmap with collections",
			map[string]string{},
			cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
				"bar": cty.Set(cty.String),
			}),
		},
		{
			"nil flatmap with collections",
			nil,
			cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
				"bar": cty.Set(cty.String),
			}),
		},
		{
			"empty flatmap with nested collections",
			map[string]string{},
			cty.Object(map[string]cty.Type{
				"foo": cty.Object(
					map[string]cty.Type{
						"baz": cty.Map(cty.String),
					},
				),
				"bar": cty.Set(cty.String),
			}),
		},
		{
			"partial flatmap with nested collections",
			map[string]string{
				"foo.baz.%":   "1",
				"foo.baz.key": "val",
			},
			cty.Object(map[string]cty.Type{
				"foo": cty.Object(
					map[string]cty.Type{
						"baz": cty.Map(cty.String),
						"biz": cty.Map(cty.String),
					},
				),
				"bar": cty.Set(cty.String),
			}),
		},
	}

	for _, test := range tests {
		t.Run(test.Name, func(t *testing.T) {
			val, err := HCL2ValueFromFlatmap(test.Map, test.Type)
			if err != nil {
				t.Fatal(err)
			}

			got := FlatmapValueFromHCL2(val)

			for _, problem := range deep.Equal(got, test.Map) {
				t.Error(problem)
			}
		})
	}
}
func TestHCL2ValueFromFlatmap(t *testing.T) {
	tests := []struct {
		Flatmap map[string]string
		Type    cty.Type
		Want    cty.Value
		WantErr string
	}{
		{
			Flatmap: map[string]string{},
			Type:    cty.EmptyObject,
			Want:    cty.EmptyObjectVal,
		},
		{
			Flatmap: map[string]string{
				"ignored": "foo",
			},
			Type: cty.EmptyObject,
			Want: cty.EmptyObjectVal,
		},
		{
			Flatmap: map[string]string{
				"foo": "blah",
				"bar": "true",
				"baz": "12.5",
				"unk": UnknownVariableValue,
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.String,
				"bar": cty.Bool,
				"baz": cty.Number,
				"unk": cty.Bool,
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("blah"),
				"bar": cty.True,
				"baz": cty.NumberFloatVal(12.5),
				"unk": cty.UnknownVal(cty.Bool),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "0",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListValEmpty(cty.String),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": UnknownVariableValue,
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.List(cty.String)),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "1",
				"foo.0": "hello",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "2",
				"foo.0": "true",
				"foo.1": "false",
				"foo.2": "ignored", // (because the count is 2, so this is out of range)
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.Bool),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.True,
					cty.False,
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "2",
				"foo.0": "hello",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Tuple([]cty.Type{
					cty.String,
					cty.Bool,
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.TupleVal([]cty.Value{
					cty.StringVal("hello"),
					cty.NullVal(cty.Bool),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": UnknownVariableValue,
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Tuple([]cty.Type{
					cty.String,
					cty.Bool,
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.Tuple([]cty.Type{
					cty.String,
					cty.Bool,
				})),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "0",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetValEmpty(cty.String),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": UnknownVariableValue,
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.Set(cty.String)),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#":        "1",
				"foo.24534534": "hello",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetVal([]cty.Value{
					cty.StringVal("hello"),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#":        "1",
				"foo.24534534": "true",
				"foo.95645644": "true",
				"foo.34533452": "false",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Bool),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetVal([]cty.Value{
					cty.True,
					cty.False,
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.%": "0",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapValEmpty(cty.String),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.%":       "2",
				"foo.baz":     "true",
				"foo.bar.baz": "false",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.Bool),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"baz":     cty.True,
					"bar.baz": cty.False,
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.%": UnknownVariableValue,
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.Bool),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.Map(cty.Bool)),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#":     "2",
				"foo.0.bar": "hello",
				"foo.0.baz": "1",
				"foo.1.bar": "world",
				"foo.1.baz": "false",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.Object(map[string]cty.Type{
					"bar": cty.String,
					"baz": cty.Bool,
				})),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("hello"),
						"baz": cty.True,
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("world"),
						"baz": cty.False,
					}),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#":            "2",
				"foo.34534534.bar": "hello",
				"foo.34534534.baz": "1",
				"foo.93453345.bar": "world",
				"foo.93453345.baz": "false",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Object(map[string]cty.Type{
					"bar": cty.String,
					"baz": cty.Bool,
				})),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("hello"),
						"baz": cty.True,
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("world"),
						"baz": cty.False,
					}),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "not-valid",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.String),
			}),
			WantErr: `invalid count value for "foo." in state: strconv.Atoi: parsing "not-valid": invalid syntax`,
		},
		{
			Flatmap: nil,
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Object(map[string]cty.Type{
					"bar": cty.String,
				})),
			}),
			Want: cty.NullVal(cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Object(map[string]cty.Type{
					"bar": cty.String,
				})),
			})),
		},
		{
			Flatmap: map[string]string{
				"foo.#":   "2",
				"foo.0.%": "2",
				"foo.0.a": "a",
				"foo.0.b": "b",
				"foo.1.%": "1",
				"foo.1.a": "a",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.Map(cty.String)),
			}),

			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.MapVal(map[string]cty.Value{
						"a": cty.StringVal("a"),
						"b": cty.StringVal("b"),
					}),
					cty.MapVal(map[string]cty.Value{
						"a": cty.StringVal("a"),
					}),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"single.#":                 "1",
				"single.~1.value":          "a",
				"single.~1.optional":       UnknownVariableValue,
				"two.#":                    "2",
				"two.~2381914684.value":    "a",
				"two.~2381914684.optional": UnknownVariableValue,
				"two.~2798940671.value":    "b",
				"two.~2798940671.optional": UnknownVariableValue,
			},
			Type: cty.Object(map[string]cty.Type{
				"single": cty.Set(
					cty.Object(map[string]cty.Type{
						"value":    cty.String,
						"optional": cty.String,
					}),
				),
				"two": cty.Set(
					cty.Object(map[string]cty.Type{
						"optional": cty.String,
						"value":    cty.String,
					}),
				),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"single": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"value":    cty.StringVal("a"),
						"optional": cty.UnknownVal(cty.String),
					}),
				}),
				"two": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"value":    cty.StringVal("a"),
						"optional": cty.UnknownVal(cty.String),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"value":    cty.StringVal("b"),
						"optional": cty.UnknownVal(cty.String),
					}),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"foo.#": "1",
			},
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Object(map[string]cty.Type{
					"bar": cty.String,
				})),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.NullVal(cty.String),
					}),
				}),
			}),
		},
		{
			Flatmap: map[string]string{
				"multi.#":                "1",
				"multi.2.set.#":          "1",
				"multi.2.set.3.required": "val",
			},
			Type: cty.Object(map[string]cty.Type{
				"multi": cty.Set(cty.Object(map[string]cty.Type{
					"set": cty.Set(cty.Object(map[string]cty.Type{
						"required": cty.String,
					})),
				})),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"multi": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"set": cty.SetVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"required": cty.StringVal("val"),
							}),
						}),
					}),
				}),
			}),
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%d %#v as %#v", i, test.Flatmap, test.Type), func(t *testing.T) {
			got, err := HCL2ValueFromFlatmap(test.Flatmap, test.Type)

			if test.WantErr != "" {
				if err == nil {
					t.Fatalf("succeeded; want error: %s", test.WantErr)
				}
				if got, want := err.Error(), test.WantErr; got != want {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				if got == cty.NilVal {
					t.Fatalf("result is cty.NilVal; want valid placeholder value")
				}
				return
			} else {
				if err != nil {
					t.Fatalf("unexpected error: %s", err.Error())
				}
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}
