// Copyright 2026 Microsoft Corporation
//
// 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 integrationutils

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"dario.cat/mergo"
	"go.uber.org/mock/gomock"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/util/rand"
	"k8s.io/apimachinery/pkg/util/sets"

	csarhcpv1alpha1 "github.com/openshift-online/ocm-api-model/clientapi/arohcp/v1alpha1"
	arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1"
	cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
	ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"

	"github.com/Azure/ARO-HCP/backend/pkg/controllers/datadump"
	"github.com/Azure/ARO-HCP/internal/ocm"
)

type ClusterServiceMock struct {
	ArtifactsDir string
	mockData     map[string]map[string][]any

	MockClusterServiceClient *ocm.MockClusterServiceClientSpec
}

func NewClusterServiceMock(t *testing.T, artifactsDir string) *ClusterServiceMock {
	ctrl := gomock.NewController(t)
	clusterServiceClient := ocm.NewMockClusterServiceClientSpec(ctrl)

	ret := &ClusterServiceMock{
		ArtifactsDir:             artifactsDir,
		mockData:                 map[string]map[string][]any{},
		MockClusterServiceClient: clusterServiceClient,
	}
	ret.setupMockClusterService(t)
	return ret
}

func (s *ClusterServiceMock) setupMockClusterService(t *testing.T) {
	internalIDToCluster := s.GetOrCreateMockData(t.Name() + "_clusters")
	internalIDToExternalAuth := s.GetOrCreateMockData(t.Name() + "_externalAuths")
	internalIDToNodePool := s.GetOrCreateMockData(t.Name() + "_nodePools")
	internalIDToProvisionShard := s.GetOrCreateMockData(t.Name() + "_provisionShards")
	internalIDToHypershiftDetails := s.GetOrCreateMockData(t.Name() + "_hypershiftDetails")

	s.MockClusterServiceClient.EXPECT().PostCluster(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, clusterBuilder *arohcpv1alpha1.ClusterBuilder) (*arohcpv1alpha1.Cluster, error) {
		justID := rand.String(10)
		internalID := "/api/clusters_mgmt/v1/clusters/" + justID

		ret, err := clusterBuilder.ID(justID).HREF(internalID).Build()
		if err != nil {
			return nil, err
		}

		// these values are normally looked up directly from azure inside of cluster-service.  For mocks we do it here.
		ret, err = addFakeAzureIdentityData(ret)
		if err != nil {
			return nil, err
		}

		internalIDToCluster[internalID] = append(internalIDToCluster[internalID], ret)
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().UpdateCluster(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID, builder *arohcpv1alpha1.ClusterBuilder) (*arohcpv1alpha1.Cluster, error) {
		ret, err := builder.Build()
		if err != nil {
			return nil, err
		}

		internalIDToCluster[id.String()] = append(internalIDToCluster[id.String()], ret)
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().DeleteCluster(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) error {
		_, exists := internalIDToCluster[id.String()]
		if !exists {
			return fmt.Errorf("cluster %q does not exist", id.String())
		}
		delete(internalIDToCluster, id.String())

		return nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().GetCluster(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) (*csarhcpv1alpha1.Cluster, error) {
		ret, err := mergeClusterServiceInstance[csarhcpv1alpha1.Cluster](internalIDToCluster[id.String()])
		if err != nil {
			return nil, fmt.Errorf("failed to merge cluster id %q: %w", id.String(), err)
		}
		return ret, nil

	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().ListClusters(gomock.Any()).DoAndReturn(func(s string) ocm.ClusterListIterator {
		allObjs := []*csarhcpv1alpha1.Cluster{}
		for _, key := range sets.StringKeySet(internalIDToCluster).List() {
			obj, err := mergeClusterServiceInstance[csarhcpv1alpha1.Cluster](internalIDToCluster[key])
			if err != nil {
				panic(fmt.Errorf("failed to merge cluster id %q: %w", key, err))
			}
			allObjs = append(allObjs, obj)
		}
		return ocm.NewSimpleClusterListIterator(allObjs, nil)
	}).AnyTimes()

	s.MockClusterServiceClient.EXPECT().PostExternalAuth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, clusterID ocm.InternalID, builder *arohcpv1alpha1.ExternalAuthBuilder) (*arohcpv1alpha1.ExternalAuth, error) {
		justID := rand.String(10)
		builder.ID(justID)
		externalAuthInternalID := clusterID.String() + "/external_auth_config/external_auths/" + justID
		builder = builder.HREF(externalAuthInternalID)
		ret, err := builder.Build()
		if err != nil {
			return nil, err
		}

		internalIDToExternalAuth[externalAuthInternalID] = append(internalIDToExternalAuth[externalAuthInternalID], ret)
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().UpdateExternalAuth(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID, builder *arohcpv1alpha1.ExternalAuthBuilder) (*arohcpv1alpha1.ExternalAuth, error) {
		ret, err := builder.Build()
		if err != nil {
			return nil, err
		}

		internalIDToExternalAuth[id.String()] = append(internalIDToExternalAuth[id.String()], ret)
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().GetExternalAuth(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) (*arohcpv1alpha1.ExternalAuth, error) {
		ret, err := mergeClusterServiceInstance[csarhcpv1alpha1.ExternalAuth](internalIDToExternalAuth[id.String()])
		if err != nil {
			return nil, fmt.Errorf("failed to merge external auth id %q: %w", id.String(), err)
		}
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().ListExternalAuths(gomock.Any(), gomock.Any()).DoAndReturn(func(id ocm.InternalID, s string) ocm.ExternalAuthListIterator {
		clusterIDString := id.String()
		allObjs := []*csarhcpv1alpha1.ExternalAuth{}
		for _, key := range sets.StringKeySet(internalIDToExternalAuth).List() {
			if !strings.Contains(key, clusterIDString) {
				// only include for the right cluster
				continue
			}
			obj, err := mergeClusterServiceInstance[csarhcpv1alpha1.ExternalAuth](internalIDToExternalAuth[key])
			if err != nil {
				panic(fmt.Errorf("failed to merge external auth id %q: %w", key, err))
			}
			allObjs = append(allObjs, obj)
		}
		return ocm.NewSimpleExternalAuthListIterator(allObjs, nil)
	}).AnyTimes()

	s.MockClusterServiceClient.EXPECT().PostNodePool(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, clusterID ocm.InternalID, builder *arohcpv1alpha1.NodePoolBuilder) (*arohcpv1alpha1.NodePool, error) {
		justID := rand.String(10)
		nodePoolInternalID := clusterID.String() + "/node_pools/" + justID

		ret, err := builder.ID(justID).HREF(nodePoolInternalID).Build()
		if err != nil {
			return nil, err
		}

		internalIDToNodePool[nodePoolInternalID] = append(internalIDToNodePool[nodePoolInternalID], ret)
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().UpdateNodePool(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID, builder *arohcpv1alpha1.NodePoolBuilder) (*arohcpv1alpha1.NodePool, error) {
		ret, err := builder.Build()
		if err != nil {
			return nil, err
		}

		internalIDToNodePool[id.String()] = append(internalIDToNodePool[id.String()], ret)
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().GetNodePool(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) (*arohcpv1alpha1.NodePool, error) {
		ret, err := mergeClusterServiceInstance[csarhcpv1alpha1.NodePool](internalIDToNodePool[id.String()])
		if err != nil {
			return nil, fmt.Errorf("failed to merge nodepool id %q: %w", id.String(), err)
		}
		return ret, nil

	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().ListNodePools(gomock.Any(), gomock.Any()).DoAndReturn(func(id ocm.InternalID, s string) ocm.NodePoolListIterator {
		clusterIDString := id.String()
		allObjs := []*csarhcpv1alpha1.NodePool{}
		for _, key := range sets.StringKeySet(internalIDToNodePool).List() {
			if !strings.Contains(key, clusterIDString) {
				// only include for the right cluster
				continue
			}
			obj, err := mergeClusterServiceInstance[csarhcpv1alpha1.NodePool](internalIDToNodePool[key])
			if err != nil {
				panic(fmt.Errorf("failed to merge nodepool id %q: %w", key, err))
			}
			allObjs = append(allObjs, obj)
		}
		return ocm.NewSimpleNodePoolListIterator(allObjs, nil)
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().DeleteNodePool(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) error {
		_, exists := internalIDToNodePool[id.String()]
		if !exists {
			retErr, err := ocmerrors.NewError().Status(http.StatusNotFound).Build()
			if err != nil {
				return err
			}
			return retErr
		}
		delete(internalIDToNodePool, id.String())
		return nil
	}).AnyTimes()

	s.MockClusterServiceClient.EXPECT().DeleteBreakGlassCredentials(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
	s.MockClusterServiceClient.EXPECT().PostBreakGlassCredential(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, clusterID ocm.InternalID) (*cmv1.BreakGlassCredential, error) {
		justID := rand.String(10)
		credentialInternalID := clusterID.String() + "/break_glass_credentials/" + justID
		ret, err := cmv1.NewBreakGlassCredential().ID(justID).HREF(credentialInternalID).Build()
		if err != nil {
			return nil, err
		}
		return ret, nil
	}).AnyTimes()

	s.MockClusterServiceClient.EXPECT().GetClusterProvisionShard(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) (*csarhcpv1alpha1.ProvisionShard, error) {
		ret, err := mergeClusterServiceInstance[csarhcpv1alpha1.ProvisionShard](internalIDToProvisionShard[id.String()])
		if err != nil {
			return nil, fmt.Errorf("failed to merge provision shard id %q: %w", id.String(), err)
		}
		return ret, nil
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().ListProvisionShards().DoAndReturn(func() ocm.ProvisionShardListIterator {
		allObjs := []*csarhcpv1alpha1.ProvisionShard{}
		for _, key := range sets.StringKeySet(internalIDToProvisionShard).List() {
			obj, err := mergeClusterServiceInstance[csarhcpv1alpha1.ProvisionShard](internalIDToProvisionShard[key])
			if err != nil {
				panic(fmt.Errorf("failed to merge provision shard id %q: %w", key, err))
			}
			allObjs = append(allObjs, obj)
		}
		return ocm.NewSimpleProvisionShardListIterator(allObjs, nil)
	}).AnyTimes()
	s.MockClusterServiceClient.EXPECT().GetClusterHypershiftDetails(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, id ocm.InternalID) (*cmv1.HypershiftConfig, error) {
		ret, err := mergeClusterServiceInstance[cmv1.HypershiftConfig](internalIDToHypershiftDetails[id.String()])
		if err != nil {
			return nil, fmt.Errorf("failed to merge hypershift details id %q: %w", id.String(), err)
		}
		return ret, nil
	}).AnyTimes()
}

func (s *ClusterServiceMock) AddContent(t *testing.T, initialDataDir fs.FS) error {
	internalIDToCluster := s.GetOrCreateMockData(t.Name() + "_clusters")
	internalIDToExternalAuth := s.GetOrCreateMockData(t.Name() + "_externalAuths")
	internalIDToNodePool := s.GetOrCreateMockData(t.Name() + "_nodePools")
	internalIDToProvisionShard := s.GetOrCreateMockData(t.Name() + "_provisionShards")
	internalIDToHypershiftDetails := s.GetOrCreateMockData(t.Name() + "_hypershiftDetails")

	dirContent, err := fs.ReadDir(initialDataDir, ".")
	if err != nil {
		return fmt.Errorf("failed to read dir: %w", err)
	}

	var pendingAutoscalers []*arohcpv1alpha1.ClusterAutoscaler
	internalIDToAutoscaler := make(map[string][]*arohcpv1alpha1.ClusterAutoscaler)

	for _, dirEntry := range dirContent {
		if dirEntry.IsDir() {
			return fmt.Errorf("dir %s is not a file", dirEntry.Name())
		}
		fileReader, err := initialDataDir.Open(dirEntry.Name())
		if err != nil {
			return fmt.Errorf("failed to open file %s: %w", dirEntry.Name(), err)
		}
		fileContent, err := io.ReadAll(fileReader)
		if err != nil {
			return fmt.Errorf("failed to read file %s: %w", dirEntry.Name(), err)
		}

		switch {
		case strings.HasSuffix(dirEntry.Name(), "-cluster.json"):
			obj, err := arohcpv1alpha1.UnmarshalCluster(fileContent)
			if err != nil {
				return fmt.Errorf("failed to unmarshal cluster: %w", err)
			}
			if _, exists := internalIDToCluster[obj.HREF()]; exists {
				return fmt.Errorf("duplicate cluster: %s", obj.HREF())
			}
			internalIDToCluster[obj.HREF()] = []any{obj}

		case strings.HasSuffix(dirEntry.Name(), "-externalauth.json"):
			obj, err := arohcpv1alpha1.UnmarshalExternalAuth(fileContent)
			if err != nil {
				return fmt.Errorf("failed to unmarshal nodepool: %w", err)
			}
			if _, exists := internalIDToExternalAuth[obj.HREF()]; exists {
				return fmt.Errorf("duplicate nodepool: %s", obj.HREF())
			}
			internalIDToExternalAuth[obj.HREF()] = []any{obj}

		case strings.HasSuffix(dirEntry.Name(), "-nodepool.json"):
			obj, err := arohcpv1alpha1.UnmarshalNodePool(fileContent)
			if err != nil {
				return fmt.Errorf("failed to unmarshal nodepool: %w", err)
			}
			if _, exists := internalIDToNodePool[obj.HREF()]; exists {
				return fmt.Errorf("duplicate nodepool: %s", obj.HREF())
			}
			internalIDToNodePool[obj.HREF()] = []any{obj}

		case strings.HasSuffix(dirEntry.Name(), "-autoscaler.json"):
			obj, err := arohcpv1alpha1.UnmarshalClusterAutoscaler(fileContent)
			if err != nil {
				return fmt.Errorf("failed to unmarshal autoscaler: %w", err)
			}
			if _, exists := internalIDToAutoscaler[obj.HREF()]; exists {
				return fmt.Errorf("duplicate autoscaler: %s", obj.HREF())
			}
			internalIDToAutoscaler[obj.HREF()] = []*arohcpv1alpha1.ClusterAutoscaler{obj}
			pendingAutoscalers = append(pendingAutoscalers, obj)

		case strings.HasSuffix(dirEntry.Name(), "-provisionshard.json"):
			href, err := clusterHrefFromClusterServiceSubResource(fileContent)
			if err != nil {
				return fmt.Errorf("failed to get _cluster_href from provision shard file %s: %w", dirEntry.Name(), err)
			}
			obj, err := arohcpv1alpha1.UnmarshalProvisionShard(fileContent)
			if err != nil {
				return fmt.Errorf("failed to unmarshal provision shard: %w", err)
			}
			if _, exists := internalIDToProvisionShard[href]; exists {
				return fmt.Errorf("duplicate provision shard for cluster: %s", href)
			}
			internalIDToProvisionShard[href] = []any{obj}

		case strings.HasSuffix(dirEntry.Name(), "-hypershiftdetails.json"):
			href, err := clusterHrefFromClusterServiceSubResource(fileContent)
			if err != nil {
				return fmt.Errorf("failed to get _cluster_href from hypershift details file %s: %w", dirEntry.Name(), err)
			}
			obj, err := cmv1.UnmarshalHypershiftConfig(fileContent)
			if err != nil {
				return fmt.Errorf("failed to unmarshal hypershift details: %w", err)
			}
			if _, exists := internalIDToHypershiftDetails[href]; exists {
				return fmt.Errorf("duplicate hypershift details for cluster: %s", href)
			}
			internalIDToHypershiftDetails[href] = []any{obj}

		default:
			return fmt.Errorf("unknown file %s", dirEntry.Name())
		}
	}

	for _, autoscaler := range pendingAutoscalers {
		clusterHref := autoscaler.HREF()
		clusterHistory, exists := internalIDToCluster[clusterHref]
		if !exists {
			return fmt.Errorf("autoscaler fixture references cluster %q but no matching -cluster.json was loaded", clusterHref)
		}
		updatedHistory, err := embedAutoscalerInClusterHistory(clusterHistory, autoscaler)
		if err != nil {
			return fmt.Errorf("failed to embed autoscaler into cluster %q: %w", clusterHref, err)
		}
		internalIDToCluster[clusterHref] = updatedHistory
	}

	return nil
}

func clusterHrefFromClusterServiceSubResource(fileContent []byte) (string, error) {
	content := map[string]any{}
	if err := json.Unmarshal(fileContent, &content); err != nil {
		return "", fmt.Errorf("failed to unmarshal cluster-service type: %w", err)
	}
	return content["_cluster_href"].(string), nil
}

func addFakeAzureIdentityData(clusterServiceCluster any) (*csarhcpv1alpha1.Cluster, error) {
	// the API is so hard to work with that we'll make it a map[string]any to manipulate it
	inJSON, err := datadump.MarshalClusterServiceAny(clusterServiceCluster)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal cluster-service type: %w", err)
	}
	content := map[string]any{}
	if err := json.Unmarshal(inJSON, &content); err != nil {
		return nil, fmt.Errorf("failed to unmarshal cluster-service type: %w", err)
	}

	controlPlaneManagedIdentities, _, _ := unstructured.NestedMap(content, "azure", "operators_authentication", "managed_identities", "control_plane_operators_managed_identities")
	if len(controlPlaneManagedIdentities) > 0 {
		for key, clusterServiceManagedIdentityInfo := range controlPlaneManagedIdentities {
			setFakeAzureIdentityFields(key, clusterServiceManagedIdentityInfo)
		}
		err := unstructured.SetNestedMap(content, controlPlaneManagedIdentities, "azure", "operators_authentication", "managed_identities", "control_plane_operators_managed_identities")
		if err != nil {
			return nil, fmt.Errorf("failed to set nested map: %w", err)
		}
	}

	dataPlaneManagedIdentities, _, _ := unstructured.NestedMap(content, "azure", "operators_authentication", "managed_identities", "data_plane_operators_managed_identities")
	if len(dataPlaneManagedIdentities) > 0 {
		for key, clusterServiceManagedIdentityInfo := range dataPlaneManagedIdentities {
			setFakeAzureIdentityFields(key, clusterServiceManagedIdentityInfo)
		}
		err := unstructured.SetNestedMap(content, dataPlaneManagedIdentities, "azure", "operators_authentication", "managed_identities", "data_plane_operators_managed_identities")
		if err != nil {
			return nil, fmt.Errorf("failed to set nested map: %w", err)
		}
	}

	serviceManagedIdentity, _, _ := unstructured.NestedMap(content, "azure", "operators_authentication", "managed_identities", "service_managed_identity")
	if serviceManagedIdentity != nil {
		setFakeAzureIdentityFields("service-managed-identity", serviceManagedIdentity)
		err = unstructured.SetNestedMap(content, serviceManagedIdentity, "azure", "operators_authentication", "managed_identities", "service_managed_identity")
		if err != nil {
			return nil, fmt.Errorf("failed to set nested map: %w", err)
		}
	}

	outJSON, err := json.MarshalIndent(content, "", "    ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal cluster-service type: %w", err)
	}
	return csarhcpv1alpha1.UnmarshalCluster(outJSON)
}

func setFakeAzureIdentityFields(key string, uncastManagedIdentity any) any {
	managedIdentity := uncastManagedIdentity.(map[string]any)
	managedIdentity["client_id"] = key + "_fake-client-id"
	managedIdentity["principal_id"] = key + "_fake-principal-id"
	return managedIdentity
}

func (s *ClusterServiceMock) Cleanup(ctx context.Context) {
	if err := s.saveClusterServiceMockData(ctx); err != nil {
		fmt.Printf("Failed to save mock data: %v\n", err)
	}
}

func (s *ClusterServiceMock) saveClusterServiceMockData(ctx context.Context) error {
	for dataName, clusterServiceData := range s.mockData {
		for clusterServiceName, clusterServiceHistory := range clusterServiceData {
			for i, currCluster := range clusterServiceHistory {
				basename := fmt.Sprintf("%d_%s.json", i, strings.ReplaceAll(clusterServiceName, "/", "."))
				filename := filepath.Join(s.ArtifactsDir, "cluster-service-mock-data", dataName, strings.ReplaceAll(clusterServiceName, "/", "."), basename)
				dirname := filepath.Dir(filename)
				if err := os.MkdirAll(dirname, 0755); err != nil {
					return fmt.Errorf("failed to create directory %s: %w", dirname, err)
				}

				clusterServiceBytes, err := datadump.MarshalClusterServiceAny(currCluster)
				if err != nil {
					return fmt.Errorf("failed to marshal cluster: %w", err)
				}
				obj := map[string]any{}
				if err := json.Unmarshal(clusterServiceBytes, &obj); err != nil {
					return fmt.Errorf("failed to unmarshal cluster: %w", err)
				}
				prettyPrint, err := json.MarshalIndent(obj, "", "    ")
				if err != nil {
					return fmt.Errorf("failed to marshal document: %w", err)
				}
				if err := os.WriteFile(filename, prettyPrint, 0644); err != nil {
					return fmt.Errorf("failed to write document to %s: %w", filename, err)
				}
			}
		}
	}

	return nil
}

func (s *ClusterServiceMock) GetOrCreateMockData(dataName string) map[string][]any {
	if existing, ok := s.mockData[dataName]; ok {
		return existing
	}
	newData := map[string][]any{}
	s.mockData[dataName] = newData
	return newData
}

// GetMergedClusters returns all clusters stored in the mock, each merged from
// its full mutation history and serialized to a JSON map. The testName should
// be t.Name() from the test that created the mock.
func (s *ClusterServiceMock) GetMergedClusters(testName string) ([]map[string]any, error) {
	data := s.GetOrCreateMockData(testName + "_clusters")
	var result []map[string]any
	for _, history := range data {
		mergedJSON, err := mergeClusterServiceReturn(history)
		if err != nil {
			return nil, fmt.Errorf("merging cluster service return: %w", err)
		}
		var m map[string]any
		if err := json.Unmarshal(mergedJSON, &m); err != nil {
			return nil, fmt.Errorf("unmarshaling merged cluster JSON: %w", err)
		}
		result = append(result, m)
	}
	return result, nil
}

func mergeClusterServiceReturn(history []any) ([]byte, error) {
	if len(history) == 0 {
		retErr, err := ocmerrors.NewError().Status(http.StatusNotFound).Build()
		if err != nil {
			panic(err)
		}
		return nil, retErr
	}
	// this looks insane, but cluster-service has some of the toughest API and client constructs to manage.
	// we need to merge the history together, but the CS types resist that, so taking it all back to maps is easier.
	dest := map[string]any{}
	for _, curr := range history {
		clusterServiceJSON, err := datadump.MarshalClusterServiceAny(curr)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal cluster-service type: %w", err)
		}
		currMap := map[string]any{}
		if err := json.Unmarshal(clusterServiceJSON, &currMap); err != nil {
			return nil, fmt.Errorf("failed to unmarshal cluster-service type: %w", err)
		}
		// mergo merges map keys additively — keys in dest absent from src are
		// kept. Properties must be replaced wholesale so that deleted properties
		// don't survive across mutations.
		if _, ok := currMap["properties"]; ok {
			delete(dest, "properties")
		}
		if err := mergo.Merge(&dest, currMap, mergo.WithOverride); err != nil {
			return nil, fmt.Errorf("failed to merge cluster-service type: %w", err)
		}
	}
	return json.Marshal(dest)
}

func mergeClusterServiceInstance[T any](history []any) (*T, error) {
	mergedJSON, err := mergeClusterServiceReturn(history)
	if err != nil {
		return nil, err
	}

	return unmarshalClusterServiceAny[T](mergedJSON)
}

func embedAutoscalerInClusterHistory(clusterHistory []any, autoscaler *arohcpv1alpha1.ClusterAutoscaler) ([]any, error) {
	if len(clusterHistory) == 0 {
		return nil, fmt.Errorf("cluster history is empty")
	}

	if autoscaler == nil {
		return nil, fmt.Errorf("autoscaler is nil")
	}

	mergedJSON, err := mergeClusterServiceReturn(clusterHistory)
	if err != nil {
		return nil, err
	}

	cluster, err := arohcpv1alpha1.UnmarshalCluster(mergedJSON)
	if err != nil {
		return nil, err
	}

	embedded, err := arohcpv1alpha1.NewCluster().Copy(cluster).
		Autoscaler(arohcpv1alpha1.NewClusterAutoscaler().Copy(autoscaler)).
		Build()
	if err != nil {
		return nil, err
	}
	result := append([]any(nil), clusterHistory[:len(clusterHistory)-1]...)
	result = append(result, embedded)
	return result, nil
}

func unmarshalClusterServiceAny[T any](mergedJSON []byte) (*T, error) {
	var obj T
	switch any(&obj).(type) {
	case *csarhcpv1alpha1.Cluster:
		ret, err := csarhcpv1alpha1.UnmarshalCluster(mergedJSON)
		if err != nil {
			return nil, err
		}
		return any(ret).(*T), err
	case *csarhcpv1alpha1.ClusterAutoscaler:
		ret, err := csarhcpv1alpha1.UnmarshalClusterAutoscaler(mergedJSON)
		if err != nil {
			return nil, err
		}
		return any(ret).(*T), err
	case *csarhcpv1alpha1.NodePool:
		ret, err := csarhcpv1alpha1.UnmarshalNodePool(mergedJSON)
		if err != nil {
			return nil, err
		}
		return any(ret).(*T), err
	case *csarhcpv1alpha1.ExternalAuth:
		ret, err := csarhcpv1alpha1.UnmarshalExternalAuth(mergedJSON)
		if err != nil {
			return nil, err
		}
		return any(ret).(*T), err
	case *csarhcpv1alpha1.ProvisionShard:
		ret, err := csarhcpv1alpha1.UnmarshalProvisionShard(mergedJSON)
		if err != nil {
			return nil, err
		}
		return any(ret).(*T), err
	case *cmv1.HypershiftConfig:
		ret, err := cmv1.UnmarshalHypershiftConfig(mergedJSON)
		if err != nil {
			return nil, err
		}
		return any(ret).(*T), err
	default:
		return nil, fmt.Errorf("unknown type: %T", &obj)
	}
}
