// 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 properties

import (
	"context"
	"fmt"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"

	arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1"

	"github.com/Azure/ARO-HCP/backend/pkg/utils/controllerutils"
	"github.com/Azure/ARO-HCP/internal/api/coreapi"
	"github.com/Azure/ARO-HCP/internal/api/metadataapi"
	"github.com/Azure/ARO-HCP/internal/database/cosmosstoragetesting/corecosmosstoragetesting"
	"github.com/Azure/ARO-HCP/internal/database/listertesting/corelistertesting"
	"github.com/Azure/ARO-HCP/internal/ocm"
)

func TestDesiredControlPlaneSizeSyncer_SyncOnce(t *testing.T) {
	largeStr := string(coreapi.HostedClusterControlPlaneSizeLarge)
	mediumStr := string(coreapi.HostedClusterControlPlaneSizeMedium)
	// CS stores the size override in lowercase (ocm.ConvertHostedClusterSizeOverrideToCS
	// normalizes it), so anything we seed into or expect from CS uses the
	// lowercased form even though the API surface uses the capitalized enum.
	largeCS := strings.ToLower(largeStr)
	mediumCS := strings.ToLower(mediumStr)

	type setup struct {
		specSize                   *string
		statusSize                 *string
		cluster                    *coreapi.HCPOpenShiftCluster
		seedServiceProviderCluster bool
		csProperties               map[string]string
		csGetErr                   error
		expectCSGet                bool
		// expectedStatusSize is what
		// ServiceProviderCluster.Status.DesiredHostedClusterControlPlaneSize
		// must equal after a successful reconcile. Nil pointer means absent.
		expectedStatusSize                *string
		expectServiceProviderClusterWrite bool
		expectErrContains                 string
	}

	tests := []struct {
		name string
		setup
	}{
		{
			name: "no ServiceProviderCluster in cache - skip",
			setup: setup{
				seedServiceProviderCluster: false,
			},
		},
		{
			name: "Spec and Status both nil - skip",
			setup: setup{
				seedServiceProviderCluster: true,
				cluster:                    newTestCluster(testClusterName),
			},
		},
		{
			name: "Spec and Status both equal - skip",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &largeStr,
				statusSize:                 &largeStr,
				cluster:                    newTestCluster(testClusterName),
			},
		},
		{
			name: "cluster missing in cache - skip",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &largeStr,
				cluster:                    nil,
			},
		},
		{
			name: "cluster has no ClusterServiceID - skip",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &largeStr,
				cluster: newTestCluster(testClusterName, func(c *coreapi.HCPOpenShiftCluster) {
					c.ServiceProviderProperties.ClusterServiceID = nil
				}),
			},
		},
		{
			name: "set transition - waits when cluster-service not yet updated",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &largeStr,
				statusSize:                 nil,
				cluster:                    newTestCluster(testClusterName),
				csProperties:               nil,
				expectCSGet:                true,
			},
		},
		{
			name: "change transition - waits when cluster-service still has stale value",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &mediumStr,
				statusSize:                 &largeStr,
				cluster:                    newTestCluster(testClusterName),
				csProperties: map[string]string{
					ocm.CSPropertySizeOverride:  largeCS,
					ocm.CSPropertySingleReplica: ocm.CSPropertyEnabled,
				},
				expectCSGet: true,
			},
		},
		{
			name: "unset transition - waits when cluster-service still has override",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   nil,
				statusSize:                 &largeStr,
				cluster:                    newTestCluster(testClusterName),
				csProperties: map[string]string{
					ocm.CSPropertySizeOverride:  largeCS,
					ocm.CSPropertySingleReplica: ocm.CSPropertyEnabled,
				},
				expectCSGet: true,
			},
		},
		{
			name: "set transition - records Status when cluster-service already matches",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &largeStr,
				statusSize:                 nil,
				cluster:                    newTestCluster(testClusterName),
				csProperties: map[string]string{
					ocm.CSPropertySizeOverride: largeCS,
				},
				expectCSGet:                       true,
				expectedStatusSize:                &largeStr,
				expectServiceProviderClusterWrite: true,
			},
		},
		{
			name: "change transition - records Status when cluster-service already matches",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &mediumStr,
				statusSize:                 &largeStr,
				cluster:                    newTestCluster(testClusterName),
				csProperties: map[string]string{
					ocm.CSPropertySizeOverride: mediumCS,
				},
				expectCSGet:                       true,
				expectedStatusSize:                &mediumStr,
				expectServiceProviderClusterWrite: true,
			},
		},
		{
			name: "unset transition - records Status when cluster-service already cleared",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   nil,
				statusSize:                 &largeStr,
				cluster:                    newTestCluster(testClusterName),
				csProperties: map[string]string{
					ocm.CSPropertySingleReplica: ocm.CSPropertyEnabled,
				},
				expectCSGet:                       true,
				expectedStatusSize:                nil,
				expectServiceProviderClusterWrite: true,
			},
		},
		{
			name: "unset transition - records Status when cluster-service has experimental fallback",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   nil,
				statusSize:                 &largeStr,
				cluster: newTestCluster(testClusterName, func(c *coreapi.HCPOpenShiftCluster) {
					c.ServiceProviderProperties.ExperimentalFeatures.ControlPlanePodSizing = coreapi.MinimalControlPlanePodSizing
				}),
				csProperties: map[string]string{
					ocm.CSPropertySizeOverride: ocm.CSPropertyE2EMinimalControlPlaneSize,
				},
				expectCSGet:                       true,
				expectedStatusSize:                nil,
				expectServiceProviderClusterWrite: true,
			},
		},
		{
			name: "CS GetCluster fails - error",
			setup: setup{
				seedServiceProviderCluster: true,
				specSize:                   &largeStr,
				cluster:                    newTestCluster(testClusterName),
				csGetErr:                   fmt.Errorf("boom"),
				expectCSGet:                true,
				expectErrContains:          "failed to get cluster from Cluster Service",
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ctx := context.Background()
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockResourcesDBClient := corecosmosstoragetesting.NewMockResourcesDBClient()

			var seededServiceProviderCluster *coreapi.ServiceProviderCluster
			if tc.seedServiceProviderCluster {
				clusterResourceID := metadataapi.Must(azcorearm.ParseResourceID(
					"/subscriptions/" + testSubscriptionID +
						"/resourceGroups/" + testResourceGroupName +
						"/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/" + testClusterName,
				))
				serviceProviderClusterCRUD := mockResourcesDBClient.ServiceProviderClusters(
					clusterResourceID.SubscriptionID,
					clusterResourceID.ResourceGroupName,
					clusterResourceID.Name,
				)
				// Use the Created return so the lister seed carries the ETag
				// the mock CRUD assigned; the syncer's Replace requires it.
				created, err := serviceProviderClusterCRUD.Create(ctx, newTestServiceProviderCluster(testClusterName, tc.specSize, tc.statusSize), nil)
				require.NoError(t, err)
				seededServiceProviderCluster = created
			}

			serviceProviderClusterListerStub := &corelistertesting.SliceServiceProviderClusterLister{}
			if seededServiceProviderCluster != nil {
				serviceProviderClusterListerStub.ServiceProviderClusters = []*coreapi.ServiceProviderCluster{seededServiceProviderCluster}
			}
			clusterLister := &corelistertesting.SliceClusterLister{}
			if tc.cluster != nil {
				clusterLister.Clusters = []*coreapi.HCPOpenShiftCluster{tc.cluster}
			}

			mockCSClient := ocm.NewMockClusterServiceClientSpec(ctrl)
			if tc.expectCSGet {
				mockCSClient.EXPECT().
					GetCluster(gomock.Any(), metadataapi.Must(metadataapi.NewInternalID(testClusterServiceIDStr))).
					Return(buildBareCSCluster(t, tc.csProperties), tc.csGetErr)
			}

			syncer := &desiredControlPlaneSizeSyncer{
				serviceProviderClusterLister: serviceProviderClusterListerStub,
				clusterLister:                clusterLister,
				resourcesDBClient:            mockResourcesDBClient,
				clusterServiceClient:         mockCSClient,
			}

			key := controllerutils.HCPClusterKey{
				SubscriptionID:    testSubscriptionID,
				ResourceGroupName: testResourceGroupName,
				HCPClusterName:    testClusterName,
			}
			err := syncer.SyncOnce(ctx, key)

			if tc.expectErrContains != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectErrContains)
				return
			}
			require.NoError(t, err)

			if !tc.expectServiceProviderClusterWrite {
				return
			}
			liveServiceProviderCluster, err := mockResourcesDBClient.
				ServiceProviderClusters(testSubscriptionID, testResourceGroupName, testClusterName).
				Get(ctx, coreapi.ServiceProviderClusterResourceName)
			require.NoError(t, err)
			if tc.expectedStatusSize == nil {
				assert.Nil(t, liveServiceProviderCluster.Status.DesiredHostedClusterControlPlaneSize)
			} else {
				require.NotNil(t, liveServiceProviderCluster.Status.DesiredHostedClusterControlPlaneSize)
				assert.Equal(t, *tc.expectedStatusSize, *liveServiceProviderCluster.Status.DesiredHostedClusterControlPlaneSize)
			}
		})
	}
}

func newTestServiceProviderCluster(hcpClusterName string, specSize, statusSize *string) *coreapi.ServiceProviderCluster {
	resourceID := metadataapi.Must(azcorearm.ParseResourceID(
		"/subscriptions/" + testSubscriptionID +
			"/resourceGroups/" + testResourceGroupName +
			"/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/" + hcpClusterName +
			"/" + coreapi.ServiceProviderClusterResourceTypeName +
			"/" + coreapi.ServiceProviderClusterResourceName,
	))
	return &coreapi.ServiceProviderCluster{
		CosmosMetadata: coreapi.CosmosMetadata{
			ResourceID:   resourceID,
			PartitionKey: strings.ToLower(resourceID.SubscriptionID),
		},
		Spec: coreapi.ServiceProviderClusterSpec{
			DesiredHostedClusterControlPlaneSize: specSize,
		},
		Status: coreapi.ServiceProviderClusterStatus{
			DesiredHostedClusterControlPlaneSize: statusSize,
		},
	}
}

// buildBareCSCluster constructs a minimal CS cluster with the supplied
// properties bag for use as the "existing" cluster in update-path tests.
func buildBareCSCluster(t *testing.T, properties map[string]string) *arohcpv1alpha1.Cluster {
	t.Helper()
	builder := arohcpv1alpha1.NewCluster()
	if len(properties) > 0 {
		builder = builder.Properties(properties)
	}
	cluster, err := builder.Build()
	require.NoError(t, err)
	return cluster
}
