/*
Copyright The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package validation

import (
	"context"
	"fmt"
	"slices"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/api/operation"
	"k8s.io/apimachinery/pkg/api/validate"
	"k8s.io/apimachinery/pkg/api/validation"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	validation2 "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation/field"
	apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
	"k8s.io/kubernetes/pkg/apis/lifecycle"
	"k8s.io/utils/clock"
	"k8s.io/utils/ptr"
)

const defaultEvictionResponderCount = 1 // EvictionResponderImperativeEviction
// more than .pod.spec.evictionResponders to account for default responders
const maxEvictionResponders = apivalidation.MaxPodEvictionResponders + defaultEvictionResponderCount

// ValidateEvictionRequest validates an EvictionRequest.
func ValidateEvictionRequest(evictionRequest *lifecycle.EvictionRequest) field.ErrorList {
	validateLongName := func(fldPath *field.Path, name string) field.ErrorList {
		return validate.LongName(context.Background(), operation.Operation{}, fldPath, &name, nil).MarkCoveredByDeclarative()
	}
	allErrs := apivalidation.ValidateObjectMetaWithOpts(&evictionRequest.ObjectMeta, true, validateLongName, field.NewPath("metadata"))
	// Spec validation is covered by DV (Validate_EvictionRequestSpec)
	return allErrs
}

// ValidateEvictionRequestUpdate validates an EvictionRequest.
func ValidateEvictionRequestUpdate(evictionRequest, oldEvictionRequest *lifecycle.EvictionRequest) field.ErrorList {
	allErrs := apivalidation.ValidateObjectMetaUpdate(&evictionRequest.ObjectMeta, &oldEvictionRequest.ObjectMeta, field.NewPath("metadata"))
	// Spec validation is covered by DV (Validate_EvictionRequestSpec)
	return allErrs
}

// ValidateEvictionRequestStatusUpdate validates an EvictionRequest Status update.
func ValidateEvictionRequestStatusUpdate(evictionRequest, oldEvictionRequest *lifecycle.EvictionRequest) field.ErrorList {
	allErrs := apivalidation.ValidateObjectMetaUpdate(&evictionRequest.ObjectMeta, &oldEvictionRequest.ObjectMeta, field.NewPath("metadata"))
	allErrs = append(allErrs, ValidateEvictionRequestStatus(&evictionRequest.Status, &oldEvictionRequest.Status, field.NewPath("status"))...)
	return allErrs
}

// ValidateEvictionRequestStatus validates an EvictionRequest Status.
func ValidateEvictionRequestStatus(status, oldStatus *lifecycle.EvictionRequestStatus, fldPath *field.Path) field.ErrorList {
	var allErrs field.ErrorList

	// observedGeneration covered by DV

	// conditions
	conditionsPath := fldPath.Child("conditions")
	// conditions validation covered by DV
	allErrs = append(allErrs, validation2.ValidateConditions(status.Conditions, conditionsPath)...)

	return allErrs
}

// ValidateEviction validates an Eviction.
func ValidateEviction(eviction *lifecycle.Eviction) field.ErrorList {
	validateLongName := func(fldPath *field.Path, name string) field.ErrorList {
		return validate.LongName(context.Background(), operation.Operation{}, fldPath, &name, nil).MarkCoveredByDeclarative()
	}
	allErrs := apivalidation.ValidateObjectMetaWithOpts(&eviction.ObjectMeta, true, validateLongName, field.NewPath("metadata"))
	// Spec validation is covered by DV (Validate_EvictionSpec)
	return allErrs
}

// ValidateEvictionUpdate validates an Eviction.
func ValidateEvictionUpdate(eviction, oldEviction *lifecycle.Eviction) field.ErrorList {
	allErrs := apivalidation.ValidateObjectMetaUpdate(&eviction.ObjectMeta, &oldEviction.ObjectMeta, field.NewPath("metadata"))
	// Spec validation is covered by DV (Validate_EvictionSpec)
	return allErrs
}

type EvictionStatusValidationOptions struct {
	Clock clock.PassiveClock
}

// ValidateEvictionStatusUpdate validates an Eviction Status update.
func ValidateEvictionStatusUpdate(eviction, oldEviction *lifecycle.Eviction, opts EvictionStatusValidationOptions) field.ErrorList {
	allErrs := apivalidation.ValidateObjectMetaUpdate(&eviction.ObjectMeta, &oldEviction.ObjectMeta, field.NewPath("metadata"))
	allErrs = append(allErrs, ValidateEvictionStatus(&eviction.Status, &oldEviction.Status, field.NewPath("status"), opts)...)
	return allErrs
}

// ValidateEvictionStatus validates an Eviction Status.
func ValidateEvictionStatus(status, oldStatus *lifecycle.EvictionStatus, fldPath *field.Path, opts EvictionStatusValidationOptions) field.ErrorList {
	var allErrs field.ErrorList

	// observedGeneration covered by DV

	// conditions
	conditionsPath := fldPath.Child("conditions")
	// conditions validation covered by DV
	allErrs = append(allErrs, validation2.ValidateConditions(status.Conditions, conditionsPath)...)
	isEvicted := meta.IsStatusConditionTrue(status.Conditions, string(lifecycle.EvictionConditionTargetEvicted))
	isFailed := meta.IsStatusConditionTrue(status.Conditions, string(lifecycle.EvictionConditionFailed))
	for _, oldCondition := range oldStatus.Conditions {
		if oldCondition.Type == string(lifecycle.EvictionConditionTargetEvicted) || oldCondition.Type == string(lifecycle.EvictionConditionFailed) {
			newCondition := meta.FindStatusCondition(status.Conditions, oldCondition.Type)
			if oldCondition.Status == v1.ConditionTrue && (newCondition == nil || newCondition.Status == v1.ConditionFalse || oldCondition.Reason != newCondition.Reason) {
				allErrs = append(allErrs, field.Invalid(conditionsPath, status.Conditions, fmt.Sprintf("%s condition status cannot be reverted", oldCondition.Type)))
				if oldCondition.Type == string(lifecycle.EvictionConditionTargetEvicted) {
					isEvicted = true // do not use invalid condition state for next validations
				}
				if oldCondition.Type == string(lifecycle.EvictionConditionFailed) {
					isFailed = true // do not use invalid condition state for next validations
				}
			}
		}
	}

	// requesters covered by DV (Validate_EvictionStatus)

	// targetResponders, and responders
	allErrs = append(allErrs, ValidateAllEvictionStatusResponderFields(status, oldStatus, fldPath, EvictionStatusRespondersValidationOptions{
		Clock:     opts.Clock,
		IsEvicted: isEvicted,
		IsFailed:  isFailed,
	})...)

	return allErrs
}

type EvictionStatusRespondersValidationOptions struct {
	Clock     clock.PassiveClock
	IsEvicted bool
	IsFailed  bool
}

// ValidateAllEvictionStatusResponderFields validates .status.targetResponders and .status.responders
//
// Multiple actors are expected to update the status (eviction-controller, responders and requesters updating conditions).
// We have to emulate the eviction-controller behavior, to prevent invalid updates by other misbehaving/malicious actors.
//  1. .status.targetResponders and .status.responders should be set first.
//  2. Responders' state transitions from Inactive to Active, and from Active to Interrupted, Canceled or, Complete
//     gradually in the order of the responder priorities This is not reversible.
//  3. Responders' state transition from Active to Interrupted, Canceled or, Complete when
//     .status.responders[].completionTime is set or when heartbeat is exceeded. This is not reversible.
//  4. .status.responders items cannot be removed once set. Only active responders can mutate it.
//  5. The controller can mark an Eviction as Evicted or Canceled via conditions. We then have to allow removal
//     from activeResponders and prevent any new ones.
func ValidateAllEvictionStatusResponderFields(status, oldStatus *lifecycle.EvictionStatus, fldPath *field.Path, opts EvictionStatusRespondersValidationOptions) field.ErrorList {
	var allErrs field.ErrorList
	heartbeatDeadline := time.Minute * 20
	allowedTimeSkew := time.Second * 30
	allowedMaxExpectedCompletionTime := time.Hour * 24 * 365 * 10 // 10 years

	statusResponders := append(make([]lifecycle.ResponderStatus, 0, len(status.Responders)), status.Responders...)

	// targetResponders
	allErrs = append(allErrs, ValidateEvictionTargetResponders(status.TargetResponders, oldStatus.TargetResponders, fldPath.Child("targetResponders"), ValidateEvictionTargetRespondersOptions{
		Clock:             opts.Clock,
		IsFailed:          opts.IsFailed,
		IsEvicted:         opts.IsEvicted,
		StatusResponders:  statusResponders,
		HeartbeatDeadline: heartbeatDeadline,
		AllowedTimeSkew:   allowedTimeSkew,
	})...)

	// responders
	allErrs = append(allErrs, ValidateEvictionStatusResponders(status.Responders, oldStatus.Responders, fldPath.Child("responders"), ValidateEvictionStatusRespondersOptions{
		Clock:                     opts.Clock,
		TargetResponders:          status.TargetResponders,
		AllowedTimeSkew:           allowedTimeSkew,
		MaxExpectedCompletionTime: allowedMaxExpectedCompletionTime,
	})...)

	return allErrs
}

type ValidateEvictionTargetRespondersOptions struct {
	Clock             clock.PassiveClock
	IsEvicted         bool
	IsFailed          bool
	StatusResponders  []lifecycle.ResponderStatus
	HeartbeatDeadline time.Duration
	AllowedTimeSkew   time.Duration
}

func ValidateEvictionTargetResponders(targetResponders, oldTargetResponders []lifecycle.TargetResponder, fldPath *field.Path, opts ValidateEvictionTargetRespondersOptions) field.ErrorList {
	var allErrs field.ErrorList
	if len(oldTargetResponders) != 0 {
		if len(targetResponders) != len(oldTargetResponders) {
			return append(allErrs, field.Invalid(fldPath, targetResponders, "must preserve the same length and the same keys"))
		}
		targetRespondersNames := sets.New[string]()
		for _, responder := range targetResponders {
			targetRespondersNames.Insert(responder.Name)
		}
		for _, oldTargetResponder := range oldTargetResponders {
			if !targetRespondersNames.Has(oldTargetResponder.Name) {
				return append(allErrs, field.Invalid(fldPath, targetResponders, "must preserve the same keys"))
			}
		}
	}
	if len(targetResponders) == 0 {
		// not initialized yet
		return allErrs
	}

	if len(targetResponders) > maxEvictionResponders {
		// covered by declarative validation; exit early
		return allErrs
	}

	// simulate declarative error for early exit
	uniqueErrors := validate.ValSliceUnique(context.TODO(), operation.Operation{}, fldPath, targetResponders, nil,
		func(a *lifecycle.TargetResponder, b *lifecycle.TargetResponder) bool { return a.Name == b.Name })
	if len(uniqueErrors) > 0 {
		// does not make sense to check for state transition with duplicates
		return allErrs
	}
	statusResponders := map[string]*lifecycle.ResponderStatus{}
	for _, responder := range opts.StatusResponders {
		statusResponders[responder.Name] = &responder
	}
	targetRespondersOriginalIdx := map[string]int{}
	for i, responder := range targetResponders {
		targetRespondersOriginalIdx[responder.Name] = i
	}

	// Sort responders first, then use ordered progression to simplify the processing.
	// This means lower indexes are Activated first, and the last index is processed last.
	sortedOldTargetResponders := append([]lifecycle.TargetResponder{}, oldTargetResponders...)
	SortTargetResponders(sortedOldTargetResponders)

	sortedTargetResponders := append([]lifecycle.TargetResponder{}, targetResponders...)
	SortTargetResponders(sortedTargetResponders)

	lastActiveIdx := -1
	for i, responder := range sortedOldTargetResponders {
		if responder.State == lifecycle.ResponderStateActive {
			lastActiveIdx = i
			break
		}
	}
	hasNeverBeenActive := len(sortedOldTargetResponders) == 0 // we should Activate during the first sync
	hasFoundLastActive := lastActiveIdx != -1
	activeChanged := hasFoundLastActive && sortedTargetResponders[lastActiveIdx].State != lifecycle.ResponderStateActive
	isFinal := opts.IsEvicted || opts.IsFailed

	lastOldResponderState := lifecycle.ResponderStateInactive
	if len(sortedOldTargetResponders) > 0 {
		lastOldResponderState = sortedOldTargetResponders[len(sortedOldTargetResponders)-1].State
	}
	isFinalOrAllProcessed := isFinal || lastOldResponderState == lifecycle.ResponderStateCompleted ||
		lastOldResponderState == lifecycle.ResponderStateCanceled ||
		lastOldResponderState == lifecycle.ResponderStateInterrupted

	for i, responder := range sortedTargetResponders {
		expectedStates := sets.New[lifecycle.ResponderStateType]()

		expectedStatesReason := ""
		switch {
		//
		case i < lastActiveIdx ||
			(len(sortedOldTargetResponders) > 0 && !hasFoundLastActive && isFinalOrAllProcessed):
			// Processed responders must preserve their state.
			expectedStates.Insert(sortedOldTargetResponders[i].State)
			expectedStatesReason = "final state is immutable"
		case i == lastActiveIdx && activeChanged:
			// If Active responder changes, it must have a final state.
			expectedStates.Insert(lifecycle.ResponderStateInterrupted,
				lifecycle.ResponderStateCanceled,
				lifecycle.ResponderStateCompleted)
			expectedStatesReason = "this responder must reach a final state"
		case i == lastActiveIdx:
			// Unchanged Active responders can stay Active.
			expectedStates.Insert(lifecycle.ResponderStateActive)
			expectedStatesReason = "the eviction request stays active"
			if isFinal {
				// Last Responder must finish before setting the final condition.
				expectedStates.Clear().Insert(lifecycle.ResponderStateInterrupted,
					lifecycle.ResponderStateCanceled,
					lifecycle.ResponderStateCompleted)
				expectedStatesReason = "the eviction request has finished processing"
			}
		case i == 0 && hasNeverBeenActive,
			i == lastActiveIdx+1 && activeChanged:
			// Next responder must move to an Active state, when the old one is final.
			expectedStates.Insert(lifecycle.ResponderStateActive)
			expectedStatesReason = "this responder is next in line"
			if isFinal {
				// Do not active next responder if we have finished.
				expectedStates.Clear().Insert(lifecycle.ResponderStateInactive)
				expectedStatesReason = "the eviction request has finished processing"
			}
		default:
			expectedStates.Insert(lifecycle.ResponderStateInactive)
			expectedStatesReason = "the previously active has not finished processing"
		}

		allErrs = append(allErrs, ValidateTargetResponder(responder, fldPath.Index(targetRespondersOriginalIdx[responder.Name]), ValidateEvictionTargetResponderOptions{
			Clock:                opts.Clock,
			IsEvicted:            opts.IsEvicted,
			IsFailed:             opts.IsFailed,
			responderStatus:      statusResponders[responder.Name],
			expectedStates:       expectedStates,
			expectedStatesReason: expectedStatesReason,
			HeartbeatDeadline:    opts.HeartbeatDeadline,
			AllowedTimeSkew:      opts.AllowedTimeSkew,
		})...)
	}
	return allErrs
}

type ValidateEvictionTargetResponderOptions struct {
	Clock                clock.PassiveClock
	IsEvicted            bool
	IsFailed             bool
	responderStatus      *lifecycle.ResponderStatus
	expectedStates       sets.Set[lifecycle.ResponderStateType]
	expectedStatesReason string
	HeartbeatDeadline    time.Duration
	AllowedTimeSkew      time.Duration
}

func ValidateTargetResponder(evictionResponder lifecycle.TargetResponder, fldPath *field.Path, opts ValidateEvictionTargetResponderOptions) field.ErrorList {
	var allErrs field.ErrorList

	// name covered by DV

	statusResponderPath := field.NewPath("status", "responders")
	if opts.responderStatus == nil {
		msg := fmt.Sprintf("%q has to be tracked in %s first", evictionResponder.Name, statusResponderPath)
		return append(allErrs, field.Invalid(fldPath, evictionResponder, msg))
	}

	// priority
	const reservedKubernetesRespondersPriority = 1000
	priorityAssignedToK8sResponders := map[string]int32{
		lifecycle.EvictionResponderImperativeEviction: 100,
	}
	priorityPath := fldPath.Child("priority")
	// validate.Maximum covered by DV
	// validate.Minimum covered by DV
	if evictionResponder.Priority != nil && *evictionResponder.Priority >= 0 {
		if expectedPriority, ok := priorityAssignedToK8sResponders[evictionResponder.Name]; ok {
			if *evictionResponder.Priority != expectedPriority {
				allErrs = append(allErrs, field.Invalid(priorityPath, *evictionResponder.Priority, fmt.Sprintf("core k8s responder is expected to have priority: %d", expectedPriority)))
			}
		} else if *evictionResponder.Priority < reservedKubernetesRespondersPriority {
			// Each core responder should have priority assigned - see priorityAssignedToK8sResponders
			allErrs = append(allErrs, field.Invalid(priorityPath, *evictionResponder.Priority, "priorities 0-999 are reserved for responders with *.k8s.io suffix"))
		}
	}

	// state
	statePath := fldPath.Child("state")
	// validate.RequiredValue covered by DV
	// validate.Enum covered by DV

	// check that the state transition is allowed
	if !opts.expectedStates.Has(evictionResponder.State) {
		var expectedValues []string
		for _, ev := range sets.List(opts.expectedStates) {
			expectedValues = append(expectedValues, string(ev))
		}
		msg := fmt.Sprintf("must be one of: %s", strings.Join(expectedValues, ", "))
		if len(opts.expectedStatesReason) > 0 {
			msg = fmt.Sprintf("%s, because %s", msg, opts.expectedStatesReason)
		}
		allErrs = append(allErrs, field.Invalid(statePath, evictionResponder.State, msg))
		// short circuit as the validations below depend on a correct state
		return allErrs
	}

	// must be present in status responders

	// check the heartbeat deadline if the responder is not active anymore (.status.responders)
	// we can skip the check if the Eviction is final (failed or evicted).
	// StartTime presence is validated in ValidateEvictionStatusResponder
	if evictionResponder.State != lifecycle.ResponderStateActive && opts.responderStatus.StartTime != nil &&
		opts.responderStatus.CompletionTime == nil && !opts.IsEvicted && !opts.IsFailed {
		heartbeat := opts.responderStatus.StartTime
		if opts.responderStatus.HeartbeatTime != nil {
			heartbeat = opts.responderStatus.HeartbeatTime
		}
		if opts.Clock.Now().Before(heartbeat.Add(opts.HeartbeatDeadline).Add(-opts.AllowedTimeSkew)) {
			msg := fmt.Sprintf("must stay Active because the responder is in progress and it should report %s or %s", statusResponderPath.Child("heartbeatTime"), statusResponderPath.Child("completionTime"))
			allErrs = append(allErrs, field.Forbidden(statePath, msg))
		}
	}
	if evictionResponder.State == lifecycle.ResponderStateCompleted && opts.responderStatus.CompletionTime == nil {
		msg := fmt.Sprintf("cannot become Completed because the responder didn't report %s", statusResponderPath.Child("completionTime"))
		allErrs = append(allErrs, field.Invalid(statePath, evictionResponder.State, msg))
	}
	return allErrs
}

type ValidateEvictionStatusRespondersOptions struct {
	Clock                     clock.PassiveClock
	TargetResponders          []lifecycle.TargetResponder
	AllowedTimeSkew           time.Duration
	MaxExpectedCompletionTime time.Duration
}

func ValidateEvictionStatusResponders(statusResponders, oldStatusResponders []lifecycle.ResponderStatus, fldPath *field.Path, opts ValidateEvictionStatusRespondersOptions) field.ErrorList {
	var allErrs field.ErrorList

	if len(opts.TargetResponders) != len(statusResponders) {
		return append(allErrs, field.Invalid(fldPath, statusResponders, "must be the same length as status.targetResponders and contain the same keys"))
	}
	statusRespondersNames := sets.New[string]()
	for _, responder := range statusResponders {
		statusRespondersNames.Insert(responder.Name)
	}
	targetResponders := map[string]*lifecycle.TargetResponder{}
	for i, targetResponder := range opts.TargetResponders {
		targetResponders[targetResponder.Name] = &opts.TargetResponders[i]
		if !statusRespondersNames.Has(targetResponder.Name) {
			return append(allErrs, field.Invalid(fldPath, statusResponders, "must contain the same keys as status.targetResponders"))
		}
	}
	if len(statusResponders) == 0 {
		// statusResponders and TargetResponders are not initialized yet
		return allErrs
	}

	if len(statusResponders) > maxEvictionResponders {
		// TooMany is handled by declarative validation - detect early return
		return allErrs
	}
	// validate.ValSliceUnique covered by DV - simulate an error
	uniqueErrors := validate.ValSliceUnique(context.TODO(), operation.Operation{}, fldPath, statusResponders, nil,
		func(a *lifecycle.ResponderStatus, b *lifecycle.ResponderStatus) bool { return a.Name == b.Name })
	if len(uniqueErrors) > 0 {
		// does not make sense to check each responder status with duplicates as we depend on the target responder and an oldStatusResponder
		return allErrs
	}

	oldStatusRespondersMap := map[string]*lifecycle.ResponderStatus{}
	for i := range oldStatusResponders {
		oldStatusRespondersMap[oldStatusResponders[i].Name] = &oldStatusResponders[i] // +k8s:verify-mutation:reason=clone
	}
	for i, responder := range statusResponders {
		targetResponder := targetResponders[responder.Name] // key presence checked above
		responderPath := fldPath.Index(i)
		allErrs = append(allErrs, ValidateEvictionStatusResponder(&responder, oldStatusRespondersMap[responder.Name], responderPath,
			EvictionStatusResponderValidationOptions{
				Clock:                     opts.Clock,
				responderState:            targetResponder.State,
				AllowedTimeSkew:           opts.AllowedTimeSkew,
				MaxExpectedCompletionTime: opts.MaxExpectedCompletionTime,
			},
		)...)
	}
	return allErrs
}

type EvictionStatusResponderValidationOptions struct {
	responderState            lifecycle.ResponderStateType
	AllowedTimeSkew           time.Duration
	MaxExpectedCompletionTime time.Duration
	Clock                     clock.PassiveClock
}

func ValidateEvictionStatusResponder(status, oldStatus *lifecycle.ResponderStatus, fldPath *field.Path, opts EvictionStatusResponderValidationOptions) field.ErrorList {
	var allErrs field.ErrorList
	oldDefaultedStatus := lifecycle.ResponderStatus{}
	if oldStatus != nil {
		oldDefaultedStatus = *oldStatus // +k8s:verify-mutation:reason=clone
	}

	if oldStatus != nil && !validate.SemanticDeepEqual(status, oldStatus) && opts.responderState != lifecycle.ResponderStateActive {
		// immutable; changes to the ResponderStatus are only allowed by the active responder or during initialization
		return append(allErrs, field.Invalid(fldPath, status, validation.FieldImmutableErrorMsg).WithOrigin("immutable"))
	}

	// name covered by DV
	// The existence of targetResponder with the same name is done in ValidateEvictionStatusResponders

	// startTime
	startTimePath := fldPath.Child("startTime")
	// immutable once set
	if oldDefaultedStatus.StartTime != nil && !oldDefaultedStatus.StartTime.Equal(status.StartTime) {
		// validate.NoUnset and validate.NoModify checks covered by DV
	} else if status.StartTime == nil && opts.responderState == lifecycle.ResponderStateActive {
		allErrs = append(allErrs, field.Required(startTimePath, "is required for an active responder"))
	} else if status.StartTime != nil && !oldDefaultedStatus.StartTime.Equal(status.StartTime) && !timeNear(status.StartTime.Time, opts.Clock.Now(), opts.AllowedTimeSkew) {
		allErrs = append(allErrs, field.Invalid(startTimePath, status.StartTime, "must be set to the present time"))
	}

	// heartbeatTime
	heartbeatTimePath := fldPath.Child("heartbeatTime")
	if oldDefaultedStatus.HeartbeatTime != nil && status.HeartbeatTime == nil {
		allErrs = append(allErrs, field.Required(heartbeatTimePath, "is required once set"))
	}
	if !oldDefaultedStatus.HeartbeatTime.Equal(status.HeartbeatTime) && status.HeartbeatTime != nil {
		if status.StartTime == nil {
			allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, fmt.Sprintf("cannot be set before %s is set", startTimePath.String())))
		} else if status.HeartbeatTime.Before(oldDefaultedStatus.HeartbeatTime) {
			// this could still happen since we allow for the skew
			allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, "cannot be decreased"))
		} else if status.HeartbeatTime.Before(status.StartTime) {
			// this could still happen since we allow for the skew
			allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, fmt.Sprintf("must occur after %s", startTimePath.String())))
		} else if !timeNear(status.HeartbeatTime.Time, opts.Clock.Now(), opts.AllowedTimeSkew) {
			allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, "must be set to the present time"))
		}
	}

	// expectedCompletionTime
	expectedCompletionTimePath := fldPath.Child("expectedCompletionTime")
	if !oldDefaultedStatus.ExpectedCompletionTime.Equal(status.ExpectedCompletionTime) && status.ExpectedCompletionTime != nil {
		if status.StartTime == nil {
			allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, fmt.Sprintf("cannot be set before %s is set", startTimePath.String())))
		} else if status.ExpectedCompletionTime.Before(status.StartTime) {
			// this could still happen since we allow for the skew
			allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, fmt.Sprintf("must occur after %s", startTimePath.String())))
		} else if status.ExpectedCompletionTime.Time.Before(opts.Clock.Now().Add(-opts.AllowedTimeSkew)) {
			allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, "cannot be set to the past time"))
		} else if status.ExpectedCompletionTime.Time.After(opts.Clock.Now().Add(opts.MaxExpectedCompletionTime)) {
			allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, "must complete within 10 years")) // sanity check
		}
	}

	// completionTime
	completionTimePath := fldPath.Child("completionTime")
	// immutable once set
	if oldDefaultedStatus.CompletionTime != nil && !oldDefaultedStatus.CompletionTime.Equal(status.CompletionTime) {
		// validate.NoUnset and validate.NoModify checks covered by DV
	} else if status.CompletionTime != nil {
		if status.StartTime == nil {
			allErrs = append(allErrs, field.Invalid(completionTimePath, status.CompletionTime, fmt.Sprintf("cannot be set before %s is set", startTimePath.String())))
		} else if status.CompletionTime.Before(status.StartTime) {
			allErrs = append(allErrs, field.Invalid(completionTimePath, status.CompletionTime, fmt.Sprintf("must occur after %s", startTimePath.String())))
		} else if !oldDefaultedStatus.CompletionTime.Equal(status.CompletionTime) && !timeNear(status.CompletionTime.Time, opts.Clock.Now(), opts.AllowedTimeSkew) {
			allErrs = append(allErrs, field.Invalid(completionTimePath, status.CompletionTime, "must be set to the present time"))
		}
	}

	// message  covered by DV

	return allErrs
}

func timeNear(a, b time.Time, skew time.Duration) bool {
	return a.After(b.Add(-skew)) && a.Before(b.Add(skew))
}

// SortTargetResponders returns highest priority responders on a lower indes.
// If there are responders with the same priority, the responder whose domain name comes first in the
// alphabetical higher domain order, will be picked. This means that the top domain labels are compared
// alphabetically first, followed by the lower domain labels. The key is compared last.
func SortTargetResponders(responders []lifecycle.TargetResponder) {
	getComponents := func(domainPrefixedKey string) ([]string, string) {
		segments := strings.SplitN(domainPrefixedKey, "/", 2)
		if len(segments) == 0 {
			return nil, ""
		}
		var domainComponents []string
		var key string
		if len(segments) == 2 {
			key = segments[1]
		}
		domainComponents = strings.Split(segments[0], ".")
		slices.Reverse(domainComponents)
		return domainComponents, key
	}
	slices.SortFunc(responders, func(a, b lifecycle.TargetResponder) int {
		if !ptr.Equal(a.Priority, b.Priority) {
			if a.Priority == nil {
				return 1
			}
			if b.Priority == nil {
				return -1
			}
			return int(ptr.Deref(b.Priority, -1) - ptr.Deref(a.Priority, -1))
		}
		aComponents, aKey := getComponents(a.Name)
		bComponents, bKey := getComponents(b.Name)

		for i, aComponent := range aComponents {
			if i > len(bComponents)-1 {
				// b has a higher priority if no component left for b
				return 1
			}
			bComponent := bComponents[i]
			cmp := strings.Compare(aComponent, bComponent)
			if cmp != 0 {
				return cmp
			}
		}
		if len(bComponents) > len(aComponents) {
			// a has a higher priority if no component left for a
			return -1
		}
		return strings.Compare(aKey, bKey)
	})
}
