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

package compliancetest

import (
	"bytes"
	"errors"
	"reflect"
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/opentofu/opentofu/internal/encryption/compliancetest"
	"github.com/opentofu/opentofu/internal/encryption/config"
	"github.com/opentofu/opentofu/internal/encryption/method"
	"github.com/zclconf/go-cty/cty"
)

// ComplianceTest tests the functionality of a method to make sure it conforms to the expectations of the method
// interface.
func ComplianceTest[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method](
	t *testing.T,
	testConfig TestConfiguration[TDescriptor, TConfig, TMethod],
) {
	testConfig.execute(t)
}

type TestConfiguration[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method] struct {
	Descriptor TDescriptor
	// HCLParseTestCases contains the test cases of parsing HCL configuration and then validating it using the Build()
	// function.
	HCLParseTestCases map[string]HCLParseTestCase[TDescriptor, TConfig, TMethod]

	// ProvideTestCase exercises the entire chain and generates two keys.
	EncryptDecryptTestCase EncryptDecryptTestCase[TConfig, TMethod]
}

func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) execute(t *testing.T) {
	t.Run("id", func(t *testing.T) {
		cfg.testID(t)
	})
	t.Run("hcl", func(t *testing.T) {
		cfg.testHCL(t)
	})
	t.Run("encrypt-decrypt", func(t *testing.T) {
		cfg.EncryptDecryptTestCase.execute(t)
	})
}

func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) testID(t *testing.T) {
	id := cfg.Descriptor.ID()
	if err := id.Validate(); err != nil {
		compliancetest.Fail(t, "Invalid ID returned from method descriptor: %s (%v)", id, err)
	} else {
		compliancetest.Log(t, "The ID provided by the method descriptor is valid: %s", id)
	}
}

func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) testHCL(t *testing.T) {
	if cfg.HCLParseTestCases == nil {
		compliancetest.Fail(t, "Please provide a map to HCLParseTestCases.")
	}
	hasInvalidHCL := false
	hasValidHCLInvalidBuild := false
	hasValidBuild := false
	for name, tc := range cfg.HCLParseTestCases {
		if !tc.ValidHCL {
			hasInvalidHCL = true
		} else {
			if tc.ValidBuild {
				hasValidBuild = true
			} else {
				hasValidHCLInvalidBuild = true
			}
		}
		t.Run(name, func(t *testing.T) {
			tc.execute(t, cfg.Descriptor)
		})
	}
	t.Run("completeness", func(t *testing.T) {
		if !hasInvalidHCL {
			compliancetest.Fail(t, "Please provide at least one test case with an invalid HCL.")
		}
		if !hasValidHCLInvalidBuild {
			compliancetest.Fail(t, "Please provide at least one test case with a valid HCL that fails on Build()")
		}
		if !hasValidBuild {
			compliancetest.Fail(
				t,
				"Please provide at least one test case with a valid HCL that succeeds on Build()",
			)
		}
	})
}

// HCLParseTestCase contains a test case that parses HCL into a configuration.
type HCLParseTestCase[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method] struct {
	// HCL contains the code that should be parsed into the configuration structure.
	HCL string
	// ValidHCL indicates that the HCL block should be parsable into the configuration structure, but not necessarily
	// result in a valid Build() call.
	ValidHCL bool
	// ValidBuild indicates that calling the Build() function should not result in an error.
	ValidBuild bool
	// Validate is an extra optional validation function that can check if the configuration contains the correct
	// values parsed from HCL. If ValidBuild is true, the method will be passed as well.
	Validate func(config TConfig, method TMethod) error
}

func (h *HCLParseTestCase[TDescriptor, TConfig, TMethod]) execute(t *testing.T, descriptor TDescriptor) {
	parseError := false
	parsedConfig, diags := config.LoadConfigFromString("config.hcl", h.HCL)
	if h.ValidHCL {
		if diags.HasErrors() {
			compliancetest.Fail(t, "Unexpected HCL error (%v).", diags)
		} else {
			compliancetest.Log(t, "HCL successfully parsed.")
		}
	} else {
		if diags.HasErrors() {
			parseError = true
		}
	}

	methodCtx := method.EvalContext{ValueForExpression: func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) {
		return expr.Value(nil)
	}}
	configStruct, diags := descriptor.DecodeConfig(methodCtx, parsedConfig.MethodConfigs[0].Body)

	var m TMethod
	if h.ValidHCL {
		if diags.HasErrors() {
			compliancetest.Fail(t, "Failed to parse empty HCL block into config struct (%v).", diags)
		} else {
			compliancetest.Log(t, "HCL successfully loaded into config struct.")
		}

		m = buildConfigAndValidate[TMethod](t, configStruct, h.ValidBuild)
	} else {
		if !parseError && !diags.HasErrors() {
			compliancetest.Fail(t, "Expected error during HCL parsing, but no error was returned.")
		} else {
			compliancetest.Log(t, "HCL loading errored correctly (%v).", diags)
		}
	}

	if h.Validate != nil {
		if err := h.Validate(configStruct.(TConfig), m); err != nil {
			compliancetest.Fail(t, "Error during validation and configuration (%v).", err)
		} else {
			compliancetest.Log(t, "Successfully validated parsed HCL config and applied modifications.")
		}
	} else {
		compliancetest.Log(t, "No ValidateAndConfigure provided, skipping HCL parse validation.")
	}
}

// EncryptDecryptTestCase handles a full encryption-decryption cycle.
type EncryptDecryptTestCase[TConfig method.Config, TMethod method.Method] struct {
	// ValidEncryptOnlyConfig is a configuration that has no decryption key and can only be used for encryption. The
	// key must match ValidFullConfig.
	ValidEncryptOnlyConfig TConfig
	// ValidFullConfig is a configuration that contains both an encryption and decryption key.
	ValidFullConfig TConfig
	// DecryptCannotBeVerified allows the decryption to succeed unencrypted data. This is needed for methods that
	// cannot verify if data decrypted successfully (e.g. xor).
	DecryptCannotBeVerified bool
}

func (m EncryptDecryptTestCase[TConfig, TMethod]) execute(t *testing.T) {
	if reflect.ValueOf(m.ValidEncryptOnlyConfig).IsNil() {
		compliancetest.Fail(t, "Please provide a ValidEncryptOnlyConfig to EncryptDecryptTestCase.")
	}
	if reflect.ValueOf(m.ValidFullConfig).IsNil() {
		compliancetest.Fail(t, "Please provide a ValidFullConfig to EncryptDecryptTestCase.")
	}

	encryptMethod := buildConfigAndValidate[TMethod, TConfig](t, m.ValidEncryptOnlyConfig, true)
	decryptMethod := buildConfigAndValidate[TMethod, TConfig](t, m.ValidFullConfig, true)

	plainData := []byte("Hello world!")
	encryptedData, err := encryptMethod.Encrypt(plainData)
	if err != nil {
		compliancetest.Fail(t, "Unexpected error after Encrypt() on the encrypt-only method (%v).", err)
	}

	_, err = encryptMethod.Decrypt(encryptedData)
	if err == nil {
		compliancetest.Fail(t, "Decrypt() did not fail without a decryption key.")
	} else {
		compliancetest.Log(t, "Decrypt() returned an error with a decryption key.")
	}
	var noDecryptionKeyError *method.ErrDecryptionKeyUnavailable
	if !errors.As(err, &noDecryptionKeyError) {
		compliancetest.Fail(t, "Decrypt() returned a %T instead of a %T without a decryption key. Please use the correct typed errors.", err, noDecryptionKeyError)
	} else {
		compliancetest.Log(t, "Decrypt() returned the correct error type of %T without a decryption key.", noDecryptionKeyError)
	}

	_, err = decryptMethod.Decrypt([]byte{})
	if err == nil {
		compliancetest.Fail(t, "Decrypt() must return an error when decrypting empty data, no error returned.")
	} else {
		compliancetest.Log(t, "Decrypt() correctly returned an error when decrypting empty data.")
	}
	var typedDecryptError *method.ErrDecryptionFailed
	if !errors.As(err, &typedDecryptError) {
		compliancetest.Fail(t, "Decrypt() returned a %T instead of a %T when decrypting empty data. Please use the correct typed errors.", err, typedDecryptError)
	} else {
		compliancetest.Log(t, "Decrypt() returned the correct error type of %T when decrypting empty data.", typedDecryptError)
	}
	typedDecryptError = nil

	if !m.DecryptCannotBeVerified {
		_, err = decryptMethod.Decrypt(plainData)
		if err == nil {
			compliancetest.Fail(t, "Decrypt() must return an error when decrypting unencrypted data, no error returned.")
		} else {
			compliancetest.Log(t, "Decrypt() correctly returned an error when decrypting unencrypted data.")
		}
		if !errors.As(err, &typedDecryptError) {
			compliancetest.Fail(t, "Decrypt() returned a %T instead of a %T when decrypting unencrypted data. Please use the correct typed errors.", err, typedDecryptError)
		} else {
			compliancetest.Log(t, "Decrypt() returned the correct error type of %T when decrypting unencrypted data.", typedDecryptError)
		}
	}

	decryptedData, err := decryptMethod.Decrypt(encryptedData)
	if err != nil {
		compliancetest.Fail(t, "Decrypt() failed to decrypt previously-encrypted data (%v).", err)
	} else {
		compliancetest.Log(t, "Decrypt() succeeded.")
	}

	if !bytes.Equal(decryptedData, plainData) {
		compliancetest.Fail(t, "Decrypt() returned incorrect plain text data:\n%v\nexpected:\n%v", decryptedData, plainData)
	} else {
		compliancetest.Log(t, "Decrypt() returned the correct plain text data.")
	}
}

func buildConfigAndValidate[TMethod method.Method, TConfig method.Config](
	t *testing.T,
	configStruct TConfig,
	validBuild bool,
) TMethod {
	if reflect.ValueOf(configStruct).IsNil() {
		compliancetest.Fail(t, "Nil struct passed!")
	}

	var typedMethod TMethod
	var ok bool
	kp, err := configStruct.Build()
	if validBuild {
		if err != nil {
			compliancetest.Fail(t, "Build() returned an unexpected error: %v.", err)
		} else {
			compliancetest.Log(t, "Build() did not return an error.")
		}
		typedMethod, ok = kp.(TMethod)
		if !ok {
			compliancetest.Fail(t, "Build() returned an invalid method type of %T, expected %T", kp, typedMethod)
		} else {
			compliancetest.Log(t, "Build() returned the correct method type of %T.", typedMethod)
		}
	} else {
		if err == nil {
			compliancetest.Fail(t, "Build() did not return an error.")
		} else {
			compliancetest.Log(t, "Build() correctly returned an error: %v", err)
		}

		var typedError *method.ErrInvalidConfiguration
		if !errors.As(err, &typedError) {
			compliancetest.Fail(
				t,
				"Build() did not return the correct error type, got %T but expected %T",
				err,
				typedError,
			)
		} else {
			compliancetest.Log(t, "Build() returned the correct error type of %T", typedError)
		}
	}
	return typedMethod
}
