package testing

import (
	"fmt"
	"time"

	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	clusterapiv1 "open-cluster-management.io/api/cluster/v1"
	clusterapiv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1"
	clusterapiv1beta1 "open-cluster-management.io/api/cluster/v1beta1"
	clusterapiv1beta2 "open-cluster-management.io/api/cluster/v1beta2"
)

type PlacementBuilder struct {
	placement *clusterapiv1beta1.Placement
}

func NewPlacement(namespace, name string) *PlacementBuilder {
	return &PlacementBuilder{
		placement: &clusterapiv1beta1.Placement{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: namespace,
				Name:      name,
			},
		},
	}
}

func NewPlacementWithAnnotations(namespace, name string, annotations map[string]string) *PlacementBuilder {
	return &PlacementBuilder{
		placement: &clusterapiv1beta1.Placement{
			ObjectMeta: metav1.ObjectMeta{
				Namespace:   namespace,
				Name:        name,
				Annotations: annotations,
			},
		},
	}
}

func (b *PlacementBuilder) WithUID(uid string) *PlacementBuilder {
	b.placement.UID = types.UID(uid)
	return b
}

func (b *PlacementBuilder) WithNOC(noc int32) *PlacementBuilder {
	b.placement.Spec.NumberOfClusters = &noc
	return b
}

func (b *PlacementBuilder) WithGroupStrategy(groupStrategy clusterapiv1beta1.GroupStrategy) *PlacementBuilder {
	b.placement.Spec.DecisionStrategy.GroupStrategy = groupStrategy
	return b
}

func (b *PlacementBuilder) WithPrioritizerPolicy(mode clusterapiv1beta1.PrioritizerPolicyModeType) *PlacementBuilder {
	b.placement.Spec.PrioritizerPolicy = clusterapiv1beta1.PrioritizerPolicy{
		Mode: mode,
	}
	return b
}

func (b *PlacementBuilder) WithPrioritizerConfig(name string, weight int32) *PlacementBuilder {
	if b.placement.Spec.PrioritizerPolicy.Configurations == nil {
		b.placement.Spec.PrioritizerPolicy.Configurations = []clusterapiv1beta1.PrioritizerConfig{}
	}
	if len(name) > 0 {
		b.placement.Spec.PrioritizerPolicy.Configurations = append(b.placement.Spec.PrioritizerPolicy.Configurations, clusterapiv1beta1.PrioritizerConfig{
			ScoreCoordinate: &clusterapiv1beta1.ScoreCoordinate{
				Type:    clusterapiv1beta1.ScoreCoordinateTypeBuiltIn,
				BuiltIn: name,
			},
			Weight: weight,
		})
	}
	return b
}

func (b *PlacementBuilder) WithScoreCoordinateAddOn(resourceName, scoreName string, weight int32) *PlacementBuilder {
	if b.placement.Spec.PrioritizerPolicy.Configurations == nil {
		b.placement.Spec.PrioritizerPolicy.Configurations = []clusterapiv1beta1.PrioritizerConfig{}
	}
	b.placement.Spec.PrioritizerPolicy.Configurations = append(b.placement.Spec.PrioritizerPolicy.Configurations, clusterapiv1beta1.PrioritizerConfig{
		ScoreCoordinate: &clusterapiv1beta1.ScoreCoordinate{
			Type: clusterapiv1beta1.ScoreCoordinateTypeAddOn,
			AddOn: &clusterapiv1beta1.AddOnScore{
				ResourceName: resourceName,
				ScoreName:    scoreName,
			},
		},
		Weight: weight})
	return b
}

func (b *PlacementBuilder) WithClusterSets(clusterSets ...string) *PlacementBuilder {
	b.placement.Spec.ClusterSets = clusterSets
	return b
}

func (b *PlacementBuilder) WithDeletionTimestamp() *PlacementBuilder {
	now := metav1.Now()
	b.placement.DeletionTimestamp = &now
	return b
}

func (b *PlacementBuilder) AddPredicate(
	labelSelector *metav1.LabelSelector,
	claimSelector *clusterapiv1beta1.ClusterClaimSelector,
	celSelector *clusterapiv1beta1.ClusterCelSelector,
) *PlacementBuilder {
	if b.placement.Spec.Predicates == nil {
		b.placement.Spec.Predicates = []clusterapiv1beta1.ClusterPredicate{}
	}
	b.placement.Spec.Predicates = append(b.placement.Spec.Predicates, NewClusterPredicate(labelSelector, claimSelector, celSelector))
	return b
}

func (b *PlacementBuilder) AddToleration(toleration *clusterapiv1beta1.Toleration) *PlacementBuilder {
	if b.placement.Spec.Tolerations == nil {
		b.placement.Spec.Tolerations = []clusterapiv1beta1.Toleration{}
	}
	b.placement.Spec.Tolerations = append(b.placement.Spec.Tolerations, *toleration)
	return b
}

func (b *PlacementBuilder) WithNumOfSelectedClusters(nosc int32, placementName string) *PlacementBuilder {
	b.placement.Status.NumberOfSelectedClusters = nosc
	b.placement.Status.DecisionGroups = []clusterapiv1beta1.DecisionGroupStatus{
		{
			DecisionGroupIndex: 0,
			DecisionGroupName:  "",
			ClustersCount:      nosc,
			Decisions:          []string{PlacementDecisionName(placementName, 1)},
		},
	}
	return b
}

func (b *PlacementBuilder) WithSatisfiedCondition(numbOfScheduledDecisions, numbOfUnscheduledDecisions int) *PlacementBuilder {
	condition := metav1.Condition{
		Type: clusterapiv1beta1.PlacementConditionSatisfied,
	}
	switch {
	case numbOfUnscheduledDecisions == 0:
		condition.Status = metav1.ConditionTrue
		condition.Reason = "AllDecisionsScheduled"
		condition.Message = "All cluster decisions scheduled"
	default:
		condition.Status = metav1.ConditionFalse
		condition.Reason = "NotAllDecisionsScheduled"
		condition.Message = fmt.Sprintf("%d cluster decisions unscheduled", numbOfUnscheduledDecisions)
	}
	meta.SetStatusCondition(&b.placement.Status.Conditions, condition)
	return b
}

func (b *PlacementBuilder) WithMisconfiguredCondition(status metav1.ConditionStatus) *PlacementBuilder {
	condition := metav1.Condition{
		Type:    clusterapiv1beta1.PlacementConditionMisconfigured,
		Status:  status,
		Reason:  "Succeedconfigured",
		Message: "Placement configurations check pass",
	}
	meta.SetStatusCondition(&b.placement.Status.Conditions, condition)
	return b
}

func (b *PlacementBuilder) Build() *clusterapiv1beta1.Placement {
	return b.placement
}

func NewClusterPredicate(
	labelSelector *metav1.LabelSelector,
	claimSelector *clusterapiv1beta1.ClusterClaimSelector,
	celSelector *clusterapiv1beta1.ClusterCelSelector,
) clusterapiv1beta1.ClusterPredicate {
	predicate := clusterapiv1beta1.ClusterPredicate{
		RequiredClusterSelector: clusterapiv1beta1.ClusterSelector{},
	}

	if labelSelector != nil {
		predicate.RequiredClusterSelector.LabelSelector = *labelSelector
	}

	if claimSelector != nil {
		predicate.RequiredClusterSelector.ClaimSelector = *claimSelector
	}

	if celSelector != nil {
		predicate.RequiredClusterSelector.CelSelector = *celSelector
	}

	return predicate
}

type PlacementDecisionBuilder struct {
	placementDecision *clusterapiv1beta1.PlacementDecision
}

func NewPlacementDecision(namespace, name string) *PlacementDecisionBuilder {
	return &PlacementDecisionBuilder{
		placementDecision: &clusterapiv1beta1.PlacementDecision{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: namespace,
				Name:      name,
			},
		},
	}
}

func (b *PlacementDecisionBuilder) WithController(uid string) *PlacementDecisionBuilder {
	controller := true
	b.placementDecision.OwnerReferences = append(b.placementDecision.OwnerReferences, metav1.OwnerReference{
		Controller: &controller,
		UID:        types.UID(uid),
	})
	return b
}

func (b *PlacementDecisionBuilder) WithLabel(name, value string) *PlacementDecisionBuilder {
	if b.placementDecision.Labels == nil {
		b.placementDecision.Labels = map[string]string{}
	}
	b.placementDecision.Labels[name] = value
	return b
}

func (b *PlacementDecisionBuilder) WithDeletionTimestamp() *PlacementDecisionBuilder {
	now := metav1.Now()
	b.placementDecision.DeletionTimestamp = &now
	return b
}

func (b *PlacementDecisionBuilder) WithDecisions(clusterNames ...string) *PlacementDecisionBuilder {
	var decisions []clusterapiv1beta1.ClusterDecision
	for _, clusterName := range clusterNames {
		decisions = append(decisions, clusterapiv1beta1.ClusterDecision{
			ClusterName: clusterName,
		})
	}
	b.placementDecision.Status.Decisions = decisions
	return b
}

func (b *PlacementDecisionBuilder) Build() *clusterapiv1beta1.PlacementDecision {
	return b.placementDecision
}

type ManagedClusterBuilder struct {
	cluster *clusterapiv1.ManagedCluster
}

func NewManagedCluster(clusterName string) *ManagedClusterBuilder {
	return &ManagedClusterBuilder{
		cluster: &clusterapiv1.ManagedCluster{
			ObjectMeta: metav1.ObjectMeta{
				Name: clusterName,
			},
		},
	}
}

func (b *ManagedClusterBuilder) WithLabel(name, value string) *ManagedClusterBuilder {
	if b.cluster.Labels == nil {
		b.cluster.Labels = map[string]string{}
	}
	b.cluster.Labels[name] = value
	return b
}

func (b *ManagedClusterBuilder) WithClaim(name, value string) *ManagedClusterBuilder {
	claimMap := map[string]string{}
	for _, claim := range b.cluster.Status.ClusterClaims {
		claimMap[claim.Name] = claim.Value
	}
	claimMap[name] = value

	var clusterClaims []clusterapiv1.ManagedClusterClaim
	for k, v := range claimMap {
		clusterClaims = append(clusterClaims, clusterapiv1.ManagedClusterClaim{
			Name:  k,
			Value: v,
		})
	}

	b.cluster.Status.ClusterClaims = clusterClaims
	return b
}

func (b *ManagedClusterBuilder) WithResource(resourceName clusterapiv1.ResourceName, allocatable, capacity string) *ManagedClusterBuilder {
	if b.cluster.Status.Allocatable == nil {
		b.cluster.Status.Allocatable = make(map[clusterapiv1.ResourceName]resource.Quantity)
	}
	if b.cluster.Status.Capacity == nil {
		b.cluster.Status.Capacity = make(map[clusterapiv1.ResourceName]resource.Quantity)
	}

	b.cluster.Status.Allocatable[resourceName], _ = resource.ParseQuantity(allocatable)
	b.cluster.Status.Capacity[resourceName], _ = resource.ParseQuantity(capacity)
	return b
}

func (b *ManagedClusterBuilder) WithTaint(taint *clusterapiv1.Taint) *ManagedClusterBuilder {
	if b.cluster.Spec.Taints == nil {
		b.cluster.Spec.Taints = []clusterapiv1.Taint{}
	}
	b.cluster.Spec.Taints = append(b.cluster.Spec.Taints, *taint)
	return b
}

func (b *ManagedClusterBuilder) WithDeletionTimestamp() *ManagedClusterBuilder {
	if b.cluster.DeletionTimestamp.IsZero() {
		t := metav1.Now()
		b.cluster.DeletionTimestamp = &t
	}

	return b
}

func (b *ManagedClusterBuilder) Build() *clusterapiv1.ManagedCluster {
	return b.cluster
}

type ManagedClusterSetBuilder struct {
	clusterset *clusterapiv1beta2.ManagedClusterSet
}

func NewClusterSet(clusterSetName string) *ManagedClusterSetBuilder {
	return &ManagedClusterSetBuilder{
		clusterset: &clusterapiv1beta2.ManagedClusterSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: clusterSetName,
			},
		},
	}
}

func (b *ManagedClusterSetBuilder) WithClusterSelector(clusterSelector clusterapiv1beta2.ManagedClusterSelector) *ManagedClusterSetBuilder {
	b.clusterset.Spec.ClusterSelector = clusterSelector
	return b
}

func (b *ManagedClusterSetBuilder) Build() *clusterapiv1beta2.ManagedClusterSet {
	return b.clusterset
}

func NewClusterSetBinding(namespace, clusterSetName string) *clusterapiv1beta2.ManagedClusterSetBinding {
	return &clusterapiv1beta2.ManagedClusterSetBinding{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      clusterSetName,
		},
		Spec: clusterapiv1beta2.ManagedClusterSetBindingSpec{
			ClusterSet: clusterSetName,
		},
	}
}

type AddOnPlacementScoreBuilder struct {
	addOnPlacementScore *clusterapiv1alpha1.AddOnPlacementScore
}

func NewAddOnPlacementScore(clusternamespace, name string) *AddOnPlacementScoreBuilder {
	return &AddOnPlacementScoreBuilder{
		addOnPlacementScore: &clusterapiv1alpha1.AddOnPlacementScore{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: clusternamespace,
				Name:      name,
			},
		},
	}
}

func (a *AddOnPlacementScoreBuilder) WithScore(name string, score int32) *AddOnPlacementScoreBuilder {
	if a.addOnPlacementScore.Status.Scores == nil {
		a.addOnPlacementScore.Status.Scores = []clusterapiv1alpha1.AddOnPlacementScoreItem{}
	}

	a.addOnPlacementScore.Status.Scores = append(a.addOnPlacementScore.Status.Scores, clusterapiv1alpha1.AddOnPlacementScoreItem{
		Name:  name,
		Value: score,
	})
	return a
}

func (a *AddOnPlacementScoreBuilder) WithValidUntil(validUntil time.Time) *AddOnPlacementScoreBuilder {
	vu := metav1.NewTime(validUntil)
	a.addOnPlacementScore.Status.ValidUntil = &vu
	return a
}

func (a *AddOnPlacementScoreBuilder) Build() *clusterapiv1alpha1.AddOnPlacementScore {
	return a.addOnPlacementScore
}

func PlacementDecisionName(placementName string, index int) string {
	return fmt.Sprintf("%s-decision-%d", placementName, index)
}
