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

package blocktoattr

import (
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/ext/dynblock"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	hcljson "github.com/hashicorp/hcl/v2/json"
	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/zclconf/go-cty/cty"
)

func TestFixUpBlockAttrs(t *testing.T) {
	fooSchema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"foo": {
				Type: cty.List(cty.Object(map[string]cty.Type{
					"bar": cty.String,
				})),
				Optional: true,
			},
		},
	}

	tests := map[string]struct {
		src      string
		json     bool
		schema   *configschema.Block
		want     cty.Value
		wantErrs bool
	}{
		"empty": {
			src:    ``,
			schema: &configschema.Block{},
			want:   cty.EmptyObjectVal,
		},
		"empty JSON": {
			src:    `{}`,
			json:   true,
			schema: &configschema.Block{},
			want:   cty.EmptyObjectVal,
		},
		"unset": {
			src:    ``,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(fooSchema.Attributes["foo"].Type),
			}),
		},
		"unset JSON": {
			src:    `{}`,
			json:   true,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(fooSchema.Attributes["foo"].Type),
			}),
		},
		"no fixup required, with one value": {
			src: `
foo = [
  {
    bar = "baz"
  },
]
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz"),
					}),
				}),
			}),
		},
		"no fixup required, with two values": {
			src: `
foo = [
  {
    bar = "baz"
  },
  {
    bar = "boop"
  },
]
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
					}),
				}),
			}),
		},
		"no fixup required, with values, JSON": {
			src:    `{"foo": [{"bar": "baz"}]}`,
			json:   true,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz"),
					}),
				}),
			}),
		},
		"no fixup required, empty": {
			src: `
foo = []
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()),
			}),
		},
		"no fixup required, empty, JSON": {
			src:    `{"foo":[]}`,
			json:   true,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()),
			}),
		},
		"fixup one block": {
			src: `
foo {
  bar = "baz"
}
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz"),
					}),
				}),
			}),
		},
		"fixup one block omitting attribute": {
			src: `
foo {}
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.NullVal(cty.String),
					}),
				}),
			}),
		},
		"fixup two blocks": {
			src: `
foo {
  bar = baz
}
foo {
  bar = "boop"
}
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz value"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
					}),
				}),
			}),
		},
		"interaction with dynamic block generation": {
			src: `
dynamic "foo" {
  for_each = ["baz", beep]
  content {
    bar = foo.value
  }
}
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("beep value"),
					}),
				}),
			}),
		},
		"dynamic block with empty iterator": {
			src: `
dynamic "foo" {
  for_each = []
  content {
    bar = foo.value
  }
}
`,
			schema: fooSchema,
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(fooSchema.Attributes["foo"].Type),
			}),
		},
		"both attribute and block syntax": {
			src: `
foo = []
foo {
  bar = "baz"
}
`,
			schema:   fooSchema,
			wantErrs: true, // Unsupported block type (user must be consistent about whether they consider foo to be a block type or an attribute)
			want: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("baz"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"bar": cty.StringVal("boop"),
					}),
				}),
			}),
		},
		"fixup inside block": {
			src: `
container {
  foo {
    bar = "baz"
  }
  foo {
    bar = "boop"
  }
}
container {
  foo {
    bar = beep
  }
}
`,
			schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"container": {
						Nesting: configschema.NestingList,
						Block:   *fooSchema,
					},
				},
			},
			want: cty.ObjectVal(map[string]cty.Value{
				"container": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("baz"),
							}),
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("boop"),
							}),
						}),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("beep value"),
							}),
						}),
					}),
				}),
			}),
		},
		"fixup inside attribute-as-block": {
			src: `
container {
  foo {
    bar = "baz"
  }
  foo {
    bar = "boop"
  }
}
container {
  foo {
    bar = beep
  }
}
`,
			schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"container": {
						Type: cty.List(cty.Object(map[string]cty.Type{
							"foo": cty.List(cty.Object(map[string]cty.Type{
								"bar": cty.String,
							})),
						})),
						Optional: true,
					},
				},
			},
			want: cty.ObjectVal(map[string]cty.Value{
				"container": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("baz"),
							}),
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("boop"),
							}),
						}),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("beep value"),
							}),
						}),
					}),
				}),
			}),
		},
		"nested fixup with dynamic block generation": {
			src: `
container {
  dynamic "foo" {
    for_each = ["baz", beep]
    content {
      bar = foo.value
    }
  }
}
`,
			schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"container": {
						Nesting: configschema.NestingList,
						Block:   *fooSchema,
					},
				},
			},
			want: cty.ObjectVal(map[string]cty.Value{
				"container": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("baz"),
							}),
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("beep value"),
							}),
						}),
					}),
				}),
			}),
		},

		"missing nested block items": {
			src: `
container {
  foo {
    bar = "one"
  }
}
`,
			schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"container": {
						Nesting:  configschema.NestingList,
						MinItems: 2,
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"foo": {
									Type: cty.List(cty.Object(map[string]cty.Type{
										"bar": cty.String,
									})),
									Optional: true,
								},
							},
						},
					},
				},
			},
			want: cty.ObjectVal(map[string]cty.Value{
				"container": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"bar": cty.StringVal("baz"),
							}),
						}),
					}),
				}),
			}),
			wantErrs: true,
		},
		"no fixup allowed with NestedType": {
			src: `
 container {
   foo = "one"
 }
 `,
			schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"container": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingList,
							Attributes: map[string]*configschema.Attribute{
								"foo": {
									Type: cty.String,
								},
							},
						},
					},
				},
			},
			want: cty.ObjectVal(map[string]cty.Value{
				"container": cty.NullVal(cty.List(
					cty.Object(map[string]cty.Type{
						"foo": cty.String,
					}),
				)),
			}),
			wantErrs: true,
		},
		"no fixup allowed new types": {
			src: `
 container {
   foo = "one"
 }
 `,
			schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					// This could be a ConfigModeAttr fixup
					"container": {
						Type: cty.List(cty.Object(map[string]cty.Type{
							"foo": cty.String,
						})),
					},
					// But the presence of this type means it must have been
					// declared by a new SDK
					"new_type": {
						Type: cty.Object(map[string]cty.Type{
							"boo": cty.String,
						}),
					},
				},
			},
			want: cty.ObjectVal(map[string]cty.Value{
				"container": cty.NullVal(cty.List(
					cty.Object(map[string]cty.Type{
						"foo": cty.String,
					}),
				)),
			}),
			wantErrs: true,
		},
	}

	ctx := &hcl.EvalContext{
		Variables: map[string]cty.Value{
			"bar":  cty.StringVal("bar value"),
			"baz":  cty.StringVal("baz value"),
			"beep": cty.StringVal("beep value"),
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			var f *hcl.File
			var diags hcl.Diagnostics
			if test.json {
				f, diags = hcljson.Parse([]byte(test.src), "test.tf.json")
			} else {
				f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1})
			}
			if diags.HasErrors() {
				for _, diag := range diags {
					t.Errorf("unexpected diagnostic: %s", diag)
				}
				t.FailNow()
			}

			// We'll expand dynamic blocks in the body first, to mimic how
			// we process this fixup when using the main "lang" package API.
			spec := test.schema.DecoderSpec()
			body := dynblock.Expand(f.Body, ctx)

			body = FixUpBlockAttrs(body, test.schema)
			got, diags := hcldec.Decode(body, spec, ctx)

			if test.wantErrs {
				if !diags.HasErrors() {
					t.Errorf("succeeded, but want error\ngot: %#v", got)
				}

				// check that our wrapped body returns the correct context by
				// verifying the Subject is valid.
				for _, d := range diags {
					if d.Subject.Filename == "" {
						t.Errorf("empty diagnostic subject: %#v", d.Subject)
					}
				}
				return
			}

			if !test.want.RawEquals(got) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
			}
			for _, diag := range diags {
				t.Errorf("unexpected diagnostic: %s", diag)
			}
		})
	}
}
