package configschema

import (
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/go-cty/cty"

	"github.com/hashicorp/terraform-plugin-sdk/v2/internal/tfdiags"
)

func TestCoerceValue(t *testing.T) {
	tests := map[string]struct {
		Schema    *Block
		Input     cty.Value
		WantValue cty.Value
		WantErr   string
	}{
		"empty schema and value": {
			&Block{},
			cty.EmptyObjectVal,
			cty.EmptyObjectVal,
			``,
		},
		"attribute present": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("true"),
			}),
			``,
		},
		"single block present": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingSingle,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.EmptyObjectVal,
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.EmptyObjectVal,
			}),
			``,
		},
		"single block wrong type": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingSingle,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
			}),
			cty.DynamicVal,
			`.foo: an object is required`,
		},
		"list block with one item": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingList,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
			}),
			``,
		},
		"set block with one item": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingSet,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), // can implicitly convert to set
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
			}),
			``,
		},
		"map block with one item": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingMap,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
			}),
			``,
		},
		"list block with one item having an attribute": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block: Block{
							Attributes: map[string]*Attribute{
								"bar": {
									Type:     cty.String,
									Required: true,
								},
							},
						},
						Nesting: NestingList,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"bar": cty.StringVal("hello"),
				})}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"bar": cty.StringVal("hello"),
				})}),
			}),
			``,
		},
		"list block with one item having a missing attribute": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block: Block{
							Attributes: map[string]*Attribute{
								"bar": {
									Type:     cty.String,
									Required: true,
								},
							},
						},
						Nesting: NestingList,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
			}),
			cty.DynamicVal,
			`.foo[0]: attribute "bar" is required`,
		},
		"list block with one item having an extraneous attribute": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingList,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"bar": cty.StringVal("hello"),
				})}),
			}),
			cty.DynamicVal,
			`.foo[0]: unexpected attribute "bar"`,
		},
		"missing optional attribute": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
			}),
			``,
		},
		"missing optional single block": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingSingle,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.EmptyObject),
			}),
			``,
		},
		"missing optional list block": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingList,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListValEmpty(cty.EmptyObject),
			}),
			``,
		},
		"missing optional set block": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingSet,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.SetValEmpty(cty.EmptyObject),
			}),
			``,
		},
		"missing optional map block": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:   Block{},
						Nesting: NestingMap,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapValEmpty(cty.EmptyObject),
			}),
			``,
		},
		"missing required attribute": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Required: true,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.DynamicVal,
			`attribute "foo" is required`,
		},
		"missing required single block": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:    Block{},
						Nesting:  NestingSingle,
						MinItems: 1,
						MaxItems: 1,
					},
				},
			},
			cty.EmptyObjectVal,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.EmptyObject),
			}),
			``,
		},
		"unknown nested list": {
			&Block{
				Attributes: map[string]*Attribute{
					"attr": {
						Type:     cty.String,
						Required: true,
					},
				},
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:    Block{},
						Nesting:  NestingList,
						MinItems: 2,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("test"),
				"foo":  cty.UnknownVal(cty.EmptyObject),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("test"),
				"foo":  cty.UnknownVal(cty.List(cty.EmptyObject)),
			}),
			"",
		},
		"unknowns in nested list": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block: Block{
							Attributes: map[string]*Attribute{
								"attr": {
									Type:     cty.String,
									Required: true,
								},
							},
						},
						Nesting:  NestingList,
						MinItems: 2,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"attr": cty.UnknownVal(cty.String),
					}),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"attr": cty.UnknownVal(cty.String),
					}),
				}),
			}),
			"",
		},
		"unknown nested set": {
			&Block{
				Attributes: map[string]*Attribute{
					"attr": {
						Type:     cty.String,
						Required: true,
					},
				},
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:    Block{},
						Nesting:  NestingSet,
						MinItems: 1,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("test"),
				"foo":  cty.UnknownVal(cty.EmptyObject),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("test"),
				"foo":  cty.UnknownVal(cty.Set(cty.EmptyObject)),
			}),
			"",
		},
		"unknown nested map": {
			&Block{
				Attributes: map[string]*Attribute{
					"attr": {
						Type:     cty.String,
						Required: true,
					},
				},
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Block:    Block{},
						Nesting:  NestingMap,
						MinItems: 1,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("test"),
				"foo":  cty.UnknownVal(cty.Map(cty.String)),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("test"),
				"foo":  cty.UnknownVal(cty.Map(cty.EmptyObject)),
			}),
			"",
		},
		"extraneous attribute": {
			&Block{},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			cty.DynamicVal,
			`unexpected attribute "foo"`,
		},
		"wrong attribute type": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.Number,
						Required: true,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.False,
			}),
			cty.DynamicVal,
			`.foo: number required`,
		},
		"unset computed value": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
						Computed: true,
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
			}),
			``,
		},
		"dynamic value attributes": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Nesting: NestingMap,
						Block: Block{
							Attributes: map[string]*Attribute{
								"bar": {
									Type:     cty.String,
									Optional: true,
									Computed: true,
								},
								"baz": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
									Computed: true,
								},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ObjectVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("beep"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
						"baz": cty.NumberIntVal(8),
					}),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ObjectVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("beep"),
						"baz": cty.NullVal(cty.DynamicPseudoType),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
						"baz": cty.NumberIntVal(8),
					}),
				}),
			}),
			``,
		},
		"dynamic attributes in map": {
			// Convert a block represented as a map to an object if a
			// DynamicPseudoType causes the element types to mismatch.
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Nesting: NestingMap,
						Block: Block{
							Attributes: map[string]*Attribute{
								"bar": {
									Type:     cty.String,
									Optional: true,
									Computed: true,
								},
								"baz": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
									Computed: true,
								},
							},
						},
					},
				},
			},
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("beep"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
					}),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ObjectVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("beep"),
						"baz": cty.NullVal(cty.DynamicPseudoType),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
						"baz": cty.NullVal(cty.DynamicPseudoType),
					}),
				}),
			}),
			``,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			gotValue, gotErrObj := test.Schema.CoerceValue(test.Input)

			if gotErrObj == nil {
				if test.WantErr != "" {
					t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
				}
			} else {
				gotErr := tfdiags.FormatError(gotErrObj)
				if gotErr != test.WantErr {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", gotErr, test.WantErr)
				}
				return
			}

			if !gotValue.RawEquals(test.WantValue) {
				t.Errorf("wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v", test.Input, gotValue, test.WantValue)
			}

			// The coerced value must always conform to the implied type of
			// the schema.
			wantTy := test.Schema.ImpliedType()
			gotTy := gotValue.Type()
			if errs := gotTy.TestConformance(wantTy); len(errs) > 0 {
				t.Errorf("empty value has incorrect type\ngot: %#v\nwant: %#v\nerrors: %s", gotTy, wantTy, spew.Sdump(errs))
			}
		})
	}
}
