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

package hclsyntax

import (
	"strings"
	"testing"

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

func TestTemplateExprParseAndValue(t *testing.T) {
	// This is a combo test that exercises both the parser and the Value
	// method, with the focus on the latter but indirectly testing the former.
	tests := []struct {
		input     string
		ctx       *hcl.EvalContext
		want      cty.Value
		diagCount int
	}{
		{
			`1`,
			nil,
			cty.StringVal("1"),
			0,
		},
		{
			`(1)`,
			nil,
			cty.StringVal("(1)"),
			0,
		},
		{
			`true`,
			nil,
			cty.StringVal("true"),
			0,
		},
		{
			`
hello world
`,
			nil,
			cty.StringVal("\nhello world\n"),
			0,
		},
		{
			`hello ${"world"}`,
			nil,
			cty.StringVal("hello world"),
			0,
		},
		{
			`hello\nworld`, // backslash escapes not supported in bare templates
			nil,
			cty.StringVal("hello\\nworld"),
			0,
		},
		{
			`hello ${12.5}`,
			nil,
			cty.StringVal("hello 12.5"),
			0,
		},
		{
			`silly ${"${"nesting"}"}`,
			nil,
			cty.StringVal("silly nesting"),
			0,
		},
		{
			`silly ${"${true}"}`,
			nil,
			cty.StringVal("silly true"),
			0,
		},
		{
			`hello $${escaped}`,
			nil,
			cty.StringVal("hello ${escaped}"),
			0,
		},
		{
			`hello $$nonescape`,
			nil,
			cty.StringVal("hello $$nonescape"),
			0,
		},
		{
			`hello %${"world"}`,
			nil,
			cty.StringVal("hello %world"),
			0,
		},
		{
			`${true}`,
			nil,
			cty.True, // any single expression is unwrapped without stringification
			0,
		},
		{
			`trim ${~ "trim"}`,
			nil,
			cty.StringVal("trimtrim"),
			0,
		},
		{
			`${"trim" ~} trim`,
			nil,
			cty.StringVal("trimtrim"),
			0,
		},
		{
			`trim
${~"trim"~}
trim`,
			nil,
			cty.StringVal("trimtrimtrim"),
			0,
		},
		{
			` ${~ true ~} `,
			nil,
			cty.StringVal("true"), // can't trim space to reduce to a single expression
			0,
		},
		{
			`${"hello "}${~"trim"~}${" hello"}`,
			nil,
			cty.StringVal("hello trim hello"), // trimming can't reach into a neighboring interpolation
			0,
		},
		{
			`${true}${~"trim"~}${true}`,
			nil,
			cty.StringVal("truetrimtrue"), // trimming is no-op of neighbors aren't literal strings
			0,
		},

		{
			`%{ if true ~} hello %{~ endif }`,
			nil,
			cty.StringVal("hello"),
			0,
		},
		{
			`%{ if false ~} hello %{~ endif}`,
			nil,
			cty.StringVal(""),
			0,
		},
		{
			`%{ if true ~} hello %{~ else ~} goodbye %{~ endif }`,
			nil,
			cty.StringVal("hello"),
			0,
		},
		{
			`%{ if false ~} hello %{~ else ~} goodbye %{~ endif }`,
			nil,
			cty.StringVal("goodbye"),
			0,
		},
		{
			`%{ if true ~} %{~ if false ~} hello %{~ else ~} goodbye %{~ endif ~} %{~ endif }`,
			nil,
			cty.StringVal("goodbye"),
			0,
		},
		{
			`%{ if false ~} %{~ if false ~} hello %{~ else ~} goodbye %{~ endif ~} %{~ endif }`,
			nil,
			cty.StringVal(""),
			0,
		},
		{
			`%{ of true ~} hello %{~ endif}`,
			nil,
			cty.UnknownVal(cty.String).RefineNotNull(),
			2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected
		},
		{
			`%{ for v in ["a", "b", "c"] }${v}%{ endfor }`,
			nil,
			cty.StringVal("abc"),
			0,
		},
		{
			`%{ for v in ["a", "b", "c"] } ${v} %{ endfor }`,
			nil,
			cty.StringVal(" a  b  c "),
			0,
		},
		{
			`%{ for v in ["a", "b", "c"] ~} ${v} %{~ endfor }`,
			nil,
			cty.StringVal("abc"),
			0,
		},
		{
			`%{ for v in [] }${v}%{ endfor }`,
			nil,
			cty.StringVal(""),
			0,
		},
		{
			`%{ for i, v in ["a", "b", "c"] }${i}${v}%{ endfor }`,
			nil,
			cty.StringVal("0a1b2c"),
			0,
		},
		{
			`%{ for k, v in {"A" = "a", "B" = "b", "C" = "c"} }${k}${v}%{ endfor }`,
			nil,
			cty.StringVal("AaBbCc"),
			0,
		},
		{
			`%{ for v in ["a", "b", "c"] }${v}${nl}%{ endfor }`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"nl": cty.StringVal("\n"),
				},
			},
			cty.StringVal("a\nb\nc\n"),
			0,
		},
		{
			`\n`, // backslash escapes are not interpreted in template literals
			nil,
			cty.StringVal("\\n"),
			0,
		},
		{
			`\uu1234`, // backslash escapes are not interpreted in template literals
			nil,       // (this is intentionally an invalid one to ensure we don't produce an error)
			cty.StringVal("\\uu1234"),
			0,
		},
		{
			`$`,
			nil,
			cty.StringVal("$"),
			0,
		},
		{
			`$$`,
			nil,
			cty.StringVal("$$"),
			0,
		},
		{
			`%`,
			nil,
			cty.StringVal("%"),
			0,
		},
		{
			`%%`,
			nil,
			cty.StringVal("%%"),
			0,
		},
		{
			`hello %%{ if true }world%%{ endif }`,
			nil,
			cty.StringVal(`hello %{ if true }world%{ endif }`),
			0,
		},
		{
			`hello $%{ if true }world%{ endif }`,
			nil,
			cty.StringVal("hello $world"),
			0,
		},
		{
			`%{ endif }`,
			nil,
			cty.UnknownVal(cty.String).RefineNotNull(),
			1, // Unexpected endif directive
		},
		{
			`%{ endfor }`,
			nil,
			cty.UnknownVal(cty.String).RefineNotNull(),
			1, // Unexpected endfor directive
		},
		{ // can preserve a static prefix as a refinement of an unknown result
			`test_${unknown}`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"unknown": cty.UnknownVal(cty.String),
				},
			},
			cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_").NewValue(),
			0,
		},
		{ // can preserve a dynamic known prefix as a refinement of an unknown result
			`test_${known}_${unknown}`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"known":   cty.StringVal("known"),
					"unknown": cty.UnknownVal(cty.String),
				},
			},
			cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull("test_known_").NewValue(),
			0,
		},
		{ // can preserve a static prefix as a refinement, but the length is limited to 128 B
			strings.Repeat("_", 130) + `${unknown}`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"unknown": cty.UnknownVal(cty.String),
				},
			},
			cty.UnknownVal(cty.String).Refine().NotNull().StringPrefixFull(strings.Repeat("_", 128)).NewValue(),
			0,
		},
		{ // all marks are passed through to ensure result is always consistent
			`hello%{ if false } ${target}%{ endif }`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"target": cty.StringVal("world").Mark("sensitive"),
				},
			},
			cty.StringVal("hello").Mark("sensitive"),
			0,
		},
		{ // marks from interpolated values are passed through
			`${greeting} ${target}`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"greeting": cty.StringVal("hello").Mark("english"),
					"target":   cty.StringVal("world").Mark("sensitive"),
				},
			},
			cty.StringVal("hello world").WithMarks(cty.NewValueMarks("english", "sensitive")),
			0,
		},
		{ // can use marks by traversing complex values
			`Authenticate with "${secrets.passphrase}"`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"secrets": cty.MapVal(map[string]cty.Value{
						"passphrase": cty.StringVal("my voice is my passport").Mark("sensitive"),
					}).Mark("sensitive"),
				},
			},
			cty.StringVal(`Authenticate with "my voice is my passport"`).WithMarks(cty.NewValueMarks("sensitive")),
			0,
		},
		{ // can loop over marked collections
			`%{ for s in secrets }${s}%{ endfor }`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"secrets": cty.ListVal([]cty.Value{
						cty.StringVal("foo"),
						cty.StringVal("bar"),
						cty.StringVal("baz"),
					}).Mark("sensitive"),
				},
			},
			cty.StringVal("foobarbaz").Mark("sensitive"),
			0,
		},
		{ // marks on individual elements propagate to the result
			`%{ for s in secrets }${s}%{ endfor }`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"secrets": cty.ListVal([]cty.Value{
						cty.StringVal("foo"),
						cty.StringVal("bar").Mark("sensitive"),
						cty.StringVal("baz"),
					}),
				},
			},
			cty.StringVal("foobarbaz").Mark("sensitive"),
			0,
		},
		{ // lots of marks!
			`%{ for s in secrets }${s}%{ endfor }`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"secrets": cty.ListVal([]cty.Value{
						cty.StringVal("foo").Mark("x"),
						cty.StringVal("bar").Mark("y"),
						cty.StringVal("baz").Mark("z"),
					}).Mark("x"), // second instance of x
				},
			},
			cty.StringVal("foobarbaz").WithMarks(cty.NewValueMarks("x", "y", "z")),
			0,
		},
		{ // marks from unknown values are maintained
			`test_${target}`,
			&hcl.EvalContext{
				Variables: map[string]cty.Value{
					"target": cty.UnknownVal(cty.String).Mark("sensitive"),
				},
			},
			cty.UnknownVal(cty.String).Mark("sensitive").Refine().NotNull().StringPrefixFull("test_").NewValue(),
			0,
		},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			expr, parseDiags := ParseTemplate([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})

			// We'll skip evaluating if there were parse errors because it
			// isn't reasonable to evaluate a syntactically-invalid template;
			// it'll produce strange results that we don't care about.
			got := test.want
			var valDiags hcl.Diagnostics
			if !parseDiags.HasErrors() {
				got, valDiags = expr.Value(test.ctx)
			}

			diagCount := len(parseDiags) + len(valDiags)

			if diagCount != test.diagCount {
				t.Errorf("wrong number of diagnostics %d; want %d", diagCount, test.diagCount)
				for _, diag := range parseDiags {
					t.Logf(" - %s", diag.Error())
				}
				for _, diag := range valDiags {
					t.Logf(" - %s", diag.Error())
				}
			}

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

}

func TestTemplateExprGracefulValue(t *testing.T) {
	// we don't care about diags since we know it's invalid config
	expr, _ := ParseTemplate([]byte(`prefix${provider::}`), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})

	got, _ := expr.Value(nil) // this should not panic

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

func TestTemplateExprWrappedGracefulValue(t *testing.T) {
	// we don't care about diags since we know it's invalid config
	expr, _ := ParseTemplate([]byte(`${provider::}`), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})

	got, _ := expr.Value(nil) // this should not panic

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

func TestTemplateExprIsStringLiteral(t *testing.T) {
	tests := map[string]bool{
		// A simple string value is a string literal
		"a": true,

		// Strings containing escape characters or escape sequences are
		// tokenized into multiple string literals, but this should be
		// corrected by the parser
		"a$b":        true,
		"a%%b":       true,
		"a\nb":       true,
		"a$${\"b\"}": true,

		// Wrapped values (HIL-like) are not treated as string literals for
		// legacy reasons
		"${1}":     false,
		"${\"b\"}": false,

		// Even template expressions containing only literal values do not
		// count as string literals
		"a${1}":     false,
		"a${\"b\"}": false,
	}
	for input, want := range tests {
		t.Run(input, func(t *testing.T) {
			expr, diags := ParseTemplate([]byte(input), "", hcl.InitialPos)
			if len(diags) != 0 {
				t.Fatalf("unexpected diags: %s", diags.Error())
			}

			if tmplExpr, ok := expr.(*TemplateExpr); ok {
				got := tmplExpr.IsStringLiteral()

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