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

package funcs

import (
	"fmt"
	"strings"
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/ext/customdecode"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"

	"github.com/hashicorp/terraform/internal/collections"
)

func TestReplace(t *testing.T) {
	tests := []struct {
		String  cty.Value
		Substr  cty.Value
		Replace cty.Value
		Want    cty.Value
		Err     bool
	}{
		{ // Regular search and replace
			cty.StringVal("hello"),
			cty.StringVal("hel"),
			cty.StringVal("bel"),
			cty.StringVal("bello"),
			false,
		},
		{ // Search string doesn't match
			cty.StringVal("hello"),
			cty.StringVal("nope"),
			cty.StringVal("bel"),
			cty.StringVal("hello"),
			false,
		},
		{ // Regular expression
			cty.StringVal("hello"),
			cty.StringVal("/l/"),
			cty.StringVal("L"),
			cty.StringVal("heLLo"),
			false,
		},
		{
			cty.StringVal("helo"),
			cty.StringVal("/(l)/"),
			cty.StringVal("$1$1"),
			cty.StringVal("hello"),
			false,
		},
		{ // Bad regexp
			cty.StringVal("hello"),
			cty.StringVal("/(l/"),
			cty.StringVal("$1$1"),
			cty.UnknownVal(cty.String),
			true,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("replace(%#v, %#v, %#v)", test.String, test.Substr, test.Replace), func(t *testing.T) {
			got, err := Replace(test.String, test.Substr, test.Replace)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

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

func TestStrContains(t *testing.T) {
	tests := []struct {
		String cty.Value
		Substr cty.Value
		Want   cty.Value
		Err    bool
	}{
		{
			cty.StringVal("hello"),
			cty.StringVal("hel"),
			cty.BoolVal(true),
			false,
		},
		{
			cty.StringVal("hello"),
			cty.StringVal("lo"),
			cty.BoolVal(true),
			false,
		},
		{
			cty.StringVal("hello1"),
			cty.StringVal("1"),
			cty.BoolVal(true),
			false,
		},
		{
			cty.StringVal("hello1"),
			cty.StringVal("heo"),
			cty.BoolVal(false),
			false,
		},
		{
			cty.StringVal("hello1"),
			cty.NumberIntVal(1),
			cty.UnknownVal(cty.Bool),
			true,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("includes(%#v, %#v)", test.String, test.Substr), func(t *testing.T) {
			got, err := StrContains(test.String, test.Substr)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

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

func TestStartsWith(t *testing.T) {
	tests := []struct {
		String, Prefix cty.Value
		Want           cty.Value
		WantError      string
	}{
		{
			cty.StringVal("hello world"),
			cty.StringVal("hello"),
			cty.True,
			``,
		},
		{
			cty.StringVal("hey world"),
			cty.StringVal("hello"),
			cty.False,
			``,
		},
		{
			cty.StringVal(""),
			cty.StringVal(""),
			cty.True,
			``,
		},
		{
			cty.StringVal("a"),
			cty.StringVal(""),
			cty.True,
			``,
		},
		{
			cty.StringVal(""),
			cty.StringVal("a"),
			cty.False,
			``,
		},
		{
			cty.UnknownVal(cty.String),
			cty.StringVal("a"),
			cty.UnknownVal(cty.Bool).RefineNotNull(),
			``,
		},
		{
			cty.UnknownVal(cty.String),
			cty.StringVal(""),
			cty.True,
			``,
		},
		{
			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
			cty.StringVal(""),
			cty.True,
			``,
		},
		{
			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
			cty.StringVal("a"),
			cty.False,
			``,
		},
		{
			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
			cty.StringVal("ht"),
			cty.True,
			``,
		},
		{
			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
			cty.StringVal("https:"),
			cty.True,
			``,
		},
		{
			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
			cty.StringVal("https-"),
			cty.False,
			``,
		},
		{
			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
			cty.StringVal("https://"),
			cty.UnknownVal(cty.Bool).RefineNotNull(),
			``,
		},
		{
			// Unicode combining characters edge-case: we match the prefix
			// in terms of unicode code units rather than grapheme clusters,
			// which is inconsistent with our string processing elsewhere but
			// would be a breaking change to fix that bug now.
			cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier
			cty.StringVal("\U0001f937"),             // Just the "Person Shrugging" character without any modifiers
			cty.True,
			``,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) {
			got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix})

			if test.WantError != "" {
				gotErr := fmt.Sprintf("%s", err)
				if gotErr != test.WantError {
					t.Errorf("wrong error\ngot:  %s\nwant: %s", gotErr, test.WantError)
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf(
					"wrong result\nstring: %#v\nprefix: %#v\ngot:    %#v\nwant:   %#v",
					test.String, test.Prefix, got, test.Want,
				)
			}
		})
	}
}

func TestTemplateString(t *testing.T) {
	// This function has some special restrictions on what syntax is valid
	// in its first argument, so we'll test this one using HCL expressions
	// as the inputs, rather than direct cty values as we do for most other
	// functions in this package.
	tests := []struct {
		templateExpr string
		exprScope    map[string]cty.Value
		vars         cty.Value
		want         cty.Value
		wantErr      string
	}{
		{ // a single string interpolation that evaluates to null should fail
			`template`,
			map[string]cty.Value{
				"template": cty.StringVal(`${test}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"test": cty.NullVal(cty.String),
			}),
			cty.NilVal,
			`<templatestring argument>:1,1-8: Template result is null; The result of the template is null, which is not a valid result for a templatestring call.`,
		},
		{ // a single string interpolation that evaluates to unknown should not fail
			`template`,
			map[string]cty.Value{
				"template": cty.StringVal(`${test}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"test": cty.UnknownVal(cty.String),
			}),
			cty.UnknownVal(cty.String).RefineNotNull(),
			``,
		},
		{
			`template`,
			map[string]cty.Value{
				"template": cty.StringVal(`it's ${a}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("a value"),
			}),
			cty.StringVal(`it's a value`),
			``,
		},
		{
			`template`,
			map[string]cty.Value{
				"template": cty.StringVal(`${a}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.True,
			}),
			// The special treatment of a template with only a single
			// interpolation sequence does not apply to templatestring, because
			// we're expecting to be evaluating templates fetched dynamically
			// from somewhere else and want to avoid callers needing to deal
			// with anything other than string results.
			cty.StringVal(`true`),
			``,
		},
		{
			`template`,
			map[string]cty.Value{
				"template": cty.StringVal(`${a}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.EmptyTupleVal,
			}),
			// The special treatment of a template with only a single
			// interpolation sequence does not apply to templatestring, because
			// we're expecting to be evaluating templates fetched dynamically
			// from somewhere else and want to avoid callers needing to deal
			// with anything other than string results.
			cty.NilVal,
			`invalid template result: string required`,
		},
		{
			`data.whatever.whatever["foo"].result`,
			map[string]cty.Value{
				"data": cty.ObjectVal(map[string]cty.Value{
					"whatever": cty.ObjectVal(map[string]cty.Value{
						"whatever": cty.MapVal(map[string]cty.Value{
							"foo": cty.ObjectVal(map[string]cty.Value{
								"result": cty.StringVal("it's ${a}"),
							}),
						}),
					}),
				}),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("a value"),
			}),
			cty.StringVal(`it's a value`),
			``,
		},
		{
			`data.whatever.whatever[each.key].result`,
			map[string]cty.Value{
				"data": cty.ObjectVal(map[string]cty.Value{
					"whatever": cty.ObjectVal(map[string]cty.Value{
						"whatever": cty.MapVal(map[string]cty.Value{
							"foo": cty.ObjectVal(map[string]cty.Value{
								"result": cty.StringVal("it's ${a}"),
							}),
						}),
					}),
				}),
				"each": cty.ObjectVal(map[string]cty.Value{
					"key": cty.StringVal("foo"),
				}),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("a value"),
			}),
			cty.StringVal(`it's a value`),
			``,
		},
		{
			`data.whatever.whatever[*].result`,
			map[string]cty.Value{
				"data": cty.ObjectVal(map[string]cty.Value{
					"whatever": cty.ObjectVal(map[string]cty.Value{
						"whatever": cty.TupleVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"result": cty.StringVal("it's ${a}"),
							}),
						}),
					}),
				}),
				"each": cty.ObjectVal(map[string]cty.Value{
					"key": cty.StringVal("foo"),
				}),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("a value"),
			}),
			cty.NilVal,
			// We have an intentional hole in our heuristic for whether the
			// first argument is a suitable expression which permits splat
			// expressions just so that we can return the type mismatch error
			// from the result not being a string, instead of the more general
			// error about it not being a supported expression type.
			`invalid template value: a string is required`,
		},
		{
			`"can't write $${not_allowed}"`,
			map[string]cty.Value{},
			cty.ObjectVal(map[string]cty.Value{
				"not_allowed": cty.StringVal("a literal template"),
			}),
			cty.NilVal,
			`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`,
		},
		{
			`"can't write ${not_allowed}"`,
			map[string]cty.Value{},
			cty.ObjectVal(map[string]cty.Value{
				"not_allowed": cty.StringVal("a literal template"),
			}),
			cty.NilVal,
			`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`,
		},
		{
			`"can't write %%{for x in things}a literal template%%{endfor}"`,
			map[string]cty.Value{},
			cty.ObjectVal(map[string]cty.Value{
				"things": cty.ListVal([]cty.Value{cty.True}),
			}),
			cty.NilVal,
			`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`,
		},
		{
			`"can't write %{for x in things}a literal template%{endfor}"`,
			map[string]cty.Value{},
			cty.ObjectVal(map[string]cty.Value{
				"things": cty.ListVal([]cty.Value{cty.True}),
			}),
			cty.NilVal,
			`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`,
		},
		{
			`"${not_allowed}"`,
			map[string]cty.Value{},
			cty.ObjectVal(map[string]cty.Value{
				"not allowed": cty.StringVal("an interp-only template"),
			}),
			cty.NilVal,
			`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to treat the inner expression as template syntax, write the reference expression directly without any template interpolation syntax`,
		},
		{
			`1 + 1`,
			map[string]cty.Value{},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			`invalid template expression: must be a direct reference to a single string from elsewhere, containing valid Terraform template syntax`,
		},
		{
			`not_a_string`,
			map[string]cty.Value{
				"not_a_string": cty.True,
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			`invalid template value: a string is required`,
		},
		{
			`with_lower`,
			map[string]cty.Value{
				"with_lower": cty.StringVal(`it's ${lower(a)}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("A VALUE"),
			}),
			cty.StringVal("it's a value"),
			``,
		},
		{
			`with_core_lower`,
			map[string]cty.Value{
				"with_core_lower": cty.StringVal(`it's ${core::lower(a)}`),
			},
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("A VALUE"),
			}),
			cty.StringVal("it's a value"),
			``,
		},
		{
			`with_fsfunc`,
			map[string]cty.Value{
				"with_fsfunc": cty.StringVal(`it's ${fsfunc()}`),
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			`<templatestring argument>:1,8-15: Error in function call; Call to function "fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`,
		},
		{
			`with_core_fsfunc`,
			map[string]cty.Value{
				"with_core_fsfunc": cty.StringVal(`it's ${core::fsfunc()}`),
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			`<templatestring argument>:1,8-21: Error in function call; Call to function "core::fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`,
		},
		{
			`with_templatefunc`,
			map[string]cty.Value{
				"with_templatefunc": cty.StringVal(`it's ${templatefunc()}`),
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			`<templatestring argument>:1,8-21: Error in function call; Call to function "templatefunc" failed: cannot recursively call templatefunc from inside another template function.`,
		},
		{
			`with_core_templatefunc`,
			map[string]cty.Value{
				"with_core_templatefunc": cty.StringVal(`it's ${core::templatefunc()}`),
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			`<templatestring argument>:1,8-27: Error in function call; Call to function "core::templatefunc" failed: cannot recursively call templatefunc from inside another template function.`,
		},
		{
			`with_fstemplatefunc`,
			map[string]cty.Value{
				"with_fstemplatefunc": cty.StringVal(`it's ${fstemplatefunc()}`),
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			// The template function error takes priority over the filesystem
			// function error if calling a function that's in both categories.
			`<templatestring argument>:1,8-23: Error in function call; Call to function "fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`,
		},
		{
			`with_core_fstemplatefunc`,
			map[string]cty.Value{
				"with_core_fstemplatefunc": cty.StringVal(`it's ${core::fstemplatefunc()}`),
			},
			cty.ObjectVal(map[string]cty.Value{}),
			cty.NilVal,
			// The template function error takes priority over the filesystem
			// function error if calling a function that's in both categories.
			`<templatestring argument>:1,8-29: Error in function call; Call to function "core::fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`,
		},
	}

	funcToTest := MakeTemplateStringFunc(func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]) {
		// These are the functions available for use inside the nested template
		// evaluation context. These are here only to test that we can call
		// functions and that the template/filesystem functions get blocked
		// with suitable error messages. This is not a realistic set of
		// functions that would be available in a real call.
		funcs = map[string]function.Function{
			"lower": function.New(&function.Spec{
				Params: []function.Parameter{
					{
						Name: "str",
						Type: cty.String,
					},
				},
				Type: function.StaticReturnType(cty.String),
				Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
					s := args[0].AsString()
					return cty.StringVal(strings.ToLower(s)), nil
				},
			}),
			"fsfunc": function.New(&function.Spec{
				Type: function.StaticReturnType(cty.String),
				Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
					return cty.UnknownVal(retType), fmt.Errorf("should not be able to call fsfunc")
				},
			}),
			"templatefunc": function.New(&function.Spec{
				Type: function.StaticReturnType(cty.String),
				Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
					return cty.UnknownVal(retType), fmt.Errorf("should not be able to call templatefunc")
				},
			}),
			"fstemplatefunc": function.New(&function.Spec{
				Type: function.StaticReturnType(cty.String),
				Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
					return cty.UnknownVal(retType), fmt.Errorf("should not be able to call fstemplatefunc")
				},
			}),
		}
		funcs["core::lower"] = funcs["lower"]
		funcs["core::fsfunc"] = funcs["fsfunc"]
		funcs["core::templatefunc"] = funcs["templatefunc"]
		funcs["core::fstemplatefunc"] = funcs["fstemplatefunc"]
		return funcs, collections.NewSetCmp("fsfunc", "fstemplatefunc"), collections.NewSetCmp("templatefunc", "fstemplatefunc")
	})

	for _, test := range tests {
		t.Run(test.templateExpr, func(t *testing.T) {
			// The following mimics what HCL itself would do when preparing
			// the first argument to this function, since the parameter
			// uses the special "expression closure type" which causes
			// HCL to delay evaluation of the expression and let the
			// function handle it directly itself.
			expr, diags := hclsyntax.ParseExpression([]byte(test.templateExpr), "", hcl.InitialPos)
			if diags.HasErrors() {
				t.Fatalf("unexpected errors: %s", diags.Error())
			}
			exprClosure := &customdecode.ExpressionClosure{
				Expression: expr,
				EvalContext: &hcl.EvalContext{
					Variables: test.exprScope,
				},
			}
			exprClosureVal := customdecode.ExpressionClosureVal(exprClosure)

			got, gotErr := funcToTest.Call([]cty.Value{exprClosureVal, test.vars})

			if test.wantErr != "" {
				if gotErr == nil {
					t.Fatalf("unexpected success\ngot: %#v\nwant error: %s", got, test.wantErr)
				}
				if got, want := gotErr.Error(), test.wantErr; got != want {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			}
			if gotErr != nil {
				t.Errorf("unexpected error: %s", gotErr.Error())
			}
			if !test.want.RawEquals(got) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
			}
		})
	}
}
