package context

import (
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/gocty"
)

func Test_ContextVariables(t *testing.T) {
	underlying := &hcl.EvalContext{}
	ctx := NewContext(underlying, nil)

	val, err := gocty.ToCtyValue("hello", cty.String)
	if err != nil {
		t.Fatal(err)
	}

	ctx.Set(val, "my", "value")
	value := underlying.Variables["my"].AsValueMap()["value"]
	assert.Equal(t, "hello", value.AsString())

}

func Test_ContextVariablesPreservation(t *testing.T) {

	underlying := &hcl.EvalContext{}
	underlying.Variables = make(map[string]cty.Value)
	underlying.Variables["x"], _ = gocty.ToCtyValue("does it work?", cty.String)
	str, _ := gocty.ToCtyValue("something", cty.String)
	underlying.Variables["my"] = cty.ObjectVal(map[string]cty.Value{
		"other": str,
		"obj": cty.ObjectVal(map[string]cty.Value{
			"another": str,
		}),
	})
	ctx := NewContext(underlying, nil)

	val, err := gocty.ToCtyValue("hello", cty.String)
	if err != nil {
		t.Fatal(err)
	}

	ctx.Set(val, "my", "value")
	assert.Equal(t, "hello", underlying.Variables["my"].AsValueMap()["value"].AsString())
	assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["other"].AsString())
	assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["obj"].AsValueMap()["another"].AsString())
	assert.Equal(t, "does it work?", underlying.Variables["x"].AsString())

}

func Test_SetWithMerge(t *testing.T) {
	hctx := hcl.EvalContext{
		Variables: map[string]cty.Value{
			"my": cty.ObjectVal(map[string]cty.Value{
				"someValue": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("test"),
					"bar": cty.ObjectVal(map[string]cty.Value{
						"foo": cty.StringVal("test"),
					}),
				}),
			}),
		},
	}

	ctx := NewContext(&hctx, nil)

	val := cty.ObjectVal(map[string]cty.Value{
		"foo2": cty.StringVal("test2"),
		"bar": cty.ObjectVal(map[string]cty.Value{
			"foo2": cty.StringVal("test2"),
		}),
	})

	ctx.Set(val, "my", "someValue")
	got := ctx.Get("my", "someValue")
	expected := cty.ObjectVal(map[string]cty.Value{
		"foo":  cty.StringVal("test"),
		"foo2": cty.StringVal("test2"),
		"bar": cty.ObjectVal(map[string]cty.Value{
			"foo":  cty.StringVal("test"),
			"foo2": cty.StringVal("test2"),
		}),
	})

	assert.Equal(t, expected, got)
}

func Test_ContextVariablesPreservationByDot(t *testing.T) {

	underlying := &hcl.EvalContext{}
	underlying.Variables = make(map[string]cty.Value)
	underlying.Variables["x"], _ = gocty.ToCtyValue("does it work?", cty.String)
	str, _ := gocty.ToCtyValue("something", cty.String)
	underlying.Variables["my"] = cty.ObjectVal(map[string]cty.Value{
		"other": str,
		"obj": cty.ObjectVal(map[string]cty.Value{
			"another": str,
		}),
	})
	ctx := NewContext(underlying, nil)

	val, err := gocty.ToCtyValue("hello", cty.String)
	if err != nil {
		t.Fatal(err)
	}

	ctx.SetByDot(val, "my.something.value")
	assert.Equal(t, "hello", underlying.Variables["my"].AsValueMap()["something"].AsValueMap()["value"].AsString())
	assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["other"].AsString())
	assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["obj"].AsValueMap()["another"].AsString())
	assert.Equal(t, "does it work?", underlying.Variables["x"].AsString())
}

func Test_ContextSetThenImmediateGet(t *testing.T) {

	underlying := &hcl.EvalContext{}

	ctx := NewContext(underlying, nil)

	ctx.Set(cty.ObjectVal(map[string]cty.Value{
		"mod_result": cty.StringVal("ok"),
	}), "module", "modulename")

	val := ctx.Get("module", "modulename", "mod_result")
	assert.Equal(t, "ok", val.AsString())
}

func Test_ContextSetThenImmediateGetWithChild(t *testing.T) {

	underlying := &hcl.EvalContext{}

	ctx := NewContext(underlying, nil)

	childCtx := ctx.NewChild()

	childCtx.Root().Set(cty.ObjectVal(map[string]cty.Value{
		"mod_result": cty.StringVal("ok"),
	}), "module", "modulename")

	val := ctx.Get("module", "modulename", "mod_result")
	assert.Equal(t, "ok", val.AsString())
}

func Test_MergeObjects(t *testing.T) {

	tests := []struct {
		name     string
		oldVal   cty.Value
		newVal   cty.Value
		expected cty.Value
	}{
		{
			name: "happy",
			oldVal: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"id":  cty.StringVal("some_id"),
					"arn": cty.StringVal("some_arn"),
				}),
			}),
			newVal: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"arn":    cty.StringVal("some_new_arn"),
					"bucket": cty.StringVal("test"),
				}),
			}),
			expected: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"id":     cty.StringVal("some_id"),
					"arn":    cty.StringVal("some_new_arn"),
					"bucket": cty.StringVal("test"),
				}),
			}),
		},
		{
			name:   "old value is empty",
			oldVal: cty.EmptyObjectVal,
			newVal: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"bucket": cty.StringVal("test"),
				}),
			}),
			expected: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"bucket": cty.StringVal("test"),
				}),
			}),
		},
		{
			name: "new value is empty",
			oldVal: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"bucket": cty.StringVal("test"),
				}),
			}),
			newVal: cty.EmptyObjectVal,
			expected: cty.ObjectVal(map[string]cty.Value{
				"this": cty.ObjectVal(map[string]cty.Value{
					"bucket": cty.StringVal("test"),
				}),
			}),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, mergeObjects(tt.oldVal, tt.newVal))
		})
	}

}

func Test_IsNotEmptyObject(t *testing.T) {
	tests := []struct {
		name     string
		val      cty.Value
		expected bool
	}{
		{
			name: "happy",
			val: cty.ObjectVal(map[string]cty.Value{
				"field": cty.NilVal,
			}),
			expected: true,
		},
		{
			name:     "empty object",
			val:      cty.EmptyObjectVal,
			expected: false,
		},
		{
			name:     "nil value",
			val:      cty.NilVal,
			expected: false,
		},
		{
			name:     "dynamic value",
			val:      cty.DynamicVal,
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, isNotEmptyObject(tt.val))
		})
	}
}

func TestReplace(t *testing.T) {
	t.Run("replacement of an existing value", func(t *testing.T) {
		underlying := &hcl.EvalContext{}
		ctx := NewContext(underlying, nil)
		ctx.SetByDot(cty.StringVal("some-value"), "my.value")
		require.NotEqual(t, cty.NilVal, ctx.GetByDot("my.value"))
		ctx.Replace(cty.NumberIntVal(-1), "my.value")
		assert.Equal(t, cty.NumberIntVal(-1), ctx.GetByDot("my.value"))
	})

	t.Run("replacement of a non-existing value", func(t *testing.T) {
		underlying := &hcl.EvalContext{}
		ctx := NewContext(underlying, nil)
		ctx.Replace(cty.NumberIntVal(-1), "my.value")
		assert.Equal(t, cty.NumberIntVal(-1), ctx.GetByDot("my.value"))
	})

	t.Run("empty path", func(_ *testing.T) {
		underlying := &hcl.EvalContext{}
		ctx := NewContext(underlying, nil)
		ctx.Replace(cty.NumberIntVal(-1), "")
	})
}
