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

package s3

import (
	"fmt"
	"regexp"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws/arn"
	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/internal/tfdiags"
	"github.com/zclconf/go-cty/cty"
)

func TestValidateKMSKey(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		in       string
		expected tfdiags.Diagnostics
	}{
		"kms key id": {
			in: "57ff7a43-341d-46b6-aee3-a450c9de6dc8",
		},
		"kms key arn": {
			in: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8",
		},
		"kms multi-region key id": {
			in: "mrk-f827515944fb43f9b902a09d2c8b554f",
		},
		"kms multi-region key arn": {
			in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681",
		},
		"kms key alias": {
			in: "alias/arbitrary-key",
		},
		"kms key alias arn": {
			in: "arn:aws:kms:us-west-2:111122223333:alias/arbitrary-key",
		},
		"invalid key": {
			in: "$%wrongkey",
			expected: tfdiags.Diagnostics{
				tfdiags.AttributeValue(
					tfdiags.Error,
					"Invalid KMS Key ID",
					`Value must be a valid KMS Key ID, got "$%wrongkey"`,
					path,
				),
			},
		},
		"non-kms arn": {
			in: "arn:aws:lamda:foo:bar:key/xyz",
			expected: tfdiags.Diagnostics{
				tfdiags.AttributeValue(
					tfdiags.Error,
					"Invalid KMS Key ARN",
					`Value must be a valid KMS Key ARN, got "arn:aws:lamda:foo:bar:key/xyz"`,
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			diags := validateKMSKey(path, testcase.in)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateKeyARN(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		in       string
		expected tfdiags.Diagnostics
	}{
		"kms key id": {
			in: "arn:aws:kms:us-west-2:123456789012:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8",
		},
		"kms mrk key id": {
			in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681",
		},
		"kms non-key id": {
			in: "arn:aws:kms:us-west-2:123456789012:something/else",
			expected: tfdiags.Diagnostics{
				tfdiags.AttributeValue(
					tfdiags.Error,
					"Invalid KMS Key ARN",
					`Value must be a valid KMS Key ARN, got "arn:aws:kms:us-west-2:123456789012:something/else"`,
					path,
				),
			},
		},
		"non-kms arn": {
			in: "arn:aws:iam::123456789012:user/David",
			expected: tfdiags.Diagnostics{
				tfdiags.AttributeValue(
					tfdiags.Error,
					"Invalid KMS Key ARN",
					`Value must be a valid KMS Key ARN, got "arn:aws:iam::123456789012:user/David"`,
					path,
				),
			},
		},
		"not an arn": {
			in: "not an arn",
			expected: tfdiags.Diagnostics{
				tfdiags.AttributeValue(
					tfdiags.Error,
					"Invalid KMS Key ARN",
					`Value must be a valid KMS Key ARN, got "not an arn"`,
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			diags := validateKMSKeyARN(path, testcase.in)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateStringLenBetween(t *testing.T) {
	t.Parallel()

	const min, max = 2, 5
	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      string
		expected tfdiags.Diagnostics
	}{
		"valid": {
			val: "valid",
		},

		"too short": {
			val: "x",
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value Length",
					fmt.Sprintf("Length must be between %d and %d, had %d", min, max, 1),
					path,
				),
			},
		},

		"too long": {
			val: "a very long string",
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value Length",
					fmt.Sprintf("Length must be between %d and %d, had %d", min, max, 18),
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateStringLenBetween(min, max)(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateStringMatches(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      string
		re       *regexp.Regexp
		expected tfdiags.Diagnostics
	}{
		"valid": {
			val: "ok",
			re:  regexp.MustCompile(`^o[j-l]?$`),
		},

		"invalid": {
			val: "not ok",
			re:  regexp.MustCompile(`^o[j-l]?$`),
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					"Value must be like ok",
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateStringMatches(testcase.re, "Value must be like ok")(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateARN(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val       string
		validator arnValidator
		expected  tfdiags.Diagnostics
	}{
		"valid": {
			val: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8",
		},

		"invalid": {
			val: "not an ARN",
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid ARN",
					fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", "not an ARN", arnParseError("not an ARN")),
					path,
				),
			},
		},

		"fails validator": {
			val: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8",
			validator: func(val arn.ARN, path cty.Path, diags *tfdiags.Diagnostics) {
				*diags = diags.Append(attributeErrDiag(
					"Test",
					"Test",
					path,
				))
			},
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Test",
					"Test",
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var validators []arnValidator
			if testcase.validator != nil {
				validators = []arnValidator{
					testcase.validator,
				}
			}

			var diags tfdiags.Diagnostics
			validateARN(validators...)(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func arnParseError(s string) error {
	_, err := arn.Parse(s)
	return err
}

func TestValidateIAMPolicyDocument(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      string
		expected tfdiags.Diagnostics
	}{
		"empty object": {
			val: `{}`,
			// Valid JSON, not valid IAM policy (but passes provider's test)
		},
		"array": {
			val: `{"abc":["1","2"]}`,
			// Valid JSON, not valid IAM policy (but passes provider's test)
		},
		"invalid key": {
			val: `{0:"1"}`,
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid JSON Document",
					"The JSON document contains an error: invalid character '0' looking for beginning of object key string, at byte offset 2",
					path,
				),
			},
		},
		"leading whitespace": {
			val: `    {"xyz": "foo"}`,
			// Valid, must be trimmed before passing to AWS
		},
		"is a string": {
			val: `"blub"`,
			// Valid JSON, not valid IAM policy
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid IAM Policy Document",
					`Expected a JSON object describing the policy, had a JSON-encoded string.`,
					path,
				),
			},
		},
		"contains filename": {
			val: `"../some-filename.json"`,
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid IAM Policy Document",
					`Expected a JSON object describing the policy, had a JSON-encoded string.

The string "../some-filename.json" looks like a filename, please pass the contents of the file instead of the filename.`,
					path,
				),
			},
		},
		"double encoded": {
			val: `"{\"Version\":\"...\"}"`,
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid IAM Policy Document",
					`Expected a JSON object describing the policy, had a JSON-encoded string.

The string content was valid JSON, your policy document may have been double-encoded.`,
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateIAMPolicyDocument(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateSetStringElements(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val       cty.Value
		validator stringValidator
		expected  tfdiags.Diagnostics
	}{
		"valid": {
			val: cty.SetVal([]cty.Value{
				cty.StringVal("valid"),
				cty.StringVal("also valid"),
			}),
		},

		"fails validator": {
			val: cty.SetVal([]cty.Value{
				cty.StringVal("valid"),
				cty.StringVal("invalid"),
			}),
			validator: func(val string, path cty.Path, diags *tfdiags.Diagnostics) {
				if val == "invalid" {
					*diags = diags.Append(attributeErrDiag(
						"Test",
						"Test",
						path,
					))
				}
			},
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Test",
					"Test",
					path.Index(cty.StringVal("invalid")),
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var validators []stringValidator
			if testcase.validator != nil {
				validators = []stringValidator{
					testcase.validator,
				}
			}

			var diags tfdiags.Diagnostics
			validateSetStringElements(validators...)(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

// func TestValidateStringSetValues(t *testing.T) {
// 	t.Parallel()

// 	path := cty.GetAttrPath("field")

// 	testcases := map[string]struct {
// 		val       []string
// 		validator stringValidator
// 		expected  tfdiags.Diagnostics
// 	}{
// 		"valid": {
// 			val: []string{
// 				"valid",
// 				"also valid",
// 			},
// 		},

// 		"fails validator": {
// 			val: []string{
// 				"valid",
// 				"invalid",
// 			},
// 			validator: func(val string, path cty.Path, diags *tfdiags.Diagnostics) {
// 				if val == "invalid" {
// 					*diags = diags.Append(attributeErrDiag(
// 						"Test",
// 						"Test",
// 						path,
// 					))
// 				}
// 			},
// 			expected: tfdiags.Diagnostics{
// 				attributeErrDiag(
// 					"Test",
// 					"Test",
// 					path.Index(cty.StringVal("invalid")),
// 				),
// 			},
// 		},
// 	}

// 	for name, testcase := range testcases {
// 		testcase := testcase
// 		t.Run(name, func(t *testing.T) {
// 			t.Parallel()

// 			var validators []stringValidator
// 			if testcase.validator != nil {
// 				validators = []stringValidator{
// 					testcase.validator,
// 				}
// 			}

// 			var diags tfdiags.Diagnostics
// 			validateStringSetValues(validators...)(testcase.val, path, &diags)

// 			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
// 				t.Errorf("unexpected diagnostics difference: %s", diff)
// 			}
// 		})
// 	}
// }

func TestValidateDuration(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val       string
		validator durationValidator
		expected  tfdiags.Diagnostics
	}{
		"valid": {
			val: "1h",
		},

		"invalid": {
			val: "one hour",
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Duration",
					fmt.Sprintf("The value %q cannot be parsed as a duration: %s", "one hour", durationParseError("one hour")),
					path,
				),
			},
		},

		"fails validator": {
			val: "1h",
			validator: func(val time.Duration, path cty.Path, diags *tfdiags.Diagnostics) {
				*diags = diags.Append(attributeErrDiag(
					"Test",
					"Test",
					path,
				))
			},
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Test",
					"Test",
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var validators []durationValidator
			if testcase.validator != nil {
				validators = []durationValidator{
					testcase.validator,
				}
			}

			var diags tfdiags.Diagnostics
			validateDuration(validators...)(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func durationParseError(s string) error {
	_, err := time.ParseDuration(s)
	return err
}

func TestValidateDurationBetween(t *testing.T) {
	t.Parallel()

	const min, max = 15 * time.Minute, 12 * time.Hour
	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      time.Duration
		expected tfdiags.Diagnostics
	}{
		"valid": {
			val: 1 * time.Hour,
		},

		"too short": {
			val: 1 * time.Minute,
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Duration",
					fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, 1*time.Minute),
					path,
				),
			},
		},

		"too long": {
			val: 24 * time.Hour,
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Duration",
					fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, 24*time.Hour),
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateDurationBetween(min, max)(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateStringLegacyURL(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      string
		expected tfdiags.Diagnostics
	}{
		"no trailing slash": {
			val: "https://domain.test",
		},

		"no path": {
			val: "https://domain.test/",
		},

		"with path": {
			val: "https://domain.test/path",
		},

		"with port no trailing slash": {
			val: "https://domain.test:1234",
		},

		"with port no path": {
			val: "https://domain.test:1234/",
		},

		"with port with path": {
			val: "https://domain.test:1234/path",
		},

		"no scheme no trailing slash": {
			val: "domain.test",
			expected: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("domain.test", path),
			},
		},

		"no scheme no path": {
			val: "domain.test/",
			expected: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("domain.test/", path),
			},
		},

		"no scheme with path": {
			val: "domain.test/path",
			expected: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("domain.test/path", path),
			},
		},

		"no scheme with port": {
			val: "domain.test:1234",
			expected: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("domain.test:1234", path),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateStringLegacyURL(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestValidateStringValidURL(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      string
		expected tfdiags.Diagnostics
	}{
		"no trailing slash": {
			val: "https://domain.test",
		},

		"no path": {
			val: "https://domain.test/",
		},

		"with path": {
			val: "https://domain.test/path",
		},

		"with port no trailing slash": {
			val: "https://domain.test:1234",
		},

		"with port no path": {
			val: "https://domain.test:1234/",
		},

		"with port with path": {
			val: "https://domain.test:1234/path",
		},

		"no scheme no trailing slash": {
			val: "domain.test",
			expected: tfdiags.Diagnostics{
				invalidURLDiag("domain.test", path),
			},
		},

		"no scheme no path": {
			val: "domain.test/",
			expected: tfdiags.Diagnostics{
				invalidURLDiag("domain.test/", path),
			},
		},

		"no scheme with path": {
			val: "domain.test/path",
			expected: tfdiags.Diagnostics{
				invalidURLDiag("domain.test/path", path),
			},
		},

		"no scheme with port": {
			val: "domain.test:1234",
			expected: tfdiags.Diagnostics{
				invalidURLDiag("domain.test:1234", path),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateStringValidURL(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func Test_validateStringDoesNotContain(t *testing.T) {
	t.Parallel()

	path := cty.GetAttrPath("field")

	testcases := map[string]struct {
		val      string
		s        string
		expected tfdiags.Diagnostics
	}{
		"valid": {
			val: "foo",
			s:   "bar",
		},

		"invalid": {
			val: "foobarbaz",
			s:   "bar",
			expected: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`Value must not contain "bar"`,
					path,
				),
			},
		},
	}

	for name, testcase := range testcases {
		testcase := testcase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			var diags tfdiags.Diagnostics
			validateStringDoesNotContain(testcase.s)(testcase.val, path, &diags)

			if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}
