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

package configschema

import (
	"fmt"
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"
)

func TestBlockEmptyAndUnknownValue(t *testing.T) {
	tests := []struct {
		Schema  *Block
		Empty   cty.Value
		Unknown cty.Value
	}{
		{
			&Block{},
			cty.EmptyObjectVal,
			cty.ObjectVal(nil),
		},
		{
			&Block{
				Attributes: map[string]*Attribute{
					"str": {Type: cty.String, Required: true},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"str": cty.NullVal(cty.String),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"str": cty.UnknownVal(cty.String),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"single": {
						Nesting: NestingSingle,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.String, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"single": cty.NullVal(cty.Object(map[string]cty.Type{
					"str": cty.String,
				})),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"single": cty.UnknownVal(cty.Object(map[string]cty.Type{
					"str": cty.String,
				})),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"group": {
						Nesting: NestingGroup,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.String, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"group": cty.ObjectVal(map[string]cty.Value{
					"str": cty.NullVal(cty.String),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"group": cty.UnknownVal(cty.Object(map[string]cty.Type{
					"str": cty.String,
				})),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"list": {
						Nesting: NestingList,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.String, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"list": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"str": cty.String,
				})),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"list": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"str": cty.String,
				}))),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"list_dynamic": {
						Nesting: NestingList,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.DynamicPseudoType, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"list_dynamic": cty.EmptyTupleVal,
			}),
			cty.ObjectVal(map[string]cty.Value{
				"list_dynamic": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"str": cty.DynamicPseudoType,
				}))),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"map": {
						Nesting: NestingMap,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.String, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"map": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"str": cty.String,
				})),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"map": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"str": cty.String,
				}))),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"map_dynamic": {
						Nesting: NestingMap,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.DynamicPseudoType, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"map_dynamic": cty.EmptyObjectVal,
			}),
			cty.ObjectVal(map[string]cty.Value{
				"map_dynamic": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"str": cty.DynamicPseudoType,
				}))),
			}),
		},
		{
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"set": {
						Nesting: NestingSet,
						Block: Block{
							Attributes: map[string]*Attribute{
								"str": {Type: cty.String, Required: true},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"set": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"str": cty.String,
				})),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"set": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
					"str": cty.String,
				}))),
			}),
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("empty: %#v", test.Schema), func(t *testing.T) {
			got := test.Schema.EmptyValue()
			if !test.Empty.RawEquals(got) {
				t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), ctydebug.ValueString(got), ctydebug.ValueString(test.Empty))
			}
		})
		t.Run(fmt.Sprintf("unknown: %#v", test.Schema), func(t *testing.T) {
			got := test.Schema.UnknownValue()
			if !test.Unknown.RawEquals(got) {
				t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), ctydebug.ValueString(got), ctydebug.ValueString(test.Unknown))
			}
		})
	}
}

// Attribute EmptyValue() is well covered by the Block tests above; these tests
// focus on the behavior with NestedType field inside an Attribute
func TestAttributeEmptyValue(t *testing.T) {
	tests := []struct {
		Schema *Attribute
		Want   cty.Value
	}{
		{
			&Attribute{},
			cty.NilVal,
		},
		{
			&Attribute{
				Type: cty.String,
			},
			cty.NullVal(cty.String),
		},
		{
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSingle,
					Attributes: map[string]*Attribute{
						"str": {Type: cty.String, Required: true},
					},
				},
			},
			cty.NullVal(cty.Object(map[string]cty.Type{
				"str": cty.String,
			})),
		},
		{
			&Attribute{
				NestedType: &Object{
					Nesting: NestingList,
					Attributes: map[string]*Attribute{
						"str": {Type: cty.String, Required: true},
					},
				},
			},
			cty.NullVal(cty.List(
				cty.Object(map[string]cty.Type{
					"str": cty.String,
				}),
			)),
		},
		{
			&Attribute{
				NestedType: &Object{
					Nesting: NestingMap,
					Attributes: map[string]*Attribute{
						"str": {Type: cty.String, Required: true},
					},
				},
			},
			cty.NullVal(cty.Map(
				cty.Object(map[string]cty.Type{
					"str": cty.String,
				}),
			)),
		},
		{
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSet,
					Attributes: map[string]*Attribute{
						"str": {Type: cty.String, Required: true},
					},
				},
			},
			cty.NullVal(cty.Set(
				cty.Object(map[string]cty.Type{
					"str": cty.String,
				}),
			)),
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) {
			got := test.Schema.EmptyValue()
			if !test.Want.RawEquals(got) {
				t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), ctydebug.ValueString(got), ctydebug.ValueString(test.Want))
			}
		})
	}
}
