// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package backendbase

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hcltest"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

func TestBase_coerceError(t *testing.T) {
	// This tests that we return errors if type coersion fails.
	// This doesn't thoroughly test all cases because we're just delegating
	// to the configschema package's coersion function, which is already
	// tested in its own package.

	b := Base{
		Schema: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"foo": {
					Type:     cty.String,
					Optional: true,
				},
			},
		},
	}
	// This is a fake body just to give us something to correlate the
	// diagnostic attribute paths against so we can test that the
	// errors are properly annotated. In the real implementation
	// the command package logic would evaluate the diagnostics against
	// the real HCL body written by the end-user.
	//
	// Because we're using MockExprLiteral for the expressions here,
	// the source range for each expression is just the fake filename
	// "MockExprLiteral". If the PrepareConfig function fails to properly
	// annotate its diagnostics then the source range won't be populated
	// at all.
	body := hcltest.MockBody(&hcl.BodyContent{
		Attributes: hcl.Attributes{
			"foo": {
				Expr: hcltest.MockExprLiteral(cty.StringVal("")),
			},
		},
	})

	t.Run("error", func(t *testing.T) {
		_, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{
			// This is incorrect because the schema wants a string
			"foo": cty.MapValEmpty(cty.String),
		}))
		// We'll use the "RPC" representation of the diagnostics just because
		// it's convenient for comparing without any unusual cmp options.
		gotDiags := diags.InConfigBody(body, "").ForRPC()
		var wantDiags tfdiags.Diagnostics
		wantDiags = wantDiags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid backend configuration",
			Detail:   "The backend configuration is incorrect: .foo: string required.",
			Subject:  &hcl.Range{Filename: "MockExprLiteral"},
		})
		wantDiags = wantDiags.ForRPC()
		if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
			t.Errorf("wrong diagnostics\n%s", diff)
		}
	})
}

func TestBase_deprecatedArg(t *testing.T) {
	b := Base{
		Schema: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"not_deprecated": {
					Type:     cty.String,
					Optional: true,
				},
				"deprecated": {
					Type:       cty.String,
					Optional:   true,
					Deprecated: true,
				},
			},
			BlockTypes: map[string]*configschema.NestedBlock{
				"nested": {
					Nesting: configschema.NestingList,
					Block: configschema.Block{
						Attributes: map[string]*configschema.Attribute{
							"deprecated": {
								Type:       cty.String,
								Optional:   true,
								Deprecated: true,
							},
						},
					},
				},
			},
		},
	}
	// This is a fake body just to give us something to correlate the
	// diagnostic attribute paths against so we can test that the
	// warnings are properly annotated. In the real implementation
	// the command package logic would evaluate the diagnostics against
	// the real HCL body written by the end-user.
	//
	// Because we're using MockExprLiteral for the expressions here,
	// the source range for each expression is just the fake filename
	// "MockExprLiteral". If the PrepareConfig function fails to properly
	// annotate its diagnostics then the source range won't be populated
	// at all.
	body := hcltest.MockBody(&hcl.BodyContent{
		Attributes: hcl.Attributes{
			"deprecated": {
				Expr: hcltest.MockExprLiteral(cty.StringVal("")),
			},
		},
		Blocks: hcl.Blocks{
			{
				Type: "nested",
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcl.Attributes{
						"deprecated": {
							Expr: hcltest.MockExprLiteral(cty.StringVal("")),
						},
					},
				}),
			},
			{
				Type: "nested",
				Body: hcltest.MockBody(&hcl.BodyContent{
					Attributes: hcl.Attributes{
						"deprecated": {
							Expr: hcltest.MockExprLiteral(cty.StringVal("")),
						},
					},
				}),
			},
		},
	})

	t.Run("nothing deprecated", func(t *testing.T) {
		got, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{
			"not_deprecated": cty.StringVal("hello"),
		}))
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error())
		}
		want := cty.ObjectVal(map[string]cty.Value{
			"deprecated":     cty.NullVal(cty.String),
			"not_deprecated": cty.StringVal("hello"),
			"nested": cty.ListValEmpty(cty.Object(map[string]cty.Type{
				"deprecated": cty.String,
			})),
		})
		if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("toplevel deprecated", func(t *testing.T) {
		_, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{
			"deprecated": cty.StringVal("hello"),
		}))
		// We'll use the "RPC" representation of the diagnostics just because
		// it's convenient for comparing without any unusual cmp options.
		gotDiags := diags.InConfigBody(body, "").ForRPC()
		var wantDiags tfdiags.Diagnostics
		wantDiags = wantDiags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagWarning,
			Summary:  "Deprecated provider argument",
			Detail:   "The argument .deprecated is deprecated. Refer to the backend documentation for more information.",
			Subject:  &hcl.Range{Filename: "MockExprLiteral"},
		})
		wantDiags = wantDiags.ForRPC()
		if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
			t.Errorf("wrong diagnostics\n%s", diff)
		}
	})
	t.Run("nested deprecated", func(t *testing.T) {
		_, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{
			"nested": cty.TupleVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"deprecated": cty.StringVal("hello"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"deprecated": cty.StringVal("hello"),
				}),
			}),
		}))
		// We'll use the "RPC" representation of the diagnostics just because
		// it's convenient for comparing without any unusual cmp options.
		gotDiags := diags.InConfigBody(body, "").ForRPC()
		var wantDiags tfdiags.Diagnostics
		wantDiags = wantDiags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagWarning,
			Summary:  "Deprecated provider argument",
			Detail:   "The argument .nested[0].deprecated is deprecated. Refer to the backend documentation for more information.",
			Subject:  &hcl.Range{Filename: "MockExprLiteral"},
		})
		wantDiags = wantDiags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagWarning,
			Summary:  "Deprecated provider argument",
			Detail:   "The argument .nested[1].deprecated is deprecated. Refer to the backend documentation for more information.",
			Subject:  &hcl.Range{Filename: "MockExprLiteral"},
		})
		wantDiags = wantDiags.ForRPC()
		if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
			t.Errorf("wrong diagnostics\n%s", diff)
		}
	})
}

func TestBase_nullCrash(t *testing.T) {
	// This test ensures that we don't crash while applying defaults to
	// a null value

	b := Base{
		Schema: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"foo": {
					Type:     cty.String,
					Required: true,
				},
			},
		},
		SDKLikeDefaults: SDKLikeDefaults{
			"foo": {
				Fallback: "fallback",
			},
		},
	}

	t.Run("error", func(t *testing.T) {
		// We pass an explicit null value here to simulate an interrupt
		_, gotDiags := b.PrepareConfig(cty.NullVal(cty.Object(map[string]cty.Type{
			"foo": cty.String,
		})))
		var wantDiags tfdiags.Diagnostics
		wantDiags = wantDiags.Append(
			&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid backend configuration",
				Detail:   "The backend configuration is incorrect: attribute \"foo\" is required.",
			})
		if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
			t.Errorf("wrong diagnostics\n%s", diff)
		}
	})
}
