// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package graph

import (
	"context"
	"fmt"
	"log"
	"sort"
	"strings"
	"sync"

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

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend"
	"github.com/hashicorp/terraform/internal/command/arguments"
	"github.com/hashicorp/terraform/internal/command/views"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/didyoumean"
	"github.com/hashicorp/terraform/internal/lang"
	"github.com/hashicorp/terraform/internal/lang/langrefs"
	"github.com/hashicorp/terraform/internal/moduletest"
	"github.com/hashicorp/terraform/internal/moduletest/mocking"
	teststates "github.com/hashicorp/terraform/internal/moduletest/states"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/terraform"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// EvalContext is a container for context relating to the evaluation of a
// particular .tftest.hcl file.
// This context is used to track the various values that are available to the
// test suite, both from the test suite itself and from the results of the runs
// within the suite.
// The struct provides concurrency-safe access to the various maps it contains.
type EvalContext struct {
	// unparsedVariables and parsedVariables are the values for the variables
	// required by this test file. The parsedVariables will be populated as the
	// test graph is executed, while the unparsedVariables will be lazily
	// evaluated by each run block that needs them.
	unparsedVariables map[string]arguments.UnparsedVariableValue
	parsedVariables   terraform.InputValues
	variableStatus    map[string]moduletest.Status
	variablesLock     sync.Mutex

	// runBlocks caches all the known run blocks that this EvalContext manages.
	runBlocks   map[string]*moduletest.Run
	outputsLock sync.Mutex

	providers      map[addrs.RootProviderConfig]providers.Interface
	providerStatus map[addrs.RootProviderConfig]moduletest.Status
	providersLock  sync.Mutex

	// FileStates is a mapping of module keys to it's last applied state
	// file. This is tracked and returned to log state files of ongoing test
	// operations.
	FileStates map[string]*teststates.TestRunState
	stateLock  sync.Mutex

	// cancelContext and stopContext can be used to terminate the evaluation of the
	// test suite when a cancellation or stop signal is received.
	// cancelFunc and stopFunc are the corresponding functions to call to signal
	// the termination.
	cancelContext context.Context
	cancelFunc    context.CancelFunc
	stopContext   context.Context
	stopFunc      context.CancelFunc
	config        *configs.Config
	renderer      views.Test
	verbose       bool

	// mode and repair affect the behaviour of the cleanup process of the graph.
	//
	// in cleanup mode, the tests will actually be skipped and the cleanup nodes
	// are executed immediately. Normally, the skip_cleanup attributes will
	// be skipped in cleanup mode with all states being destroyed completely.
	//
	// in repair mode, the skip_cleanup attributes are still respected. this
	// means only states that were left behind due to an error will be
	// destroyed.
	mode moduletest.CommandMode

	deferralAllowed bool
	evalSem         terraform.Semaphore

	// repair is true if the test suite is being run in cleanup repair mode.
	// It is only set when in test cleanup mode.
	repair bool

	overrides    map[string]*mocking.Overrides
	overrideLock sync.Mutex
}

type EvalContextOpts struct {
	Verbose           bool
	Repair            bool
	Render            views.Test
	CancelCtx         context.Context
	StopCtx           context.Context
	UnparsedVariables map[string]arguments.UnparsedVariableValue
	Config            *configs.Config
	FileStates        map[string]*teststates.TestRunState
	Concurrency       int
	DeferralAllowed   bool
	Mode              moduletest.CommandMode
}

// NewEvalContext constructs a new graph evaluation context for use in
// evaluating the runs within a test suite.
// The context is initialized with the provided cancel and stop contexts, and
// these contexts can be used from external commands to signal the termination of the test suite.
func NewEvalContext(opts EvalContextOpts) *EvalContext {
	cancelCtx, cancel := context.WithCancel(opts.CancelCtx)
	stopCtx, stop := context.WithCancel(opts.StopCtx)
	return &EvalContext{
		unparsedVariables: opts.UnparsedVariables,
		parsedVariables:   make(terraform.InputValues),
		variableStatus:    make(map[string]moduletest.Status),
		variablesLock:     sync.Mutex{},
		runBlocks:         make(map[string]*moduletest.Run),
		outputsLock:       sync.Mutex{},
		providers:         make(map[addrs.RootProviderConfig]providers.Interface),
		providerStatus:    make(map[addrs.RootProviderConfig]moduletest.Status),
		providersLock:     sync.Mutex{},
		FileStates:        opts.FileStates,
		stateLock:         sync.Mutex{},
		cancelContext:     cancelCtx,
		cancelFunc:        cancel,
		stopContext:       stopCtx,
		stopFunc:          stop,
		config:            opts.Config,
		verbose:           opts.Verbose,
		repair:            opts.Repair,
		renderer:          opts.Render,
		mode:              opts.Mode,
		deferralAllowed:   opts.DeferralAllowed,
		evalSem:           terraform.NewSemaphore(opts.Concurrency),
		overrides:         make(map[string]*mocking.Overrides),
	}
}

// Renderer returns the renderer for the test suite.
func (ec *EvalContext) Renderer() views.Test {
	return ec.renderer
}

// Cancel signals to the runs in the test suite that they should stop evaluating
// the test suite, and return immediately.
func (ec *EvalContext) Cancel() {
	ec.cancelFunc()
}

// Cancelled returns true if the context has been stopped. The default cause
// of the error is context.Canceled.
func (ec *EvalContext) Cancelled() bool {
	return ec.cancelContext.Err() != nil
}

// Stop signals to the runs in the test suite that they should stop evaluating
// the test suite, and just skip.
func (ec *EvalContext) Stop() {
	ec.stopFunc()
}

func (ec *EvalContext) Stopped() bool {
	return ec.stopContext.Err() != nil
}

// Verbose returns true if the context is in verbose mode.
func (ec *EvalContext) Verbose() bool {
	return ec.verbose
}

func (ec *EvalContext) HclContext(references []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	runs := make(map[string]cty.Value)
	vars := make(map[string]cty.Value)

	for _, reference := range references {
		switch subject := reference.Subject.(type) {
		case addrs.Run:
			run, ok := ec.GetOutput(subject.Name)
			if !ok {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Reference to unknown run block",
					Detail:   fmt.Sprintf("The run block %q does not exist within this test file.", subject.Name),
					Subject:  reference.SourceRange.ToHCL().Ptr(),
				})
				continue
			}
			runs[subject.Name] = run

			value, valueDiags := reference.Remaining.TraverseRel(run)
			diags = diags.Append(valueDiags)
			if valueDiags.HasErrors() {
				continue
			}

			if !value.IsWhollyKnown() {
				// This is not valid, we cannot allow users to pass unknown
				// values into references within the test file. There's just
				// going to be difficult and confusing errors later if this
				// happens.
				//
				// When reporting this we assume that it's happened because
				// the prior run was a plan-only run and that some of its
				// output values were not known. If this arises for a
				// run that performed a full apply then this is a bug in
				// Terraform's modules runtime, because unknown output
				// values should not be possible in that case.
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Reference to unknown value",
					Detail:   fmt.Sprintf("The value for %s is unknown. Run block %q is executing a \"plan\" operation, and the specified output value is only known after apply.", reference.DisplayString(), subject.Name),
					Subject:  reference.SourceRange.ToHCL().Ptr(),
				})
				continue
			}

		case addrs.InputVariable:
			if variable, ok := ec.GetVariable(subject.Name); ok {
				vars[subject.Name] = variable.Value
				continue
			}

			if variable, moreDiags := ec.EvaluateUnparsedVariableDeprecated(subject.Name, reference); variable != nil {
				diags = diags.Append(moreDiags)
				vars[subject.Name] = variable.Value
				continue
			}

			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Reference to unavailable variable",
				Detail:   fmt.Sprintf("The input variable %q does not exist within this test file.", subject.Name),
				Subject:  reference.SourceRange.ToHCL().Ptr(),
			})
			continue

		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid reference",
				Detail:   "You can only reference run blocks and variables from within Terraform Test files.",
				Subject:  reference.SourceRange.ToHCL().Ptr(),
			})
		}
	}

	return &hcl.EvalContext{
		Variables: map[string]cty.Value{
			"run": cty.ObjectVal(runs),
			"var": cty.ObjectVal(vars),
		},
		Functions: lang.TestingFunctions(),
	}, diags
}

// EvaluateRun processes the assertions inside the provided configs.TestRun against
// the run results, returning a status, an object value representing the output
// values from the module under test, and diagnostics describing any problems.
//
// extraVariableVals, if provided, overlays the input variables that are
// already available in resultScope in case there are additional input
// variables that were defined only for use in the test suite. Any variable
// not defined in extraVariableVals will be evaluated through resultScope instead.
func (ec *EvalContext) EvaluateRun(run *configs.TestRun, module *configs.Module, resultScope *lang.Scope, extraVariableVals terraform.InputValues) (moduletest.Status, cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	// We need a derived evaluation scope that also supports referring to
	// the prior run output values using the "run.NAME" syntax.
	evalData := &evaluationData{
		ctx:       ec,
		module:    module,
		current:   resultScope.Data,
		extraVars: extraVariableVals,
	}
	scope := &lang.Scope{
		Data:          evalData,
		ParseRef:      addrs.ParseRefFromTestingScope,
		SourceAddr:    resultScope.SourceAddr,
		BaseDir:       resultScope.BaseDir,
		PureOnly:      resultScope.PureOnly,
		PlanTimestamp: resultScope.PlanTimestamp,
		ExternalFuncs: resultScope.ExternalFuncs,
	}

	log.Printf("[TRACE] EvalContext.Evaluate for %s", run.Name)

	// We're going to assume the run has passed, and then if anything fails this
	// value will be updated.
	status := moduletest.Pass

	// Now validate all the assertions within this run block.
	for i, rule := range run.CheckRules {
		var ruleDiags tfdiags.Diagnostics

		refs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.Condition)
		ruleDiags = ruleDiags.Append(moreDiags)
		moreRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.ErrorMessage)
		ruleDiags = ruleDiags.Append(moreDiags)
		refs = append(refs, moreRefs...)

		// We want to emit diagnostics if users are using ephemeral resources in their checks
		// as they are not supported since they are closed before this is evaluated.
		// We do not remove the diagnostic about the ephemeral resource being closed already as it
		// might be useful to the user.
		ruleDiags = ruleDiags.Append(diagsForEphemeralResources(refs))

		hclCtx, moreDiags := scope.EvalContext(refs)
		ruleDiags = ruleDiags.Append(moreDiags)
		if moreDiags.HasErrors() {
			// if we can't evaluate the context properly, we can't evaluate the rule
			// we add the diagnostics to the main diags and continue to the next rule
			log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s is invalid, could not evalaute the context, so cannot evaluate it", i, run.Name)
			status = status.Merge(moduletest.Error)
			diags = diags.Append(ruleDiags)
			continue
		}

		errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil)
		ruleDiags = ruleDiags.Append(moreDiags)

		errorMessage, _ = errorMessage.Unmark()
		errorMessageStr := strings.TrimSpace(errorMessage.AsString())

		runVal, hclDiags := rule.Condition.Value(hclCtx)
		ruleDiags = ruleDiags.Append(hclDiags)

		diags = diags.Append(ruleDiags)
		if ruleDiags.HasErrors() {
			log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s is invalid, so cannot evaluate it", i, run.Name)
			status = status.Merge(moduletest.Error)
			continue
		}

		if runVal.IsNull() {
			status = status.Merge(moduletest.Error)
			diags = diags.Append(&hcl.Diagnostic{
				Severity:    hcl.DiagError,
				Summary:     "Invalid condition run",
				Detail:      "Condition expression must return either true or false, not null.",
				Subject:     rule.Condition.Range().Ptr(),
				Expression:  rule.Condition,
				EvalContext: hclCtx,
			})
			log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has null condition result", i, run.Name)
			continue
		}

		if !runVal.IsKnown() {
			status = status.Merge(moduletest.Error)
			diags = diags.Append(&hcl.Diagnostic{
				Severity:    hcl.DiagError,
				Summary:     "Unknown condition value",
				Detail:      "Condition expression could not be evaluated at this time. This means you have executed a `run` block with `command = plan` and one of the values your condition depended on is not known until after the plan has been applied. Either remove this value from your condition, or execute an `apply` command from this `run` block. Alternatively, if there is an override for this value, you can make it available during the plan phase by setting `override_during = plan` in the `override_` block.",
				Subject:     rule.Condition.Range().Ptr(),
				Expression:  rule.Condition,
				EvalContext: hclCtx,
			})
			log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has unknown condition result", i, run.Name)
			continue
		}

		var err error
		if runVal, err = convert.Convert(runVal, cty.Bool); err != nil {
			status = status.Merge(moduletest.Error)
			diags = diags.Append(&hcl.Diagnostic{
				Severity:    hcl.DiagError,
				Summary:     "Invalid condition run",
				Detail:      fmt.Sprintf("Invalid condition run value: %s.", tfdiags.FormatError(err)),
				Subject:     rule.Condition.Range().Ptr(),
				Expression:  rule.Condition,
				EvalContext: hclCtx,
			})
			log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has non-boolean condition result", i, run.Name)
			continue
		}

		// If the runVal refers to any sensitive values, then we'll have a
		// sensitive mark on the resulting value.
		runVal, _ = runVal.Unmark()

		if runVal.False() {
			log.Printf("[TRACE] EvalContext.Evaluate: test assertion failed for %s assertion %d", run.Name, i)
			status = status.Merge(moduletest.Fail)
			diags = diags.Append(&hcl.Diagnostic{
				Severity:    hcl.DiagError,
				Summary:     "Test assertion failed",
				Detail:      errorMessageStr,
				Subject:     rule.Condition.Range().Ptr(),
				Expression:  rule.Condition,
				EvalContext: hclCtx,
				// Diagnostic can be identified as originating from a failing test assertion.
				// Also, values that are ephemeral, sensitive, or unknown are replaced with
				// redacted values in renderings of the diagnostic.
				Extra: DiagnosticCausedByTestFailure{Verbose: ec.verbose},
			})
			continue
		} else {
			log.Printf("[TRACE] EvalContext.Evaluate: test assertion succeeded for %s assertion %d", run.Name, i)
		}
	}

	// Our result includes an object representing all of the output values
	// from the module we've just tested, which will then be available in
	// any subsequent test cases in the same test suite.
	outputVals := make(map[string]cty.Value, len(module.Outputs))
	runRng := tfdiags.SourceRangeFromHCL(run.DeclRange)
	for _, oc := range module.Outputs {
		addr := oc.Addr()
		v, moreDiags := scope.Data.GetOutput(addr, runRng)
		diags = diags.Append(moreDiags)
		if v == cty.NilVal {
			v = cty.NullVal(cty.DynamicPseudoType)
		}
		outputVals[addr.Name] = v
	}

	return status, cty.ObjectVal(outputVals), diags
}

// EvaluateUnparsedVariable accepts a variable name and a variable definition
// and checks if we have external unparsed variables that match the given
// configuration. If no variable was provided, we'll return a nil
// input value.
func (ec *EvalContext) EvaluateUnparsedVariable(name string, config *configs.Variable) (*terraform.InputValue, tfdiags.Diagnostics) {
	variable, exists := ec.unparsedVariables[name]
	if !exists {
		return nil, nil
	}

	value, diags := variable.ParseVariableValue(config.ParsingMode)
	if diags.HasErrors() {
		value = &terraform.InputValue{
			Value: cty.DynamicVal,
		}
	}

	return value, diags
}

// EvaluateUnparsedVariableDeprecated accepts a variable name without a variable
// definition and attempts to parse it.
//
// This function represents deprecated functionality within the testing
// framework. It is no longer valid to reference external variables without a
// definition, but we do our best here and provide a warning that this will
// become completely unsupported in the future.
func (ec *EvalContext) EvaluateUnparsedVariableDeprecated(name string, ref *addrs.Reference) (*terraform.InputValue, tfdiags.Diagnostics) {
	variable, exists := ec.unparsedVariables[name]
	if !exists {
		return nil, nil
	}

	var diags tfdiags.Diagnostics
	diags = diags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagWarning,
		Summary:  "Variable referenced without definition",
		Detail:   fmt.Sprintf("Variable %q was referenced without providing a definition. Referencing undefined variables within Terraform Test files is deprecated, please add a `variable` block into the relevant test file to provide a definition for the variable. This will become required in future versions of Terraform.", name),
		Subject:  ref.SourceRange.ToHCL().Ptr(),
	})

	// For backwards-compatibility reasons we do also have to support trying
	// to parse the global variables without a configuration. We introduced the
	// file-level variable definitions later, and users were already using
	// global variables so we do need to keep supporting this use case.

	// Otherwise, we have no configuration so we're going to try both parsing
	// modes.

	value, moreDiags := variable.ParseVariableValue(configs.VariableParseHCL)
	diags = diags.Append(moreDiags)
	if !moreDiags.HasErrors() {
		// then good! we can just return these values directly.
		return value, diags
	}

	// otherwise, we'll try the other one.

	value, moreDiags = variable.ParseVariableValue(configs.VariableParseLiteral)
	diags = diags.Append(moreDiags)
	if moreDiags.HasErrors() {
		// as usual make sure we still provide something for this value.
		value = &terraform.InputValue{
			Value: cty.DynamicVal,
		}
	}
	return value, diags
}

func (ec *EvalContext) SetVariable(name string, val *terraform.InputValue) {
	ec.variablesLock.Lock()
	defer ec.variablesLock.Unlock()

	ec.parsedVariables[name] = val
}

func (ec *EvalContext) GetVariable(name string) (*terraform.InputValue, bool) {
	ec.variablesLock.Lock()
	defer ec.variablesLock.Unlock()

	variable, ok := ec.parsedVariables[name]
	return variable, ok
}

func (ec *EvalContext) SetVariableStatus(address string, status moduletest.Status) {
	ec.variablesLock.Lock()
	defer ec.variablesLock.Unlock()
	ec.variableStatus[address] = status
}

func (ec *EvalContext) AddRunBlock(run *moduletest.Run) {
	ec.outputsLock.Lock()
	defer ec.outputsLock.Unlock()
	ec.runBlocks[run.Name] = run
}

func (ec *EvalContext) GetOutput(name string) (cty.Value, bool) {
	ec.outputsLock.Lock()
	defer ec.outputsLock.Unlock()
	output, ok := ec.runBlocks[name]
	if !ok {
		return cty.NilVal, false
	}
	return output.Outputs, true
}

func (ec *EvalContext) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Provider {
	return ec.config.ProviderForConfigAddr(addr)
}

func (ec *EvalContext) LocalNameForProvider(addr addrs.RootProviderConfig) string {
	return ec.config.Module.LocalNameForProvider(addr.Provider)
}

func (ec *EvalContext) GetProvider(addr addrs.RootProviderConfig) (providers.Interface, bool) {
	ec.providersLock.Lock()
	defer ec.providersLock.Unlock()
	provider, ok := ec.providers[addr]
	return provider, ok
}

func (ec *EvalContext) SetProvider(addr addrs.RootProviderConfig, provider providers.Interface) {
	ec.providersLock.Lock()
	defer ec.providersLock.Unlock()
	ec.providers[addr] = provider
}

func (ec *EvalContext) SetProviderStatus(addr addrs.RootProviderConfig, status moduletest.Status) {
	ec.providersLock.Lock()
	defer ec.providersLock.Unlock()
	ec.providerStatus[addr] = status
}

func diagsForEphemeralResources(refs []*addrs.Reference) (diags tfdiags.Diagnostics) {
	for _, ref := range refs {
		switch v := ref.Subject.(type) {
		case addrs.ResourceInstance:
			if v.Resource.Mode == addrs.EphemeralResourceMode {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Ephemeral resources cannot be asserted",
					Detail:   "Ephemeral resources are closed when the test is finished, and are not available within the test context for assertions.",
					Subject:  ref.SourceRange.ToHCL().Ptr(),
				})
			}
		}
	}
	return diags
}

func (ec *EvalContext) SetFileState(key string, run *moduletest.Run, state *states.State, reason teststates.StateReason) {
	ec.stateLock.Lock()
	defer ec.stateLock.Unlock()

	current := ec.getState(key)

	// Whatever happens we're going to record the latest state for this key.
	current.State = state
	current.Manifest.Reason = reason

	if run.Config.SkipCleanup {
		// if skip cleanup is set on the run block, we're going to track it
		// as the thing to target regardless of what else might be true.
		current.Run = run

		// we'll mark the state as being restored to the current run block
		// if (a) we're not in cleanup mode (meaning everything should be
		// destroyed) or (b) we are in cleanup mode but with the repair flag
		// which means that only errored states should be destroyed.
		current.RestoreState = ec.mode != moduletest.CleanupMode || ec.repair
	} else if !current.RestoreState {
		// otherwise, only set the new run block if we haven't been told the
		// earlier run block is more relevant.
		current.Run = run
	}
}

// GetState retrieves the current state for the specified key, exactly as it
// specified within the current cache.
func (ec *EvalContext) GetState(key string) *teststates.TestRunState {
	ec.stateLock.Lock()
	defer ec.stateLock.Unlock()
	return ec.getState(key)
}

func (ec *EvalContext) getState(key string) *teststates.TestRunState {
	current := ec.FileStates[key]
	if current == nil {
		// this shouldn't happen, all the states must be initialised prior to
		// the evaluation context being created.
		//
		// panic here, where the origin of the bug is instead of returning a
		// null state to panic later.
		panic("null state found in test execution")
	}
	return current
}

// LoadState returns the correct state for the specified run block. This differs
// from GetState in that it will load the state from any remote backend
// specified within the run block rather than simply retrieve the cached state
// (which might be empty for a run block with a backend if it hasn't executed
// yet).
func (ec *EvalContext) LoadState(run *configs.TestRun) (*states.State, error) {
	ec.stateLock.Lock()
	defer ec.stateLock.Unlock()

	current := ec.getState(run.StateKey)

	if run.Backend != nil {
		// Then we'll load the state from the backend instead of just using
		// whatever was in the state.

		stmgr, sDiags := current.Backend.StateMgr(backend.DefaultStateName)
		if sDiags.HasErrors() {
			return nil, sDiags.Err()
		}

		if err := stmgr.RefreshState(); err != nil {
			return nil, err
		}

		return stmgr.State(), nil
	}

	return current.State, nil
}

// ReferencesCompleted returns true if all the listed references were actually
// executed successfully. This allows nodes in the graph to decide if they
// should execute or not based on the status of their references.
func (ec *EvalContext) ReferencesCompleted(refs []*addrs.Reference) bool {
	for _, ref := range refs {
		switch ref := ref.Subject.(type) {
		case addrs.Run:
			ec.outputsLock.Lock()
			if run, ok := ec.runBlocks[ref.Name]; ok {
				if run.Status != moduletest.Pass && run.Status != moduletest.Fail {
					ec.outputsLock.Unlock()

					// see also prior runs completed

					return false
				}
			}
			ec.outputsLock.Unlock()
		case addrs.InputVariable:
			ec.variablesLock.Lock()
			if vStatus, ok := ec.variableStatus[ref.Name]; ok && (vStatus == moduletest.Skip || vStatus == moduletest.Error) {
				ec.variablesLock.Unlock()
				return false
			}
			ec.variablesLock.Unlock()
		}
	}
	return true
}

// ProvidersCompleted ensures that all required providers were properly
// initialised.
func (ec *EvalContext) ProvidersCompleted(providers map[addrs.RootProviderConfig]providers.Interface) bool {
	ec.providersLock.Lock()
	defer ec.providersLock.Unlock()

	for provider := range providers {
		if status, ok := ec.providerStatus[provider]; ok {
			if status == moduletest.Skip || status == moduletest.Error {
				return false
			}
		}
	}
	return true
}

// PriorRunsCompleted checks a list of run blocks against our internal log of
// completed run blocks and makes sure that any that do exist successfully
// executed to completion.
//
// Note that run blocks that are not in the list indicate a bad reference,
// which we ignore here. This is actually the problem of the caller to identify
// and error.
func (ec *EvalContext) PriorRunsCompleted(runs map[string]*moduletest.Run) bool {
	ec.outputsLock.Lock()
	defer ec.outputsLock.Unlock()

	for name := range runs {
		if run, ok := ec.runBlocks[name]; ok {
			if run.Status != moduletest.Pass && run.Status != moduletest.Fail {

				// pass and fail indicate the run block still executed the plan
				// or apply operate and wrote outputs. fail means the
				// post-execution checks failed, but we still had data to check.
				// this is in contrast to pending, skip, or error which indicate
				// that we never even wrote data for this run block.

				return false
			}
		}
	}
	return true
}

func (ec *EvalContext) SetOverrides(run *moduletest.Run, overrides *mocking.Overrides) {
	ec.overrideLock.Lock()
	defer ec.overrideLock.Unlock()
	ec.overrides[run.Name] = overrides
}

func (ec *EvalContext) GetOverrides(runName string) *mocking.Overrides {
	ec.overrideLock.Lock()
	defer ec.overrideLock.Unlock()
	return ec.overrides[runName]
}

// evaluationData augments an underlying lang.Data -- presumably resulting
// from a terraform.Context.PlanAndEval or terraform.Context.ApplyAndEval call --
// with results from prior runs that should therefore be available when
// evaluating expressions written inside a "run" block.
type evaluationData struct {
	ctx       *EvalContext
	module    *configs.Module
	current   lang.Data
	extraVars terraform.InputValues
}

var _ lang.Data = (*evaluationData)(nil)

// GetCheckBlock implements lang.Data.
func (d *evaluationData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetCheckBlock(addr, rng)
}

// GetCountAttr implements lang.Data.
func (d *evaluationData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetCountAttr(addr, rng)
}

// GetForEachAttr implements lang.Data.
func (d *evaluationData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetForEachAttr(addr, rng)
}

// GetInputVariable implements lang.Data.
func (d *evaluationData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	if extra, exists := d.extraVars[addr.Name]; exists {
		return extra.Value, nil
	}
	return d.current.GetInputVariable(addr, rng)
}

// GetLocalValue implements lang.Data.
func (d *evaluationData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetLocalValue(addr, rng)
}

// GetModule implements lang.Data.
func (d *evaluationData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetModule(addr, rng)
}

// GetOutput implements lang.Data.
func (d *evaluationData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetOutput(addr, rng)
}

// GetPathAttr implements lang.Data.
func (d *evaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetPathAttr(addr, rng)
}

// GetResource implements lang.Data.
func (d *evaluationData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetResource(addr, rng)
}

// GetRunBlock implements lang.Data.
func (d *evaluationData) GetRunBlock(addr addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	ret, exists := d.ctx.GetOutput(addr.Name)
	if !exists {
		ret = cty.DynamicVal
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Reference to undeclared run block",
			Detail:   fmt.Sprintf("There is no run %q declared in this test suite.", addr.Name),
			Subject:  rng.ToHCL().Ptr(),
		})
	}
	if ret == cty.NilVal {
		// An explicit nil value indicates that the block was declared but
		// hasn't yet been visited.
		ret = cty.DynamicVal
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Reference to unevaluated run block",
			Detail:   fmt.Sprintf("The run %q block has not yet been evaluated, so its results are not available here.", addr.Name),
			Subject:  rng.ToHCL().Ptr(),
		})
	}
	return ret, diags
}

// GetTerraformAttr implements lang.Data.
func (d *evaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
	return d.current.GetTerraformAttr(addr, rng)
}

// StaticValidateReferences implements lang.Data.
func (d *evaluationData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics {
	// We only handle addrs.Run directly here, with everything else delegated
	// to the underlying Data object to deal with.
	var diags tfdiags.Diagnostics
	for _, ref := range refs {
		switch ref.Subject.(type) {
		case addrs.Run:
			diags = diags.Append(d.staticValidateRunRef(ref))
		default:
			diags = diags.Append(d.current.StaticValidateReferences([]*addrs.Reference{ref}, self, source))
		}
	}
	return diags
}

func (d *evaluationData) staticValidateRunRef(ref *addrs.Reference) tfdiags.Diagnostics {
	d.ctx.outputsLock.Lock()
	defer d.ctx.outputsLock.Unlock()

	var diags tfdiags.Diagnostics

	addr := ref.Subject.(addrs.Run)

	if _, exists := d.ctx.runBlocks[addr.Name]; !exists {
		var suggestions []string
		for altAddr := range d.ctx.runBlocks {
			suggestions = append(suggestions, altAddr)
		}
		sort.Strings(suggestions)
		suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
		if suggestion != "" {
			suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
		}
		// A totally absent priorVals means that there is no run block with
		// the given name at all. If it was declared but hasn't yet been
		// evaluated then it would have an entry set to cty.NilVal.
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Reference to undeclared run block",
			Detail:   fmt.Sprintf("There is no run %q declared in this test suite.%s", addr.Name, suggestion),
			Subject:  ref.SourceRange.ToHCL().Ptr(),
		})
	}

	return diags
}
