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

package states

import (
	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/checks"
)

// CheckResults represents a summary snapshot of the status of a set of checks
// declared in configuration, updated after each OpenTofu Core run that
// changes the state or remote system in a way that might impact the check
// results.
//
// Unlike a checks.State, this type only tracks the overall results for
// each checkable object and doesn't aim to preserve the identity of individual
// checks in the configuration. For our UI reporting purposes, it is entire
// objects that pass or fail based on their declared checks; the individual
// checks have no durable identity between runs, and so are only a language
// design convenience to help authors describe various independent conditions
// with different failure messages each.
//
// CheckResults should typically be considered immutable once constructed:
// instead of updating it in-place,instead construct an entirely new
// CheckResults object based on a fresh checks.State.
type CheckResults struct {
	// ConfigResults has all of the individual check results grouped by the
	// configuration object they relate to.
	//
	// The top-level map here will always have a key for every configuration
	// object that includes checks at the time of evaluating the results,
	// even if there turned out to be no instances of that object and
	// therefore no individual check results.
	ConfigResults addrs.Map[addrs.ConfigCheckable, *CheckResultAggregate]
}

// CheckResultAggregate represents both the overall result for a particular
// configured object that has checks and the individual checkable objects
// it declared, if any.
type CheckResultAggregate struct {
	// Status is the aggregate status across all objects.
	//
	// Sometimes an error or check failure during planning will prevent
	// OpenTofu Core from even determining the individual checkable objects
	// associated with a downstream configuration object, and that situation is
	// described here by this Status being checks.StatusUnknown and there being
	// no elements in the ObjectResults field.
	//
	// That's different than OpenTofu Core explicitly reporting that there are
	// no instances of the config object (e.g. a resource with count = 0),
	// which leads to the aggregate status being checks.StatusPass while
	// ObjectResults is still empty.
	Status checks.Status

	ObjectResults addrs.Map[addrs.Checkable, *CheckResultObject]
}

// CheckResultObject is the check status for a single checkable object.
//
// This aggregates together all of the checks associated with a particular
// object into a single pass/fail/error/unknown result, because checkable
// objects have durable addresses that can survive between runs, but their
// individual checks do not. (Module authors are free to reorder their checks
// for a particular object in the configuration with no change in meaning.)
type CheckResultObject struct {
	// Status is the check status of the checkable object, derived from the
	// results of all of its individual checks.
	Status checks.Status

	// FailureMessages is an optional set of module-author-defined messages
	// describing the problems that the checks detected, for objects whose
	// status is checks.StatusFail.
	//
	// (checks.StatusError problems get reported as normal diagnostics during
	// evaluation instead, and so will not appear here.)
	FailureMessages []string
}

// NewCheckResults constructs a new states.CheckResults object that is a
// snapshot of the check statuses recorded in the given checks.State object.
//
// This should be called only after a OpenTofu Core run has completed and
// recorded any results from running the checks in the given object.
func NewCheckResults(source *checks.State) *CheckResults {
	ret := &CheckResults{
		ConfigResults: addrs.MakeMap[addrs.ConfigCheckable, *CheckResultAggregate](),
	}

	for _, configAddr := range source.AllConfigAddrs() {
		aggr := &CheckResultAggregate{
			Status:        source.AggregateCheckStatus(configAddr),
			ObjectResults: addrs.MakeMap[addrs.Checkable, *CheckResultObject](),
		}

		for _, objectAddr := range source.ObjectAddrs(configAddr) {
			obj := &CheckResultObject{
				Status:          source.ObjectCheckStatus(objectAddr),
				FailureMessages: source.ObjectFailureMessages(objectAddr),
			}
			aggr.ObjectResults.Put(objectAddr, obj)
		}

		ret.ConfigResults.Put(configAddr, aggr)
	}

	// If there aren't actually any configuration objects then we'll just
	// leave the map as a whole nil, because having it be zero-value makes
	// life easier for deep comparisons in unit tests elsewhere.
	if ret.ConfigResults.Len() == 0 {
		ret.ConfigResults.Elems = nil
	}

	return ret
}

// GetObjectResult looks up the result for a single object, or nil if there
// is no such object.
//
// In main code we shouldn't typically need to look up individual objects
// like this, since we'll usually be reporting check results in an aggregate
// form, but determining the result of a particular object is useful in our
// internal unit tests, and so this is here primarily for that purpose.
func (r *CheckResults) GetObjectResult(objectAddr addrs.Checkable) *CheckResultObject {
	configAddr := objectAddr.ConfigCheckable()

	aggr := r.ConfigResults.Get(configAddr)
	if aggr == nil {
		return nil
	}

	return aggr.ObjectResults.Get(objectAddr)
}

func (r *CheckResults) DeepCopy() *CheckResults {
	if r == nil {
		return nil
	}
	ret := &CheckResults{}
	if r.ConfigResults.Elems == nil {
		return ret
	}

	ret.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *CheckResultAggregate]()

	for _, configElem := range r.ConfigResults.Elems {
		aggr := &CheckResultAggregate{
			Status: configElem.Value.Status,
		}

		if configElem.Value.ObjectResults.Elems != nil {
			aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *CheckResultObject]()

			for _, objectElem := range configElem.Value.ObjectResults.Elems {
				result := &CheckResultObject{
					Status: objectElem.Value.Status,

					// NOTE: We don't deep-copy this slice because it's
					// immutable once constructed by convention.
					FailureMessages: objectElem.Value.FailureMessages,
				}
				aggr.ObjectResults.Put(objectElem.Key, result)
			}
		}

		ret.ConfigResults.Put(configElem.Key, aggr)
	}

	return ret
}

// ObjectAddrsKnown determines whether the set of objects recorded in this
// aggregate is accurate (true) or if it's incomplete as a result of the
// run being interrupted before instance expansion.
func (r *CheckResultAggregate) ObjectAddrsKnown() bool {
	if r.ObjectResults.Len() != 0 {
		// If there are any object results at all then we definitely know.
		return true
	}

	// If we don't have any object addresses then we distinguish a known
	// empty set of objects from an unknown set of objects by the aggregate
	// status being unknown.
	return r.Status != checks.StatusUnknown
}
