package dynblock

import (
	"strings"
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/hcl/v2/hcltest"
	"github.com/zclconf/go-cty/cty"
)

func TestExpand(t *testing.T) {
	srcBody := hcltest.MockBody(&hcl.BodyContent{
		Blocks: hcl.Blocks{
			{
				Type:        "a",
				Labels:      []string{"static0"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"val": hcltest.MockExprLiteral(cty.StringVal("static a 0")),
					}),
				}),
			},
			{
				Type: "b",
				Body: hcltest.MockBody(&hcl.BodyContent{
					Blocks: hcl.Blocks{
						{
							Type: "c",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val0": hcltest.MockExprLiteral(cty.StringVal("static c 0")),
								}),
							}),
						},
						{
							Type:        "dynamic",
							Labels:      []string{"c"},
							LabelRanges: []hcl.Range{hcl.Range{}},
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
										cty.StringVal("dynamic c 0"),
										cty.StringVal("dynamic c 1"),
									})),
									"iterator": hcltest.MockExprVariable("dyn_c"),
								}),
								Blocks: hcl.Blocks{
									{
										Type: "content",
										Body: hcltest.MockBody(&hcl.BodyContent{
											Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
												"val0": hcltest.MockExprTraversalSrc("dyn_c.value"),
											}),
										}),
									},
								},
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"a"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
							cty.StringVal("dynamic a 0"),
							cty.StringVal("dynamic a 1"),
							cty.StringVal("dynamic a 2"),
						})),
						"labels": hcltest.MockExprList([]hcl.Expression{
							hcltest.MockExprTraversalSrc("a.key"),
						}),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("a.value"),
								}),
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"b"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
							cty.StringVal("dynamic b 0"),
							cty.StringVal("dynamic b 1"),
						})),
						"iterator": hcltest.MockExprVariable("dyn_b"),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Blocks: hcl.Blocks{
									{
										Type: "c",
										Body: hcltest.MockBody(&hcl.BodyContent{
											Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
												"val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")),
												"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
											}),
										}),
									},
									{
										Type:        "dynamic",
										Labels:      []string{"c"},
										LabelRanges: []hcl.Range{hcl.Range{}},
										Body: hcltest.MockBody(&hcl.BodyContent{
											Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
												"for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
													cty.StringVal("dynamic c 2"),
													cty.StringVal("dynamic c 3"),
												})),
											}),
											Blocks: hcl.Blocks{
												{
													Type: "content",
													Body: hcltest.MockBody(&hcl.BodyContent{
														Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
															"val0": hcltest.MockExprTraversalSrc("c.value"),
															"val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
														}),
													}),
												},
											},
										}),
									},
								},
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"b"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
							"foo": cty.ListVal([]cty.Value{
								cty.StringVal("dynamic c nested 0"),
								cty.StringVal("dynamic c nested 1"),
							}),
						})),
						"iterator": hcltest.MockExprVariable("dyn_b"),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Blocks: hcl.Blocks{
									{
										Type:        "dynamic",
										Labels:      []string{"c"},
										LabelRanges: []hcl.Range{hcl.Range{}},
										Body: hcltest.MockBody(&hcl.BodyContent{
											Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
												"for_each": hcltest.MockExprTraversalSrc("dyn_b.value"),
											}),
											Blocks: hcl.Blocks{
												{
													Type: "content",
													Body: hcltest.MockBody(&hcl.BodyContent{
														Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
															"val0": hcltest.MockExprTraversalSrc("c.value"),
															"val1": hcltest.MockExprTraversalSrc("dyn_b.key"),
														}),
													}),
												},
											},
										}),
									},
								},
							}),
						},
					},
				}),
			},
			{
				Type:        "a",
				Labels:      []string{"static1"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"val": hcltest.MockExprLiteral(cty.StringVal("static a 1")),
					}),
				}),
			},
		},
	})

	dynBody := Expand(srcBody, nil)
	var remain hcl.Body

	t.Run("PartialDecode", func(t *testing.T) {
		decSpec := &hcldec.BlockMapSpec{
			TypeName:   "a",
			LabelNames: []string{"key"},
			Nested: &hcldec.AttrSpec{
				Name:     "val",
				Type:     cty.String,
				Required: true,
			},
		}

		var got cty.Value
		var diags hcl.Diagnostics
		got, remain, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics")
			for _, diag := range diags {
				t.Logf("- %s", diag)
			}
			return
		}

		want := cty.MapVal(map[string]cty.Value{
			"static0": cty.StringVal("static a 0"),
			"static1": cty.StringVal("static a 1"),
			"0":       cty.StringVal("dynamic a 0"),
			"1":       cty.StringVal("dynamic a 1"),
			"2":       cty.StringVal("dynamic a 2"),
		})

		if !got.RawEquals(want) {
			t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

	t.Run("Decode", func(t *testing.T) {
		decSpec := &hcldec.BlockListSpec{
			TypeName: "b",
			Nested: &hcldec.BlockListSpec{
				TypeName: "c",
				Nested: &hcldec.ObjectSpec{
					"val0": &hcldec.AttrSpec{
						Name: "val0",
						Type: cty.String,
					},
					"val1": &hcldec.AttrSpec{
						Name: "val1",
						Type: cty.String,
					},
				},
			},
		}

		var got cty.Value
		var diags hcl.Diagnostics
		got, diags = hcldec.Decode(remain, decSpec, nil)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics")
			for _, diag := range diags {
				t.Logf("- %s", diag)
			}
			return
		}

		want := cty.ListVal([]cty.Value{
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("static c 0"),
					"val1": cty.NullVal(cty.String),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c 0"),
					"val1": cty.NullVal(cty.String),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c 1"),
					"val1": cty.NullVal(cty.String),
				}),
			}),
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("static c 1"),
					"val1": cty.StringVal("dynamic b 0"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c 2"),
					"val1": cty.StringVal("dynamic b 0"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c 3"),
					"val1": cty.StringVal("dynamic b 0"),
				}),
			}),
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("static c 1"),
					"val1": cty.StringVal("dynamic b 1"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c 2"),
					"val1": cty.StringVal("dynamic b 1"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c 3"),
					"val1": cty.StringVal("dynamic b 1"),
				}),
			}),
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c nested 0"),
					"val1": cty.StringVal("foo"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"val0": cty.StringVal("dynamic c nested 1"),
					"val1": cty.StringVal("foo"),
				}),
			}),
		})

		if !got.RawEquals(want) {
			t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

}

func TestExpandUnknownBodies(t *testing.T) {
	srcContent := &hcl.BodyContent{
		Blocks: hcl.Blocks{
			{
				Type:        "dynamic",
				Labels:      []string{"list"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("each.value"),
								}),
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"tuple"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("each.value"),
								}),
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"set"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("each.value"),
								}),
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"map"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
						"labels": hcltest.MockExprList([]hcl.Expression{
							hcltest.MockExprLiteral(cty.StringVal("static")),
						}),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("each.value"),
								}),
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"object"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
						"labels": hcltest.MockExprList([]hcl.Expression{
							hcltest.MockExprLiteral(cty.StringVal("static")),
						}),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("each.value"),
								}),
							}),
						},
					},
				}),
			},
			{
				Type:        "dynamic",
				Labels:      []string{"invalid_list"},
				LabelRanges: []hcl.Range{hcl.Range{}},
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
						"for_each": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))),
					}),
					Blocks: hcl.Blocks{
						{
							Type: "content",
							Body: hcltest.MockBody(&hcl.BodyContent{
								Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
									"val": hcltest.MockExprTraversalSrc("each.value"),
									// unexpected attributes should still produce an error
									"invalid": hcltest.MockExprLiteral(cty.StringVal("static")),
								}),
							}),
						},
					},
				}),
			},
		},
	}

	srcBody := hcltest.MockBody(srcContent)
	dynBody := Expand(srcBody, nil)

	t.Run("DecodeList", func(t *testing.T) {
		decSpec := &hcldec.BlockListSpec{
			TypeName: "list",
			Nested: &hcldec.ObjectSpec{
				"val": &hcldec.AttrSpec{
					Name: "val",
					Type: cty.String,
				},
			},
		}

		var got cty.Value
		var diags hcl.Diagnostics

		got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics")
			for _, diag := range diags {
				t.Logf("- %s", diag)
			}
			return
		}

		want := cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
			"val": cty.String,
		})))

		if !got.RawEquals(want) {
			t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

	t.Run("DecodeTuple", func(t *testing.T) {
		decSpec := &hcldec.BlockTupleSpec{
			TypeName: "tuple",
			Nested: &hcldec.ObjectSpec{
				"val": &hcldec.AttrSpec{
					Name: "val",
					Type: cty.String,
				},
			},
		}

		var got cty.Value
		var diags hcl.Diagnostics

		got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics")
			for _, diag := range diags {
				t.Logf("- %s", diag)
			}
			return
		}

		want := cty.DynamicVal

		if !got.RawEquals(want) {
			t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

	t.Run("DecodeSet", func(t *testing.T) {
		decSpec := &hcldec.BlockSetSpec{
			TypeName: "tuple",
			Nested: &hcldec.ObjectSpec{
				"val": &hcldec.AttrSpec{
					Name: "val",
					Type: cty.String,
				},
			},
		}

		var got cty.Value
		var diags hcl.Diagnostics

		got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics")
			for _, diag := range diags {
				t.Logf("- %s", diag)
			}
			return
		}

		want := cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
			"val": cty.String,
		})))

		if !got.RawEquals(want) {
			t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

	t.Run("DecodeMap", func(t *testing.T) {
		decSpec := &hcldec.BlockMapSpec{
			TypeName:   "map",
			LabelNames: []string{"key"},
			Nested: &hcldec.ObjectSpec{
				"val": &hcldec.AttrSpec{
					Name: "val",
					Type: cty.String,
				},
			},
		}

		var got cty.Value
		var diags hcl.Diagnostics

		got, _, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics")
			for _, diag := range diags {
				t.Logf("- %s", diag)
			}
			return
		}

		want := cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
			"val": cty.String,
		})))

		if !got.RawEquals(want) {
			t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

	t.Run("DecodeInvalidList", func(t *testing.T) {
		decSpec := &hcldec.BlockListSpec{
			TypeName: "invalid_list",
			Nested: &hcldec.ObjectSpec{
				"val": &hcldec.AttrSpec{
					Name: "val",
					Type: cty.String,
				},
			},
		}

		_, _, diags := hcldec.PartialDecode(dynBody, decSpec, nil)
		if len(diags) != 1 {
			t.Error("expected 1 extraneous argument")
		}

		want := `Mock body has extraneous argument "invalid"`

		if !strings.Contains(diags.Error(), want) {
			t.Errorf("unexpected diagnostics: %v", diags)
		}
	})

}
