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

package tryfunc

import (
	"testing"

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

func TestTryFunc(t *testing.T) {
	tests := map[string]struct {
		expr    string
		vars    map[string]cty.Value
		want    cty.Value
		wantErr string
	}{
		"one argument succeeds": {
			`try(1)`,
			nil,
			cty.NumberIntVal(1),
			``,
		},
		"one marked argument succeeds": {
			`try(sensitive)`,
			map[string]cty.Value{
				"sensitive": cty.StringVal("secret").Mark("porpoise"),
			},
			cty.StringVal("secret").Mark("porpoise"),
			``,
		},
		"two arguments, first succeeds": {
			`try(1, 2)`,
			nil,
			cty.NumberIntVal(1),
			``,
		},
		"two arguments, first fails": {
			`try(nope, 2)`,
			nil,
			cty.NumberIntVal(2),
			``,
		},
		"two arguments, first depends on unknowns": {
			`try(unknown, 2)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.Number),
			},
			cty.DynamicVal, // can't proceed until first argument is known
			``,
		},
		"two arguments, first succeeds and second depends on unknowns": {
			`try(1, unknown)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.Number),
			},
			cty.NumberIntVal(1), // we know 1st succeeds, so it doesn't matter that 2nd is unknown
			``,
		},
		"two arguments, first depends on unknowns deeply": {
			`try(has_unknowns, 2)`,
			map[string]cty.Value{
				"has_unknowns": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
			},
			cty.DynamicVal, // can't proceed until first argument is wholly known
			``,
		},
		"two arguments, first traverses through an unkown": {
			`try(unknown.baz, 2)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.Map(cty.String)),
			},
			cty.DynamicVal, // can't proceed until first argument is wholly known
			``,
		},
		"two arguments, both marked, first succeeds": {
			`try(sensitive, other)`,
			map[string]cty.Value{
				"sensitive": cty.StringVal("secret").Mark("porpoise"),
				"other":     cty.StringVal("that").Mark("a"),
			},
			cty.StringVal("secret").Mark("porpoise"),
			``,
		},
		"two arguments, both marked, second succeeds": {
			`try(sensitive, other)`,
			map[string]cty.Value{
				"other": cty.StringVal("that").Mark("a"),
			},
			cty.StringVal("that").Mark("a"),
			``,
		},
		"two arguments, result is element of marked list ": {
			`try(sensitive[0], other)`,
			map[string]cty.Value{
				"sensitive": cty.ListVal([]cty.Value{
					cty.StringVal("list"),
					cty.StringVal("of "),
					cty.StringVal("secrets"),
				}).Mark("secret"),
				"other": cty.StringVal("not"),
			},
			cty.StringVal("list").Mark("secret"),
			``,
		},
		"nested known expression from unknown": {
			// this expression contains an unknown, but will always return in
			// "bar"
			`try({u: false ? unknown : "bar"}, other)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.String),
				"other": cty.MapVal(map[string]cty.Value{
					"v": cty.StringVal("oops"),
				}),
			},
			cty.ObjectVal(map[string]cty.Value{
				"u": cty.StringVal("bar"),
			}),
			``,
		},
		"nested index op on unknown": {
			// unknown and other have identical types, but we must return a
			// dynamic value since v could change within the final result value
			// after the first argument becomes known.
			`try({u: unknown["foo"], v: "orig"}, other)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.Map(cty.String)),
				"other": cty.MapVal(map[string]cty.Value{
					"u": cty.StringVal("oops"),
					"v": cty.StringVal("oops"),
				}),
			},
			cty.DynamicVal,
			``,
		},
		"three arguments, all fail": {
			`try(this, that, this_thing_in_particular)`,
			nil,
			cty.NumberIntVal(2),
			// The grammar of this stringification of the message is unfortunate,
			// but caller can type-assert our result to get the original
			// diagnostics directly in order to produce a better result.
			`test.hcl:1,1-5: Error in function call; Call to function "try" failed: no expression succeeded:
- Variables not allowed (at test.hcl:1,5-9)
  Variables may not be used here.
- Variables not allowed (at test.hcl:1,11-15)
  Variables may not be used here.
- Variables not allowed (at test.hcl:1,17-41)
  Variables may not be used here.

At least one expression must produce a successful result.`,
		},
		"no arguments": {
			`try()`,
			nil,
			cty.NilVal,
			`test.hcl:1,1-5: Error in function call; Call to function "try" failed: at least one argument is required.`,
		},
	}

	for k, test := range tests {
		t.Run(k, func(t *testing.T) {
			expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1})
			if diags.HasErrors() {
				t.Fatalf("unexpected problems: %s", diags.Error())
			}

			ctx := &hcl.EvalContext{
				Variables: test.vars,
				Functions: map[string]function.Function{
					"try": TryFunc,
				},
			}

			got, err := expr.Value(ctx)

			if err != nil {
				if test.wantErr != "" {
					if got, want := err.Error(), test.wantErr; got != want {
						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
					}
				} else {
					t.Errorf("unexpected error\ngot:  %s\nwant: <nil>", err)
				}
				return
			}
			if test.wantErr != "" {
				t.Errorf("wrong error\ngot:  <nil>\nwant: %s", test.wantErr)
			}

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

func TestCanFunc(t *testing.T) {
	tests := map[string]struct {
		expr string
		vars map[string]cty.Value
		want cty.Value
	}{
		"succeeds": {
			`can(1)`,
			nil,
			cty.True,
		},
		"fails": {
			`can(nope)`,
			nil,
			cty.False,
		},
		"simple unknown": {
			`can(unknown)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.Number),
			},
			cty.UnknownVal(cty.Bool),
		},
		"traversal through unknown": {
			`can(unknown.foo)`,
			map[string]cty.Value{
				"unknown": cty.UnknownVal(cty.Map(cty.Number)),
			},
			cty.UnknownVal(cty.Bool),
		},
		"deep unknown": {
			`can(has_unknown)`,
			map[string]cty.Value{
				"has_unknown": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
			},
			cty.UnknownVal(cty.Bool),
		},
	}

	for k, test := range tests {
		t.Run(k, func(t *testing.T) {
			expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1})
			if diags.HasErrors() {
				t.Fatalf("unexpected problems: %s", diags.Error())
			}

			ctx := &hcl.EvalContext{
				Variables: test.vars,
				Functions: map[string]function.Function{
					"can": CanFunc,
				},
			}

			got, err := expr.Value(ctx)
			if err != nil {
				t.Errorf("unexpected error\ngot:  %s\nwant: <nil>", err)
			}
			if !test.want.RawEquals(got) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
			}
		})
	}
}
