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

package funcs

import (
	"fmt"
	"testing"

	"github.com/opentofu/opentofu/internal/lang/marks"
	"github.com/zclconf/go-cty/cty"
)

func TestSensitive(t *testing.T) {
	tests := []struct {
		Input   cty.Value
		WantErr string
	}{
		{
			cty.NumberIntVal(1),
			``,
		},
		{
			// Unknown values stay unknown while becoming sensitive
			cty.UnknownVal(cty.String),
			``,
		},
		{
			// Null values stay unknown while becoming sensitive
			cty.NullVal(cty.String),
			``,
		},
		{
			// DynamicVal can be marked as sensitive
			cty.DynamicVal,
			``,
		},
		{
			// The marking is shallow only
			cty.ListVal([]cty.Value{cty.NumberIntVal(1)}),
			``,
		},
		{
			// A value already marked is allowed and stays marked
			cty.NumberIntVal(1).Mark(marks.Sensitive),
			``,
		},
		{
			// Any non-sensitive marks must be propagated alongside
			// with a sensitive one.
			cty.NumberIntVal(1).Mark("bloop"),
			``,
		},
		{
			// A value deep already marked is allowed and stays marked,
			// _and_ we'll also mark the outer collection as sensitive.
			cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}),
			``,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("sensitive(%#v)", test.Input), func(t *testing.T) {
			got, err := Sensitive(test.Input)

			if test.WantErr != "" {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				if got, want := err.Error(), test.WantErr; got != want {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.HasMark(marks.Sensitive) {
				t.Errorf("result is not marked sensitive")
			}

			inputMarks := test.Input.Marks()
			delete(inputMarks, marks.Sensitive)

			gotRaw, gotMarks := got.Unmark()
			if len(gotMarks) != len(inputMarks)+1 {
				t.Errorf("extraneous marks %#v", gotMarks)
			}

			// Disregarding shallow marks, the result should have the same
			// effective value as the input.
			wantRaw, _ := test.Input.Unmark()
			if !gotRaw.RawEquals(wantRaw) {
				t.Errorf("wrong unmarked result\ngot:  %#v\nwant: %#v", got, wantRaw)
			}
		})
	}
}

func TestNonsensitive(t *testing.T) {
	tests := []struct {
		Input   cty.Value
		WantErr string
	}{
		{
			cty.NumberIntVal(1).Mark(marks.Sensitive),
			``,
		},
		{
			cty.DynamicVal.Mark(marks.Sensitive),
			``,
		},
		{
			cty.UnknownVal(cty.String).Mark(marks.Sensitive),
			``,
		},
		{
			cty.NullVal(cty.EmptyObject).Mark(marks.Sensitive),
			``,
		},
		{
			// The inner sensitive remains afterwards
			cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}).Mark(marks.Sensitive),
			``,
		},

		// Passing a value that is already non-sensitive is not an error,
		// as this function may be used with specific to ensure that all
		// values are indeed non-sensitive
		{
			cty.NumberIntVal(1),
			``,
		},
		{
			cty.NullVal(cty.String),
			``,
		},

		// Unknown values may become sensitive once they are known, so we
		// permit them to be marked nonsensitive.
		{
			cty.DynamicVal,
			``,
		},
		{
			cty.UnknownVal(cty.String),
			``,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("nonsensitive(%#v)", test.Input), func(t *testing.T) {
			got, err := Nonsensitive(test.Input)

			if test.WantErr != "" {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				if got, want := err.Error(), test.WantErr; got != want {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if got.HasMark(marks.Sensitive) {
				t.Errorf("result is still marked sensitive")
			}
			wantRaw, _ := test.Input.Unmark()
			if !got.RawEquals(wantRaw) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Input)
			}
		})
	}
}

func TestIsSensitive(t *testing.T) {
	tests := []struct {
		Input       cty.Value
		IsSensitive cty.Value
	}{
		{
			cty.NumberIntVal(1).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.NumberIntVal(1),
			cty.False,
		},
		{
			cty.DynamicVal.Mark(marks.Sensitive),
			cty.UnknownVal(cty.Bool).RefineNotNull(),
		},
		{
			cty.DynamicVal,
			cty.UnknownVal(cty.Bool).RefineNotNull(),
		},
		{
			cty.UnknownVal(cty.String).Mark(marks.Sensitive),
			cty.UnknownVal(cty.Bool).RefineNotNull(),
		},
		{
			cty.UnknownVal(cty.String),
			cty.UnknownVal(cty.Bool).RefineNotNull(),
		},
		{
			cty.NullVal(cty.EmptyObject).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.NullVal(cty.EmptyObject),
			cty.False,
		},
		{
			cty.NullVal(cty.DynamicPseudoType).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.NullVal(cty.DynamicPseudoType),
			cty.False,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("issensitive(%#v)", test.Input), func(t *testing.T) {
			got, err := IsSensitive(test.Input)

			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

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