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

import (
	"encoding/json"
	"fmt"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/require"

	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

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

	"github.com/Azure/ARO-HCP/internal/api/coreapi"
	"github.com/Azure/ARO-HCP/internal/database/cosmosstorage/cosmosstorageutils"
)

func ResourceInstanceEquals(t *testing.T, expected, actual any) (string, bool) {
	expectedBytes, err := json.Marshal(expected)
	require.NoError(t, err)
	actualBytes, err := json.Marshal(actual)
	require.NoError(t, err)

	expectedMap := map[string]any{}
	require.NoError(t, json.Unmarshal(expectedBytes, &expectedMap))
	actualMap := map[string]any{}
	require.NoError(t, json.Unmarshal(actualBytes, &actualMap))

	// clear the fields from TypedDocument (wrapper) that don't compare
	for _, currMap := range []map[string]any{expectedMap, actualMap} {
		unstructured.RemoveNestedField(currMap, "_rid")
		unstructured.RemoveNestedField(currMap, "_self")
		unstructured.RemoveNestedField(currMap, "_etag")
		unstructured.RemoveNestedField(currMap, "_attachments")
		unstructured.RemoveNestedField(currMap, "_ts")
		unstructured.RemoveNestedField(currMap, "metadata", "managedFields")
		unstructured.RemoveNestedField(currMap, "endTime") // for coreapi.OperationStatus
		// etag is dynamically generated, so remove it from cosmosMetadata as well
		unstructured.RemoveNestedField(currMap, "cosmosMetadata", "etag")
		unstructured.RemoveNestedField(currMap, "properties", "cosmosMetadata", "etag")
		// temporary and not worth tracking
		unstructured.RemoveNestedField(currMap, "cosmosMetadata", "existingCosmosUID")
		unstructured.RemoveNestedField(currMap, "properties", "cosmosMetadata", "existingCosmosUID")
		// createdAt varies on every run, so ignore it (but keep createdBy and createdByType for comparison)
		unstructured.RemoveNestedField(currMap, "systemData", "createdAt")
		// lastModifiedAt also varies on every run
		unstructured.RemoveNestedField(currMap, "systemData", "lastModifiedAt")

		// these are case insensitive
		if value, ok := currMap["resourceID"].(string); ok && len(value) > 0 {
			currMap["resourceID"] = strings.ToLower(value)
		}
		if value, ok := currMap["resourceType"].(string); ok && len(value) > 0 {
			currMap["resourceType"] = strings.ToLower(value)
		}

		// if id is empty, compute it from resourceID using coreapi.ResourceIDToCosmosID
		if idVal, _ := currMap["id"].(string); len(idVal) == 0 {
			if resourceIDStr, ok := currMap["resourceID"].(string); ok && len(resourceIDStr) > 0 {
				resourceID, err := azcorearm.ParseResourceID(resourceIDStr)
				if err == nil {
					if cosmosID, err := coreapi.ResourceIDToCosmosID(resourceID); err == nil {
						currMap["id"] = cosmosID
					}
				}
			}
		}
		if resourceIDStr, ok := currMap["resourceID"].(string); ok && len(resourceIDStr) > 0 {
			currMap["resourceID"] = strings.ToLower(resourceIDStr)
		}

		resourceType, ok := currMap["resourceType"].(string)
		if !ok || len(resourceType) == 0 {
			// this happens when not working directly against cosmos data
			if resourceIDString, ok := currMap["resourceId"].(string); ok { // usually where we hold it
				resourceID, err := azcorearm.ParseResourceID(resourceIDString)
				if err == nil {
					resourceType = resourceID.ResourceType.String()
				}
			} else {
				// otherwise start checking. operations are common
				if _, ok := currMap["operationId"].(string); ok {
					resourceType = coreapi.OperationStatusResourceType.String()
				}
			}
		}

		switch {
		case strings.EqualFold(resourceType, coreapi.OperationStatusResourceType.String()):
			// this field is UUID generated, so usually cannot be compared for operations, but CAN be compared for everything else.
			unstructured.RemoveNestedField(currMap, "id")
			unstructured.RemoveNestedField(currMap, "resourceID")
		}

		// this loops handles the cosmosObj possibility and the internalObj possibility
		for _, possiblePrepend := range []string{"", "properties"} {
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "lastTransitionTime")...) // operations
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "startTime")...)          // operations
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "operationId")...)        // operations
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "cosmosMetadata", "etag")...)

			// inline serialization (cluster, nodepool, externalauth) also exposes these UUID-generated / variable fields directly under properties
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "serviceProviderProperties", "clusterUID")...)                        // cluster - UUID generated
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "serviceProviderProperties", "activeOperationId")...)                 // cluster, nodepool, externalauth - UUID generated
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "serviceProviderProperties", "clusterServiceID")...)                  // cluster, nodepool, externalauth - randomly generated by cluster-service mock
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "serviceProviderProperties", "deletionTimestamp")...)                 // nodepool - timestamp at which the NodePool deletion was requested - varies on every run on delete
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "serviceProviderProperties", "createOperationCompletionDeadline")...) // cluster, nodepool - deadline computed from time.Now() - varies on every run on create
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "serviceProviderProperties", "deleteOperationCompletionDeadline")...) // cluster - deadline computed from DeletionTimestamp - varies on every run on delete
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "systemData", "createdAt")...)
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "systemData", "lastModifiedAt")...)

			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "activeOperationId")...) // cluster, nodepool, externalauth
			unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "internalId")...)        // cluster, nodepool, externalauth

			// for controllers
			expectedConditions, found, err := unstructured.NestedSlice(currMap, prepend(possiblePrepend, "status", "conditions")...)
			if found && err == nil {
				for i := range expectedConditions {
					delete(expectedConditions[i].(map[string]any), "lastTransitionTime")
				}
				if err := unstructured.SetNestedSlice(currMap, expectedConditions, prepend(possiblePrepend, "status", "conditions")...); err != nil {
					panic(err)
				}
			}

			switch {
			case strings.EqualFold(resourceType, coreapi.OperationStatusResourceType.String()):
				// this field is UUID generated, so usually cannot be compared for operations, but CAN be compared for everything else.
				unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "resourceId")...)
				unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "resourceID")...)
				// cosmosMetadata.resourceID is derived from the same UUID-generated data, so strip it too.
				unstructured.RemoveNestedField(currMap, prepend(possiblePrepend, "cosmosMetadata")...)
			}
		}
	}

	return cmp.Diff(expectedMap, actualMap), equality.Semantic.DeepEqual(expectedMap, actualMap)
}

func prepend(first string, rest ...string) []string {
	if len(first) == 0 {
		return rest
	}
	return append([]string{first}, rest...)
}

func ResourceName(resource any) string {
	switch cast := resource.(type) {
	case coreapi.CosmosMetadataAccessor:
		return cast.GetResourceID().String()
	case coreapi.CosmosPersistable:
		return cast.GetCosmosData().ResourceID.String()

	case cosmosstorageutils.TypedDocument:
		return cast.ResourceID.String()
	case *cosmosstorageutils.TypedDocument:
		return cast.ResourceID.String()

	default:
		return fmt.Sprintf("%v", resource)
	}
}
