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

package configs

import (
	"fmt"

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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/getmodules"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

// TestCommand represents the OpenTofu a given run block will execute, plan
// or apply. Defaults to apply.
type TestCommand rune

// TestMode represents the plan mode that OpenTofu will use for a given run
// block, normal or refresh-only. Defaults to normal.
type TestMode rune

const (
	// ApplyTestCommand causes the run block to execute a OpenTofu apply
	// operation.
	ApplyTestCommand TestCommand = 0

	// PlanTestCommand causes the run block to execute a OpenTofu plan
	// operation.
	PlanTestCommand TestCommand = 'P'

	// NormalTestMode causes the run block to execute in plans.NormalMode.
	NormalTestMode TestMode = 0

	// RefreshOnlyTestMode causes the run block to execute in
	// plans.RefreshOnlyMode.
	RefreshOnlyTestMode TestMode = 'R'
)

// TestFile represents a single test file within a `tofu test` execution.
//
// A test file is made up of a sequential list of run blocks, each designating
// a command to execute and a series of validations to check after the command.
type TestFile struct {
	// Variables defines a set of global variable definitions that should be set
	// for every run block within the test file.
	Variables map[string]hcl.Expression

	// Providers defines a set of providers that are available to run blocks
	// within this test file.
	//
	// If empty, tests should use the default providers for the module under
	// test.
	Providers map[string]*Provider

	// Runs defines the sequential list of run blocks that should be executed in
	// order.
	Runs []*TestRun

	// OverrideResources is a list of resources to be overridden with static values.
	// Underlying providers shouldn't be called for overridden resources.
	OverrideResources []*OverrideResource

	// OverrideModules is a list of modules to be overridden with static values.
	// Underlying modules shouldn't be called.
	OverrideModules []*OverrideModule

	// MockProviders is a map of providers that should be mocked. It is merged
	// with Providers map to use later when instantiating provider instance.
	MockProviders map[string]*MockProvider

	VariablesDeclRange hcl.Range
}

// Validate does a very simple and cursory check across the file blocks to look
// for simple issues we can highlight early on. It doesn't validate nested run blocks.
func (file *TestFile) Validate() tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	// It's not allowed to have multiple `override_resource`, `override_data` or `override_module` blocks
	// declared globally in a file with the same target address so we want to ensure there's no such cases.
	diags = diags.Append(checkForDuplicatedOverrideResources(file.OverrideResources))
	diags = diags.Append(checkForDuplicatedOverrideModules(file.OverrideModules))

	return diags
}

func (file *TestFile) getTestProviderOrMock(addr string) (*Provider, bool) {
	testProvider, ok := file.Providers[addr]
	if ok {
		return testProvider, true
	}

	mockProvider, ok := file.MockProviders[addr]
	if ok {
		p := &Provider{
			Name:          mockProvider.Name,
			NameRange:     mockProvider.NameRange,
			Alias:         mockProvider.Alias,
			AliasRange:    mockProvider.AliasRange,
			DeclRange:     mockProvider.DeclRange,
			IsMocked:      true,
			MockResources: mockProvider.MockResources,
		}

		return p, true
	}

	return nil, false
}

// TestRun represents a single run block within a test file.
//
// Each run block represents a single OpenTofu command to be executed and a set
// of validations to run after the command.
type TestRun struct {
	Name string

	// Command is the OpenTofu command to execute.
	//
	// One of ['apply', 'plan'].
	Command TestCommand

	// Options contains the embedded plan options that will affect the given
	// Command. These should map to the options documented here:
	//   - https://opentofu.org/docs/cli/commands/plan/#planning-options
	//
	// Note, that the Variables are a top level concept and not embedded within
	// the options despite being listed as plan options in the documentation.
	Options *TestRunOptions

	// Variables defines a set of variable definitions for this command.
	//
	// Any variables specified locally that clash with the global variables will
	// take precedence over the global definition.
	Variables map[string]hcl.Expression

	// Providers specifies the set of providers that should be loaded into the
	// module for this run block.
	//
	// Providers specified here must be configured in one of the provider blocks
	// for this file. If empty, the run block will load the default providers
	// for the module under test.
	Providers []PassedProviderConfig

	// CheckRules defines the list of assertions/validations that should be
	// checked by this run block.
	CheckRules []*CheckRule

	// Module defines an address of another module that should be loaded and
	// executed as part of this run block instead of the module under test.
	//
	// In the initial version of the testing framework we will only support
	// loading alternate modules from local directories or the registry.
	Module *TestRunModuleCall

	// ConfigUnderTest describes the configuration this run block should execute
	// against.
	//
	// In typical cases, this will be null and the config under test is the
	// configuration within the directory the tofu test command is
	// executing within. However, when Module is set the config under test is
	// whichever config is defined by Module. This field is then set during the
	// configuration load process and should be used when the test is executed.
	ConfigUnderTest *Config

	// ExpectFailures should be a list of checkable objects that are expected
	// to report a failure from their custom conditions as part of this test
	// run.
	ExpectFailures []hcl.Traversal

	// OverrideResources is a list of resources to be overridden with static values.
	// Underlying providers shouldn't be called for overridden resources.
	OverrideResources []*OverrideResource

	// OverrideModules is a list of modules to be overridden with static values.
	// Underlying modules shouldn't be called.
	OverrideModules []*OverrideModule

	NameDeclRange      hcl.Range
	VariablesDeclRange hcl.Range
	DeclRange          hcl.Range
}

// Validate does a very simple and cursory check across the run block to look
// for simple issues we can highlight early on.
func (run *TestRun) Validate() tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	// We want to make sure all the ExpectFailure references
	// are the correct kind of reference.
	for _, traversal := range run.ExpectFailures {

		reference, refDiags := addrs.ParseRefFromTestingScope(traversal)
		diags = diags.Append(refDiags)
		if refDiags.HasErrors() {
			continue
		}

		switch reference.Subject.(type) {
		// You can only reference outputs, inputs, checks, and resources.
		case addrs.OutputValue, addrs.InputVariable, addrs.Check, addrs.ResourceInstance, addrs.Resource:
			// Do nothing, these are okay!
		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid `expect_failures` reference",
				Detail:   fmt.Sprintf("You cannot expect failures from %s. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", reference.Subject.String()),
				Subject:  reference.SourceRange.ToHCL().Ptr(),
			})
		}

	}

	// It's not allowed to have multiple `override_resource`, `override_data` or `override_module` blocks
	// inside a single run block with the same target address so we want to ensure there's no such cases.
	diags = diags.Append(checkForDuplicatedOverrideResources(run.OverrideResources))
	diags = diags.Append(checkForDuplicatedOverrideModules(run.OverrideModules))

	return diags
}

// TestRunModuleCall specifies which module should be executed by a given run
// block.
type TestRunModuleCall struct {
	// Source is the source of the module to test.
	Source addrs.ModuleSource

	// Version is the version of the module to load from the registry.
	Version VersionConstraint

	DeclRange       hcl.Range
	SourceDeclRange hcl.Range
}

// TestRunOptions contains the plan options for a given run block.
type TestRunOptions struct {
	// Mode is the planning mode to run in. One of ['normal', 'refresh-only'].
	Mode TestMode

	// Refresh is analogous to the -refresh=false OpenTofu plan option.
	Refresh bool

	// Replace is analogous to the -refresh=ADDRESS OpenTofu plan option.
	Replace []hcl.Traversal

	// Target is analogous to the -target=ADDRESS OpenTofu plan option.
	Target []hcl.Traversal

	DeclRange hcl.Range
}

const (
	blockNameOverrideResource = "override_resource"
	blockNameOverrideData     = "override_data"
)

// OverrideResource contains information about a resource or data block to be overridden.
type OverrideResource struct {
	// Target references resource or data block to override.
	Target       hcl.Traversal
	TargetParsed *addrs.ConfigResource

	// Mode indicates if the Target is resource or data block.
	Mode addrs.ResourceMode

	// Values represents fields to use as defaults
	// if they are not present in configuration.
	Values map[string]cty.Value
}

func (r OverrideResource) getBlockName() string {
	switch r.Mode {
	case addrs.ManagedResourceMode:
		return blockNameOverrideResource
	case addrs.DataResourceMode:
		return blockNameOverrideData
	case addrs.InvalidResourceMode:
		panic("BUG: invalid resource mode in override resource")
	default:
		panic("BUG: undefined resource mode in override resource: " + r.Mode.String())
	}
}

const blockNameOverrideModule = "override_module"

// OverrideModule contains information about a module to be overridden.
type OverrideModule struct {
	// Target references module call to override.
	Target       hcl.Traversal
	TargetParsed addrs.Module

	// Outputs represents fields to use instead
	// of the real module call output.
	Outputs map[string]cty.Value
}

const blockNameMockProvider = "mock_provider"

// MockProvider represents mocked provider block. It partially matches
// the Provider configuration block (name, alias) and includes additional
// mocking data (mock resources).
type MockProvider struct {
	// Fields below are copied from configs.Provider struct:

	Name       string
	NameRange  hcl.Range
	Alias      string
	AliasRange *hcl.Range // nil if no alias set

	DeclRange hcl.Range

	// Fields below are specific to configs.MockProvider:

	MockResources []*MockResource
}

// moduleUniqueKey is copied from Provider.moduleUniqueKey
func (p *MockProvider) moduleUniqueKey() string {
	if p.Alias != "" {
		return fmt.Sprintf("%s.%s", p.Name, p.Alias)
	}
	return p.Name
}

const (
	blockNameMockResource = "mock_resource"
	blockNameMockData     = "mock_data"
)

// MockResource represents mocked resource. It is similar to OverrideResource,
// except all the resources with the same type should be overridden (mocked).
type MockResource struct {
	Mode     addrs.ResourceMode
	Type     string
	Defaults map[string]cty.Value
}

func (r MockResource) getBlockName() string {
	switch r.Mode {
	case addrs.ManagedResourceMode:
		return blockNameMockResource
	case addrs.DataResourceMode:
		return blockNameMockData
	case addrs.InvalidResourceMode:
		panic("BUG: invalid resource mode in mock resource")
	default:
		panic("BUG: undefined resource mode in mock resource: " + r.Mode.String())
	}
}

func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := body.Content(testFileSchema)
	diags = append(diags, contentDiags...)

	tf := TestFile{
		Providers:     make(map[string]*Provider),
		MockProviders: make(map[string]*MockProvider),
	}

	for _, block := range content.Blocks {
		switch block.Type {
		case "run":
			run, runDiags := decodeTestRunBlock(block)
			diags = append(diags, runDiags...)
			if !runDiags.HasErrors() {
				tf.Runs = append(tf.Runs, run)
			}

		case "variables":
			if tf.Variables != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Multiple \"variables\" blocks",
					Detail:   fmt.Sprintf("This test file already has a variables block defined at %s.", tf.VariablesDeclRange),
					Subject:  block.DefRange.Ptr(),
				})
				continue
			}

			tf.Variables = make(map[string]hcl.Expression)
			tf.VariablesDeclRange = block.DefRange

			vars, varsDiags := block.Body.JustAttributes()
			diags = append(diags, varsDiags...)
			for _, v := range vars {
				tf.Variables[v.Name] = v.Expr
			}

		case "provider":
			provider, providerDiags := decodeProviderBlock(block)
			diags = append(diags, providerDiags...)
			if provider != nil {
				tf.Providers[provider.moduleUniqueKey()] = provider
			}

		case blockNameOverrideResource:
			overrideRes, overrideResDiags := decodeOverrideResourceBlock(block, addrs.ManagedResourceMode)
			diags = append(diags, overrideResDiags...)
			if !overrideResDiags.HasErrors() {
				tf.OverrideResources = append(tf.OverrideResources, overrideRes)
			}

		case blockNameOverrideData:
			overrideData, overrideDataDiags := decodeOverrideResourceBlock(block, addrs.DataResourceMode)
			diags = append(diags, overrideDataDiags...)
			if !overrideDataDiags.HasErrors() {
				tf.OverrideResources = append(tf.OverrideResources, overrideData)
			}

		case blockNameOverrideModule:
			overrideMod, overrideModDiags := decodeOverrideModuleBlock(block)
			diags = append(diags, overrideModDiags...)
			if !overrideModDiags.HasErrors() {
				tf.OverrideModules = append(tf.OverrideModules, overrideMod)
			}

		case blockNameMockProvider:
			mockProvider, mockProviderDiags := decodeMockProviderBlock(block)
			diags = append(diags, mockProviderDiags...)

			if !mockProviderDiags.HasErrors() {
				k := mockProvider.moduleUniqueKey()

				if _, ok := tf.MockProviders[k]; ok {
					diags = diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Duplicated `mock_provider` block",
						Detail:   fmt.Sprintf("It is not allowed to have multiple `mock_provider` blocks with the same address: `%v`.", k),
						Subject:  mockProvider.DeclRange.Ptr(),
					})
				} else {
					tf.MockProviders[k] = mockProvider
				}
			}
		}
	}

	return &tf, diags
}

func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := block.Body.Content(testRunBlockSchema)
	diags = append(diags, contentDiags...)

	r := TestRun{
		Name:          block.Labels[0],
		NameDeclRange: block.LabelRanges[0],
		DeclRange:     block.DefRange,
	}

	if !hclsyntax.ValidIdentifier(r.Name) {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid run block name",
			Detail:   badIdentifierDetail,
			Subject:  &block.LabelRanges[0],
		})
	}

	for _, block := range content.Blocks {
		switch block.Type {
		case "assert":
			cr, crDiags := decodeCheckRuleBlock(block, false)
			diags = append(diags, crDiags...)
			if !crDiags.HasErrors() {
				r.CheckRules = append(r.CheckRules, cr)
			}
		case "plan_options":
			if r.Options != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Multiple \"plan_options\" blocks",
					Detail:   fmt.Sprintf("This run block already has a plan_options block defined at %s.", r.Options.DeclRange),
					Subject:  block.DefRange.Ptr(),
				})
				continue
			}

			opts, optsDiags := decodeTestRunOptionsBlock(block)
			diags = append(diags, optsDiags...)
			if !optsDiags.HasErrors() {
				r.Options = opts
			}
		case "variables":
			if r.Variables != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Multiple \"variables\" blocks",
					Detail:   fmt.Sprintf("This run block already has a variables block defined at %s.", r.VariablesDeclRange),
					Subject:  block.DefRange.Ptr(),
				})
				continue
			}

			r.Variables = make(map[string]hcl.Expression)
			r.VariablesDeclRange = block.DefRange

			vars, varsDiags := block.Body.JustAttributes()
			diags = append(diags, varsDiags...)
			for _, v := range vars {
				r.Variables[v.Name] = v.Expr
			}
		case "module":
			if r.Module != nil {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Multiple \"module\" blocks",
					Detail:   fmt.Sprintf("This run block already has a module block defined at %s.", r.Module.DeclRange),
					Subject:  block.DefRange.Ptr(),
				})
			}

			module, moduleDiags := decodeTestRunModuleBlock(block)
			diags = append(diags, moduleDiags...)
			if !moduleDiags.HasErrors() {
				r.Module = module
			}

		case blockNameOverrideResource:
			overrideRes, overrideResDiags := decodeOverrideResourceBlock(block, addrs.ManagedResourceMode)
			diags = append(diags, overrideResDiags...)
			if !overrideResDiags.HasErrors() {
				r.OverrideResources = append(r.OverrideResources, overrideRes)
			}

		case blockNameOverrideData:
			overrideData, overrideDataDiags := decodeOverrideResourceBlock(block, addrs.DataResourceMode)
			diags = append(diags, overrideDataDiags...)
			if !overrideDataDiags.HasErrors() {
				r.OverrideResources = append(r.OverrideResources, overrideData)
			}

		case blockNameOverrideModule:
			overrideMod, overrideModDiags := decodeOverrideModuleBlock(block)
			diags = append(diags, overrideModDiags...)
			if !overrideModDiags.HasErrors() {
				r.OverrideModules = append(r.OverrideModules, overrideMod)
			}
		}
	}

	if r.Variables == nil {
		// There is no distinction between a nil map of variables or an empty
		// map, but we can avoid any potential nil pointer exceptions by just
		// creating an empty map.
		r.Variables = make(map[string]hcl.Expression)
	}

	if r.Options == nil {
		// Create an options with default values if the user didn't specify
		// anything.
		r.Options = &TestRunOptions{
			Mode:    NormalTestMode,
			Refresh: true,
		}
	}

	if attr, exists := content.Attributes["command"]; exists {
		switch hcl.ExprAsKeyword(attr.Expr) {
		case "apply":
			r.Command = ApplyTestCommand
		case "plan":
			r.Command = PlanTestCommand
		default:
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid \"command\" keyword",
				Detail:   "The \"command\" argument requires one of the following keywords without quotes: apply or plan.",
				Subject:  attr.Expr.Range().Ptr(),
			})
		}
	} else {
		r.Command = ApplyTestCommand // Default to apply
	}

	if attr, exists := content.Attributes["providers"]; exists {
		providers, providerDiags := decodePassedProviderConfigs(attr)
		diags = append(diags, providerDiags...)
		r.Providers = append(r.Providers, providers...)
	}

	if attr, exists := content.Attributes["expect_failures"]; exists {
		failures, failDiags := decodeDependsOn(attr)
		diags = append(diags, failDiags...)
		r.ExpectFailures = failures
	}

	return &r, diags
}

func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := block.Body.Content(testRunModuleBlockSchema)
	diags = append(diags, contentDiags...)

	module := TestRunModuleCall{
		DeclRange: block.DefRange,
	}

	haveVersionArg := false
	if attr, exists := content.Attributes["version"]; exists {
		var versionDiags hcl.Diagnostics
		module.Version, versionDiags = decodeVersionConstraint(attr)
		diags = append(diags, versionDiags...)
		haveVersionArg = true
	}

	if attr, exists := content.Attributes["source"]; exists {
		module.SourceDeclRange = attr.Range

		var raw string
		rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &raw)
		diags = append(diags, rawDiags...)
		if !rawDiags.HasErrors() {
			var err error
			if haveVersionArg {
				module.Source, err = addrs.ParseModuleSourceRegistry(raw)
			} else {
				module.Source, err = addrs.ParseModuleSource(raw)
			}
			if err != nil {
				// NOTE: We leave mc.SourceAddr as nil for any situation where the
				// source attribute is invalid, so any code which tries to carefully
				// use the partial result of a failed config decode must be
				// resilient to that.
				module.Source = nil

				// NOTE: In practice it's actually very unlikely to end up here,
				// because our source address parser can turn just about any string
				// into some sort of remote package address, and so for most errors
				// we'll detect them only during module installation. There are
				// still a _few_ purely-syntax errors we can catch at parsing time,
				// though, mostly related to remote package sub-paths and local
				// paths.
				switch err := err.(type) {
				case *getmodules.MaybeRelativePathErr:
					diags = append(diags, &hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid module source address",
						Detail: fmt.Sprintf(
							"OpenTofu failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
							err.Addr, err.Addr,
						),
						Subject: module.SourceDeclRange.Ptr(),
					})
				default:
					if haveVersionArg {
						// In this case we'll include some extra context that
						// we assumed a registry source address due to the
						// version argument.
						diags = append(diags, &hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Invalid registry module source address",
							Detail:   fmt.Sprintf("Failed to parse module registry address: %s.\n\nOpenTofu assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
							Subject:  module.SourceDeclRange.Ptr(),
						})
					} else {
						diags = append(diags, &hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Invalid module source address",
							Detail:   fmt.Sprintf("Failed to parse module source address: %s.", err),
							Subject:  module.SourceDeclRange.Ptr(),
						})
					}
				}
			}

			switch module.Source.(type) {
			case addrs.ModuleSourceRemote:
				// We only support local or registry modules when loading
				// modules directly from alternate sources during a test
				// execution.
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Invalid module source address",
					Detail:   "Only local or registry module sources are currently supported from within test run blocks.",
					Subject:  module.SourceDeclRange.Ptr(),
				})
			}
		}
	} else {
		// Must have a source attribute.
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Missing \"source\" attribute for module block",
			Detail:   "You must specify a source attribute when executing alternate modules during test executions.",
			Subject:  module.DeclRange.Ptr(),
		})
	}

	return &module, diags
}

func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, contentDiags := block.Body.Content(testRunOptionsBlockSchema)
	diags = append(diags, contentDiags...)

	opts := TestRunOptions{
		DeclRange: block.DefRange,
	}

	if attr, exists := content.Attributes["mode"]; exists {
		switch hcl.ExprAsKeyword(attr.Expr) {
		case "refresh-only":
			opts.Mode = RefreshOnlyTestMode
		case "normal":
			opts.Mode = NormalTestMode
		default:
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid \"mode\" keyword",
				Detail:   "The \"mode\" argument requires one of the following keywords without quotes: normal or refresh-only",
				Subject:  attr.Expr.Range().Ptr(),
			})
		}
	} else {
		opts.Mode = NormalTestMode // Default to normal
	}

	if attr, exists := content.Attributes["refresh"]; exists {
		diags = append(diags, gohcl.DecodeExpression(attr.Expr, nil, &opts.Refresh)...)
	} else {
		// Defaults to true.
		opts.Refresh = true
	}

	if attr, exists := content.Attributes["replace"]; exists {
		reps, repsDiags := decodeDependsOn(attr)
		diags = append(diags, repsDiags...)
		opts.Replace = reps
	}

	if attr, exists := content.Attributes["target"]; exists {
		tars, tarsDiags := decodeDependsOn(attr)
		diags = append(diags, tarsDiags...)
		opts.Target = tars
	}

	if !opts.Refresh && opts.Mode == RefreshOnlyTestMode {
		// These options are incompatible.
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Incompatible plan options",
			Detail:   "The \"refresh\" option cannot be set to false when running a test in \"refresh-only\" mode.",
			Subject:  content.Attributes["refresh"].Range.Ptr(),
		})
	}

	return &opts, diags
}

func decodeOverrideResourceBlock(block *hcl.Block, mode addrs.ResourceMode) (*OverrideResource, hcl.Diagnostics) {
	parseTarget := func(attr *hcl.Attribute) (hcl.Traversal, *addrs.ConfigResource, hcl.Diagnostics) {
		traversal, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
		diags := traversalDiags
		if traversalDiags.HasErrors() {
			return nil, nil, diags
		}

		configRes, configResDiags := addrs.ParseConfigResource(traversal)
		diags = append(diags, configResDiags.ToHCL()...)
		if configResDiags.HasErrors() {
			return nil, nil, diags
		}

		return traversal, &configRes, diags
	}

	res := &OverrideResource{
		Mode: mode,
	}

	content, diags := block.Body.Content(overrideResourceBlockSchema)

	if attr, exists := content.Attributes["target"]; exists {
		target, parsed, moreDiags := parseTarget(attr)
		res.Target, res.TargetParsed = target, parsed
		diags = append(diags, moreDiags...)
	}

	if attr, exists := content.Attributes["values"]; exists {
		v, moreDiags := parseObjectAttrWithNoVariables(attr)
		res.Values, diags = v, append(diags, moreDiags...)
	}

	return res, diags
}

func decodeOverrideModuleBlock(block *hcl.Block) (*OverrideModule, hcl.Diagnostics) {
	parseTarget := func(attr *hcl.Attribute) (hcl.Traversal, addrs.Module, hcl.Diagnostics) {
		traversal, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
		diags := traversalDiags
		if traversalDiags.HasErrors() {
			return nil, nil, diags
		}

		target, targetDiags := addrs.ParseModule(traversal)
		diags = append(diags, targetDiags.ToHCL()...)
		if targetDiags.HasErrors() {
			return nil, nil, diags
		}

		return traversal, target, diags
	}

	mod := &OverrideModule{}

	content, diags := block.Body.Content(overrideModuleBlockSchema)

	if attr, exists := content.Attributes["target"]; exists {
		traversal, target, moreDiags := parseTarget(attr)
		mod.Target, mod.TargetParsed = traversal, target
		diags = append(diags, moreDiags...)
	}

	if attr, exists := content.Attributes["outputs"]; exists {
		outputs, moreDiags := parseObjectAttrWithNoVariables(attr)
		mod.Outputs, diags = outputs, append(diags, moreDiags...)
	}

	return mod, diags
}

// Some code of decodeMockProviderBlock function was copied from decodeProviderBlock.
func decodeMockProviderBlock(block *hcl.Block) (*MockProvider, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	content, moreDiags := block.Body.Content(mockProviderBlockSchema)
	diags = append(diags, moreDiags...)

	// Provider names must be localized. Produce an error with a message
	// indicating the action the user can take to fix this message if the local
	// name is not localized.
	name := block.Labels[0]
	nameDiags := checkProviderNameNormalized(name, block.DefRange)
	diags = append(diags, nameDiags...)
	if nameDiags.HasErrors() {
		// If the name is invalid then we mustn't produce a result because
		// downstreams could try to use it as a provider type and then crash.
		return nil, diags
	}

	provider := &MockProvider{
		Name:      name,
		NameRange: block.LabelRanges[0],
		DeclRange: block.DefRange,
	}

	if attr, exists := content.Attributes["alias"]; exists {
		valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias)
		diags = append(diags, valDiags...)
		provider.AliasRange = attr.Expr.Range().Ptr()

		if !hclsyntax.ValidIdentifier(provider.Alias) {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid mock provider configuration alias",
				Detail:   fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail),
				Subject:  provider.AliasRange,
			})
		}
	}

	var (
		managedResources = make(map[string]struct{})
		dataResources    = make(map[string]struct{})
	)

	for _, block := range content.Blocks {
		res, resDiags := decodeMockResourceBlock(block)
		diags = append(diags, resDiags...)
		if resDiags.HasErrors() {
			continue
		}

		resources := managedResources
		if res.Mode == addrs.DataResourceMode {
			resources = dataResources
		}

		if _, ok := resources[res.Type]; ok {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  fmt.Sprintf("Duplicated `%v` block", res.getBlockName()),
				Detail:   fmt.Sprintf("`%v.%v` is already defined in `mock_provider` block.", res.getBlockName(), res.Type),
				Subject:  provider.DeclRange.Ptr(),
			})
			continue
		}

		resources[res.Type] = struct{}{}

		provider.MockResources = append(provider.MockResources, res)
	}

	return provider, diags
}

func decodeMockResourceBlock(block *hcl.Block) (*MockResource, hcl.Diagnostics) {
	var mode addrs.ResourceMode

	switch block.Type {
	case blockNameMockResource:
		mode = addrs.ManagedResourceMode
	case blockNameMockData:
		mode = addrs.DataResourceMode
	default:
		panic("BUG: unsupported block type for mock resource: " + block.Type)
	}

	res := &MockResource{
		Mode: mode,
		Type: block.Labels[0],
	}

	content, diags := block.Body.Content(mockResourceBlockSchema)

	if attr, exists := content.Attributes["defaults"]; exists {
		v, moreDiags := parseObjectAttrWithNoVariables(attr)
		res.Defaults, diags = v, append(diags, moreDiags...)
	}

	return res, diags
}

func parseObjectAttrWithNoVariables(attr *hcl.Attribute) (map[string]cty.Value, hcl.Diagnostics) {
	attrVal, valDiags := attr.Expr.Value(nil)
	diags := valDiags
	if valDiags.HasErrors() {
		return nil, diags
	}

	if !attrVal.Type().IsObjectType() {
		return nil, append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Object expected",
			Detail:   fmt.Sprintf("The attribute `%v` must be an object.", attr.Name),
			Subject:  attr.Range.Ptr(),
		})
	}

	return attrVal.AsValueMap(), diags
}

func checkForDuplicatedOverrideResources(resources []*OverrideResource) hcl.Diagnostics {
	var diags hcl.Diagnostics

	overrideResources := make(map[string]struct{}, len(resources))
	for _, res := range resources {
		k := res.TargetParsed.String()

		if _, ok := overrideResources[k]; ok {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  fmt.Sprintf("Duplicated `%v` block", res.getBlockName()),
				Detail:   fmt.Sprintf("It is not allowed to have multiple `%v` blocks with the same target: `%v`.", res.getBlockName(), res.TargetParsed),
				Subject:  res.Target.SourceRange().Ptr(),
			})
			continue
		}

		overrideResources[k] = struct{}{}
	}

	return diags
}

func checkForDuplicatedOverrideModules(modules []*OverrideModule) hcl.Diagnostics {
	var diags hcl.Diagnostics

	overrideModules := make(map[string]struct{}, len(modules))
	for _, mod := range modules {
		k := mod.TargetParsed.String()

		if _, ok := overrideModules[k]; ok {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Duplicated `override_module` block",
				Detail:   fmt.Sprintf("It is not allowed to have multiple `override_module` blocks with the same target: `%v`.", mod.TargetParsed),
				Subject:  mod.Target.SourceRange().Ptr(),
			})
			continue
		}

		overrideModules[k] = struct{}{}
	}

	return diags
}

var testFileSchema = &hcl.BodySchema{
	Blocks: []hcl.BlockHeaderSchema{
		{
			Type:       "run",
			LabelNames: []string{"name"},
		},
		{
			Type:       "provider",
			LabelNames: []string{"name"},
		},
		{
			Type: "variables",
		},
		{
			Type: blockNameOverrideResource,
		},
		{
			Type: blockNameOverrideData,
		},
		{
			Type: blockNameOverrideModule,
		},
		{
			Type:       blockNameMockProvider,
			LabelNames: []string{"name"},
		},
	},
}

var testRunBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{Name: "command"},
		{Name: "providers"},
		{Name: "expect_failures"},
	},
	Blocks: []hcl.BlockHeaderSchema{
		{
			Type: "plan_options",
		},
		{
			Type: "assert",
		},
		{
			Type: "variables",
		},
		{
			Type: "module",
		},
		{
			Type: blockNameOverrideResource,
		},
		{
			Type: blockNameOverrideData,
		},
		{
			Type: blockNameOverrideModule,
		},
	},
}

var testRunOptionsBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{Name: "mode"},
		{Name: "refresh"},
		{Name: "replace"},
		{Name: "target"},
	},
}

var testRunModuleBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{Name: "source"},
		{Name: "version"},
	},
}

//nolint:gochecknoglobals // To follow existing code style.
var overrideResourceBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name:     "target",
			Required: true,
		},
		{
			Name:     "values",
			Required: false,
		},
	},
}

//nolint:gochecknoglobals // To follow existing code style.
var overrideModuleBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name:     "target",
			Required: true,
		},
		{
			Name:     "outputs",
			Required: false,
		},
	},
}

//nolint:gochecknoglobals // To follow existing code style.
var mockProviderBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name:     "alias",
			Required: false,
		},
	},
	Blocks: []hcl.BlockHeaderSchema{
		{
			Type:       blockNameMockResource,
			LabelNames: []string{"type"},
		},
		{
			Type:       blockNameMockData,
			LabelNames: []string{"type"},
		},
	},
}

//nolint:gochecknoglobals // To follow existing code style.
var mockResourceBlockSchema = &hcl.BodySchema{
	Attributes: []hcl.AttributeSchema{
		{
			Name: "defaults",
		},
	},
}
