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

package tofu

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/zclconf/go-cty/cty"

	"github.com/mitchellh/reflectwalk"
	"github.com/opentofu/opentofu/internal/configs/hcl2shim"
)

func TestResourceConfigGet(t *testing.T) {
	fooStringSchema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"foo": {Type: cty.String, Optional: true},
		},
	}
	fooListSchema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"foo": {Type: cty.List(cty.Number), Optional: true},
		},
	}

	cases := []struct {
		Config cty.Value
		Schema *configschema.Block
		Key    string
		Value  interface{}
	}{
		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			Schema: fooStringSchema,
			Key:    "foo",
			Value:  "bar",
		},

		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.String),
			}),
			Schema: fooStringSchema,
			Key:    "foo",
			Value:  hcl2shim.UnknownVariableValue,
		},

		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.NumberIntVal(1),
					cty.NumberIntVal(2),
					cty.NumberIntVal(5),
				}),
			}),
			Schema: fooListSchema,
			Key:    "foo.0",
			Value:  1,
		},

		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.NumberIntVal(1),
					cty.NumberIntVal(2),
					cty.NumberIntVal(5),
				}),
			}),
			Schema: fooListSchema,
			Key:    "foo.5",
			Value:  nil,
		},

		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.NumberIntVal(1),
					cty.NumberIntVal(2),
					cty.NumberIntVal(5),
				}),
			}),
			Schema: fooListSchema,
			Key:    "foo.-1",
			Value:  nil,
		},

		// get from map
		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"mapname": cty.ListVal([]cty.Value{
					cty.MapVal(map[string]cty.Value{
						"key": cty.NumberIntVal(1),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
				},
			},
			Key:   "mapname.0.key",
			Value: 1,
		},

		// get from map with dot in key
		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"mapname": cty.ListVal([]cty.Value{
					cty.MapVal(map[string]cty.Value{
						"key.name": cty.NumberIntVal(1),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
				},
			},
			Key:   "mapname.0.key.name",
			Value: 1,
		},

		// get from map with overlapping key names
		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"mapname": cty.ListVal([]cty.Value{
					cty.MapVal(map[string]cty.Value{
						"key.name":   cty.NumberIntVal(1),
						"key.name.2": cty.NumberIntVal(2),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
				},
			},
			Key:   "mapname.0.key.name.2",
			Value: 2,
		},
		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"mapname": cty.ListVal([]cty.Value{
					cty.MapVal(map[string]cty.Value{
						"key.name":     cty.NumberIntVal(1),
						"key.name.foo": cty.NumberIntVal(2),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
				},
			},
			Key:   "mapname.0.key.name",
			Value: 1,
		},
		{
			Config: cty.ObjectVal(map[string]cty.Value{
				"mapname": cty.ListVal([]cty.Value{
					cty.MapVal(map[string]cty.Value{
						"listkey": cty.ListVal([]cty.Value{
							cty.MapVal(map[string]cty.Value{
								"key": cty.NumberIntVal(3),
							}),
						}),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"mapname": {Type: cty.List(cty.Map(cty.List(cty.Map(cty.Number)))), Optional: true},
				},
			},
			Key:   "mapname.0.listkey.0.key",
			Value: 3,
		},
	}

	for i, tc := range cases {
		rc := NewResourceConfigShimmed(tc.Config, tc.Schema)

		// Test getting a key
		t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) {
			v, ok := rc.Get(tc.Key)
			if ok && v == nil {
				t.Fatal("(nil, true) returned from Get")
			}

			if !reflect.DeepEqual(v, tc.Value) {
				t.Fatalf("%d bad: %#v", i, v)
			}
		})

		// Test copying and equality
		t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) {
			copy := rc.DeepCopy()
			if !reflect.DeepEqual(copy, rc) {
				t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc)
			}

			if !copy.Equal(rc) {
				t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc)
			}
			if !rc.Equal(copy) {
				t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc)
			}
		})
	}
}

func TestResourceConfigDeepCopy_nil(t *testing.T) {
	var nilRc *ResourceConfig
	actual := nilRc.DeepCopy()
	if actual != nil {
		t.Fatalf("bad: %#v", actual)
	}
}

func TestResourceConfigDeepCopy_nilComputed(t *testing.T) {
	rc := &ResourceConfig{}
	actual := rc.DeepCopy()
	if actual.ComputedKeys != nil {
		t.Fatalf("bad: %#v", actual)
	}
}

func TestResourceConfigEqual_nil(t *testing.T) {
	var nilRc *ResourceConfig
	notNil := NewResourceConfigShimmed(cty.EmptyObjectVal, &configschema.Block{})

	if nilRc.Equal(notNil) {
		t.Fatal("should not be equal")
	}

	if notNil.Equal(nilRc) {
		t.Fatal("should not be equal")
	}
}

func TestResourceConfigEqual_computedKeyOrder(t *testing.T) {
	v := cty.ObjectVal(map[string]cty.Value{
		"foo": cty.UnknownVal(cty.String),
	})
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"foo": {Type: cty.String, Optional: true},
		},
	}
	rc := NewResourceConfigShimmed(v, schema)
	rc2 := NewResourceConfigShimmed(v, schema)

	// Set the computed keys manually to force ordering to differ
	rc.ComputedKeys = []string{"foo", "bar"}
	rc2.ComputedKeys = []string{"bar", "foo"}

	if !rc.Equal(rc2) {
		t.Fatal("should be equal")
	}
}

func TestUnknownCheckWalker(t *testing.T) {
	cases := []struct {
		Name   string
		Input  interface{}
		Result bool
	}{
		{
			"primitive",
			42,
			false,
		},

		{
			"primitive computed",
			hcl2shim.UnknownVariableValue,
			true,
		},

		{
			"list",
			[]interface{}{"foo", hcl2shim.UnknownVariableValue},
			true,
		},

		{
			"nested list",
			[]interface{}{
				"foo",
				[]interface{}{hcl2shim.UnknownVariableValue},
			},
			true,
		},
	}

	for i, tc := range cases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
			var w unknownCheckWalker
			if err := reflectwalk.Walk(tc.Input, &w); err != nil {
				t.Fatalf("err: %s", err)
			}

			if w.Unknown != tc.Result {
				t.Fatalf("bad: %v", w.Unknown)
			}
		})
	}
}

func TestNewResourceConfigShimmed(t *testing.T) {
	for _, tc := range []struct {
		Name     string
		Val      cty.Value
		Schema   *configschema.Block
		Expected *ResourceConfig
	}{
		{
			Name: "empty object",
			Val:  cty.NullVal(cty.EmptyObject),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			Expected: &ResourceConfig{
				Raw:    map[string]interface{}{},
				Config: map[string]interface{}{},
			},
		},
		{
			Name: "basic",
			Val: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			Expected: &ResourceConfig{
				Raw: map[string]interface{}{
					"foo": "bar",
				},
				Config: map[string]interface{}{
					"foo": "bar",
				},
			},
		},
		{
			Name: "null string",
			Val: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			Expected: &ResourceConfig{
				Raw:    map[string]interface{}{},
				Config: map[string]interface{}{},
			},
		},
		{
			Name: "unknown string",
			Val: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			Expected: &ResourceConfig{
				ComputedKeys: []string{"foo"},
				Raw: map[string]interface{}{
					"foo": hcl2shim.UnknownVariableValue,
				},
				Config: map[string]interface{}{
					"foo": hcl2shim.UnknownVariableValue,
				},
			},
		},
		{
			Name: "unknown collections",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.UnknownVal(cty.Map(cty.String)),
				"baz": cty.UnknownVal(cty.List(cty.String)),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"bar": {
						Type:     cty.Map(cty.String),
						Required: true,
					},
					"baz": {
						Type:     cty.List(cty.String),
						Optional: true,
					},
				},
			},
			Expected: &ResourceConfig{
				ComputedKeys: []string{"bar", "baz"},
				Raw: map[string]interface{}{
					"bar": hcl2shim.UnknownVariableValue,
					"baz": hcl2shim.UnknownVariableValue,
				},
				Config: map[string]interface{}{
					"bar": hcl2shim.UnknownVariableValue,
					"baz": hcl2shim.UnknownVariableValue,
				},
			},
		},
		{
			Name: "null collections",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.NullVal(cty.Map(cty.String)),
				"baz": cty.NullVal(cty.List(cty.String)),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"bar": {
						Type:     cty.Map(cty.String),
						Required: true,
					},
					"baz": {
						Type:     cty.List(cty.String),
						Optional: true,
					},
				},
			},
			Expected: &ResourceConfig{
				Raw:    map[string]interface{}{},
				Config: map[string]interface{}{},
			},
		},
		{
			Name: "unknown blocks",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.UnknownVal(cty.Map(cty.String)),
				"baz": cty.UnknownVal(cty.List(cty.String)),
			}),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"bar": {
						Block:   configschema.Block{},
						Nesting: configschema.NestingList,
					},
					"baz": {
						Block:   configschema.Block{},
						Nesting: configschema.NestingSet,
					},
				},
			},
			Expected: &ResourceConfig{
				ComputedKeys: []string{"bar", "baz"},
				Raw: map[string]interface{}{
					"bar": hcl2shim.UnknownVariableValue,
					"baz": hcl2shim.UnknownVariableValue,
				},
				Config: map[string]interface{}{
					"bar": hcl2shim.UnknownVariableValue,
					"baz": hcl2shim.UnknownVariableValue,
				},
			},
		},
		{
			Name: "unknown in nested blocks",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"baz": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"list": cty.UnknownVal(cty.List(cty.String)),
							}),
						}),
					}),
				}),
			}),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"bar": {
						Block: configschema.Block{
							BlockTypes: map[string]*configschema.NestedBlock{
								"baz": {
									Block: configschema.Block{
										Attributes: map[string]*configschema.Attribute{
											"list": {Type: cty.List(cty.String),
												Optional: true,
											},
										},
									},
									Nesting: configschema.NestingList,
								},
							},
						},
						Nesting: configschema.NestingList,
					},
				},
			},
			Expected: &ResourceConfig{
				ComputedKeys: []string{"bar.0.baz.0.list"},
				Raw: map[string]interface{}{
					"bar": []interface{}{map[string]interface{}{
						"baz": []interface{}{map[string]interface{}{
							"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
						}},
					}},
				},
				Config: map[string]interface{}{
					"bar": []interface{}{map[string]interface{}{
						"baz": []interface{}{map[string]interface{}{
							"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
						}},
					}},
				},
			},
		},
		{
			Name: "unknown in set",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"val": cty.UnknownVal(cty.String),
					}),
				}),
			}),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"bar": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"val": {
									Type:     cty.String,
									Optional: true,
								},
							},
						},
						Nesting: configschema.NestingSet,
					},
				},
			},
			Expected: &ResourceConfig{
				ComputedKeys: []string{"bar.0.val"},
				Raw: map[string]interface{}{
					"bar": []interface{}{map[string]interface{}{
						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
					}},
				},
				Config: map[string]interface{}{
					"bar": []interface{}{map[string]interface{}{
						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
					}},
				},
			},
		},
		{
			Name: "unknown in attribute sets",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"val": cty.UnknownVal(cty.String),
					}),
				}),
				"baz": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"obj": cty.UnknownVal(cty.Object(map[string]cty.Type{
							"attr": cty.List(cty.String),
						})),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"obj": cty.ObjectVal(map[string]cty.Value{
							"attr": cty.UnknownVal(cty.List(cty.String)),
						}),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"bar": &configschema.Attribute{
						Type: cty.Set(cty.Object(map[string]cty.Type{
							"val": cty.String,
						})),
					},
					"baz": &configschema.Attribute{
						Type: cty.Set(cty.Object(map[string]cty.Type{
							"obj": cty.Object(map[string]cty.Type{
								"attr": cty.List(cty.String),
							}),
						})),
					},
				},
			},
			Expected: &ResourceConfig{
				ComputedKeys: []string{"bar.0.val", "baz.0.obj.attr", "baz.1.obj"},
				Raw: map[string]interface{}{
					"bar": []interface{}{map[string]interface{}{
						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
					}},
					"baz": []interface{}{
						map[string]interface{}{
							"obj": map[string]interface{}{
								"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
							},
						},
						map[string]interface{}{
							"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
						},
					},
				},
				Config: map[string]interface{}{
					"bar": []interface{}{map[string]interface{}{
						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
					}},
					"baz": []interface{}{
						map[string]interface{}{
							"obj": map[string]interface{}{
								"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
							},
						},
						map[string]interface{}{
							"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
						},
					},
				},
			},
		},
		{
			Name: "null blocks",
			Val: cty.ObjectVal(map[string]cty.Value{
				"bar": cty.NullVal(cty.Map(cty.String)),
				"baz": cty.NullVal(cty.List(cty.String)),
			}),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"bar": {
						Block:   configschema.Block{},
						Nesting: configschema.NestingMap,
					},
					"baz": {
						Block:   configschema.Block{},
						Nesting: configschema.NestingSingle,
					},
				},
			},
			Expected: &ResourceConfig{
				Raw:    map[string]interface{}{},
				Config: map[string]interface{}{},
			},
		},
	} {
		t.Run(tc.Name, func(*testing.T) {
			cfg := NewResourceConfigShimmed(tc.Val, tc.Schema)
			if !tc.Expected.Equal(cfg) {
				t.Fatalf("expected:\n%#v\ngot:\n%#v", tc.Expected, cfg)
			}
		})
	}
}
