// 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"
	"reflect"
	"strconv"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp/cmpopts"

	"github.com/google/go-cmp/cmp"

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

var (
	ignoreUnexported = cmpopts.IgnoreUnexported(cty.GetAttrStep{}, cty.IndexStep{})
	valueComparer    = cmp.Comparer(cty.Value.RawEquals)
)

func TestPathFromFlatmap(t *testing.T) {
	tests := []struct {
		Flatmap string
		Type    cty.Type
		Want    cty.Path
		WantErr string
	}{
		{
			Flatmap: "",
			Type:    cty.EmptyObject,
			Want:    nil,
		},
		{
			Flatmap: "attr",
			Type:    cty.EmptyObject,
			Want:    nil,
			WantErr: `attribute "attr" not found`,
		},
		{
			Flatmap: "foo",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.String,
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
			},
		},
		{
			Flatmap: "foo.#",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.String),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
			},
		},
		{
			Flatmap: "foo.1",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.String),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.NumberIntVal(1)},
			},
		},
		{
			Flatmap: "foo.1",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Tuple([]cty.Type{
					cty.String,
					cty.Bool,
				}),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.NumberIntVal(1)},
			},
		},
		{
			// a set index returns the set itself, since this being applied to
			// a diff and the set is changing.
			Flatmap: "foo.24534534",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.String),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
			},
		},
		{
			Flatmap: "foo.%",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
			},
		},
		{
			Flatmap: "foo.baz",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.Bool),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.StringVal("baz")},
			},
		},
		{
			Flatmap: "foo.bar.baz",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(
					cty.Map(cty.Bool),
				),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.StringVal("bar")},
				cty.IndexStep{Key: cty.StringVal("baz")},
			},
		},
		{
			Flatmap: "foo.bar.baz",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(
					cty.Object(map[string]cty.Type{
						"baz": cty.String,
					}),
				),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.StringVal("bar")},
				cty.GetAttrStep{Name: "baz"},
			},
		},
		{
			Flatmap: "foo.0.bar",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.Object(map[string]cty.Type{
					"bar": cty.String,
					"baz": cty.Bool,
				})),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.NumberIntVal(0)},
				cty.GetAttrStep{Name: "bar"},
			},
		},
		{
			Flatmap: "foo.34534534.baz",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Set(cty.Object(map[string]cty.Type{
					"bar": cty.String,
					"baz": cty.Bool,
				})),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
			},
		},
		{
			Flatmap: "foo.bar.bang",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.String,
			}),
			WantErr: `invalid step "bar.bang"`,
		},
		{
			// there should not be any attribute names with dots
			Flatmap: "foo.bar.bang",
			Type: cty.Object(map[string]cty.Type{
				"foo.bar": cty.Map(cty.String),
			}),
			WantErr: `attribute "foo" not found`,
		},
		{
			// We can only handle key names with dots if the map elements are a
			// primitive type.
			Flatmap: "foo.bar.bop",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.StringVal("bar.bop")},
			},
		},
		{
			Flatmap: "foo.bar.0.baz",
			Type: cty.Object(map[string]cty.Type{
				"foo": cty.Map(
					cty.List(
						cty.Map(cty.String),
					),
				),
			}),
			Want: cty.Path{
				cty.GetAttrStep{Name: "foo"},
				cty.IndexStep{Key: cty.StringVal("bar")},
				cty.IndexStep{Key: cty.NumberIntVal(0)},
				cty.IndexStep{Key: cty.StringVal("baz")},
			},
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("%s as %#v", test.Flatmap, test.Type), func(t *testing.T) {
			got, err := requiresReplacePath(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; !strings.Contains(got, want) {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			} else {
				if err != nil {
					t.Fatalf("unexpected error: %s", err.Error())
				}
			}

			if !reflect.DeepEqual(got, test.Want) {
				t.Fatalf("incorrect path\ngot:  %#v\nwant: %#v\n", got, test.Want)
			}
		})
	}
}

func TestRequiresReplace(t *testing.T) {
	for _, tc := range []struct {
		name     string
		attrs    []string
		expected []cty.Path
		ty       cty.Type
	}{
		{
			name: "basic",
			attrs: []string{
				"foo",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.String,
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}},
			},
		},
		{
			name: "two",
			attrs: []string{
				"foo",
				"bar",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.String,
				"bar": cty.String,
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}},
				cty.Path{cty.GetAttrStep{Name: "bar"}},
			},
		},
		{
			name: "nested object",
			attrs: []string{
				"foo.bar",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.Object(map[string]cty.Type{
					"bar": cty.String,
				}),
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}},
			},
		},
		{
			name: "nested objects",
			attrs: []string{
				"foo.bar.baz",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.Object(map[string]cty.Type{
					"bar": cty.Object(map[string]cty.Type{
						"baz": cty.String,
					}),
				}),
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.GetAttrStep{Name: "bar"}, cty.GetAttrStep{Name: "baz"}},
			},
		},
		{
			name: "nested map",
			attrs: []string{
				"foo.%",
				"foo.bar",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}},
			},
		},
		{
			name: "nested list",
			attrs: []string{
				"foo.#",
				"foo.1",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.String),
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}},
			},
		},
		{
			name: "object in map",
			attrs: []string{
				"foo.bar.baz",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.Map(cty.Object(
					map[string]cty.Type{
						"baz": cty.String,
					},
				)),
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.StringVal("bar")}, cty.GetAttrStep{Name: "baz"}},
			},
		},
		{
			name: "object in list",
			attrs: []string{
				"foo.1.baz",
			},
			ty: cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.Object(
					map[string]cty.Type{
						"baz": cty.String,
					},
				)),
			}),
			expected: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "baz"}},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			rp, err := RequiresReplace(tc.attrs, tc.ty)
			if err != nil {
				t.Fatal(err)
			}
			if !cmp.Equal(tc.expected, rp, ignoreUnexported, valueComparer) {
				t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, rp)
			}
		})

	}
}

func TestFlatmapKeyFromPath(t *testing.T) {
	for i, tc := range []struct {
		path cty.Path
		attr string
	}{
		{
			path: cty.Path{
				cty.GetAttrStep{Name: "force_new"},
			},
			attr: "force_new",
		},
		{
			path: cty.Path{
				cty.GetAttrStep{Name: "attr"},
				cty.IndexStep{Key: cty.NumberIntVal(0)},
				cty.GetAttrStep{Name: "force_new"},
			},
			attr: "attr.0.force_new",
		},
		{
			path: cty.Path{
				cty.GetAttrStep{Name: "attr"},
				cty.IndexStep{Key: cty.StringVal("key")},
				cty.GetAttrStep{Name: "obj_attr"},
				cty.IndexStep{Key: cty.NumberIntVal(0)},
				cty.GetAttrStep{Name: "force_new"},
			},
			attr: "attr.key.obj_attr.0.force_new",
		},
	} {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			attr := FlatmapKeyFromPath(tc.path)
			if attr != tc.attr {
				t.Fatalf("expected:%q got:%q", tc.attr, attr)
			}
		})
	}
}
