// Copyright 2025 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 frontend

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-logr/logr/testr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"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/utils"
)

func TestMiddlewareValidateSubscription(t *testing.T) {
	subscriptionId := "sub-1234-5678"
	tenantId := "tenant-1234-5678"
	defaultRequestPath := fmt.Sprintf("subscriptions/%s/resourceGroups/xyz", subscriptionId)

	tests := []struct {
		name          string
		cachedState   coreapi.SubscriptionState
		expectedState coreapi.SubscriptionState
		httpMethod    string
		requestPath   string
		expectedError *coreapi.CloudError
	}{
		{
			name:          "subscription is already registered",
			cachedState:   coreapi.SubscriptionStateRegistered,
			expectedState: coreapi.SubscriptionStateRegistered,
			httpMethod:    http.MethodGet,
			requestPath:   defaultRequestPath,
		},
		{
			name:        "subscription is missing from path",
			cachedState: coreapi.SubscriptionStateRegistered,
			httpMethod:  http.MethodGet,
			requestPath: "/resourceGroups/abc",
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusBadRequest,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidParameter,
					Message: fmt.Sprintf(SubscriptionMissingMessage, PathSegmentSubscriptionID),
				},
			},
		},
		{
			name: "subscription is not found",
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusBadRequest,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidSubscriptionState,
					Message: fmt.Sprintf(UnregisteredSubscriptionStateMessage, subscriptionId),
				},
			},
			httpMethod:  http.MethodGet,
			requestPath: defaultRequestPath,
		},
		{
			name:        "subscription is deleted",
			cachedState: coreapi.SubscriptionStateDeleted,
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusBadRequest,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidSubscriptionState,
					Message: fmt.Sprintf(InvalidSubscriptionStateMessage, coreapi.SubscriptionStateDeleted),
				},
			},
			httpMethod:  http.MethodGet,
			requestPath: defaultRequestPath,
		},
		{
			name:        "subscription is unregistered",
			cachedState: coreapi.SubscriptionStateUnregistered,
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusBadRequest,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidSubscriptionState,
					Message: fmt.Sprintf(UnregisteredSubscriptionStateMessage, subscriptionId),
				},
			},
			httpMethod:  http.MethodGet,
			requestPath: defaultRequestPath,
		},
		{
			name:          "subscription is suspended - GET is allowed",
			cachedState:   coreapi.SubscriptionStateSuspended,
			expectedState: coreapi.SubscriptionStateSuspended,
			httpMethod:    http.MethodGet,
			requestPath:   defaultRequestPath,
		},
		{
			name:          "subscription is warned - GET is allowed",
			cachedState:   coreapi.SubscriptionStateWarned,
			expectedState: coreapi.SubscriptionStateWarned,
			httpMethod:    http.MethodGet,
			requestPath:   defaultRequestPath,
		},
		{
			name:          "subscription is warned - DELETE is allowed",
			cachedState:   coreapi.SubscriptionStateWarned,
			expectedState: coreapi.SubscriptionStateWarned,
			httpMethod:    http.MethodDelete,
			requestPath:   defaultRequestPath,
		},
		{
			name:        "subscription is warned - PUT is not allowed",
			cachedState: coreapi.SubscriptionStateWarned,
			httpMethod:  http.MethodPut,
			requestPath: defaultRequestPath,
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusConflict,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidSubscriptionState,
					Message: fmt.Sprintf(InvalidSubscriptionStateMessage, coreapi.SubscriptionStateWarned),
				},
			},
		},
		{
			name:        "subscription is suspended - POST is not allowed",
			cachedState: coreapi.SubscriptionStateSuspended,
			httpMethod:  http.MethodPost,
			requestPath: defaultRequestPath,
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusConflict,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidSubscriptionState,
					Message: fmt.Sprintf(InvalidSubscriptionStateMessage, coreapi.SubscriptionStateSuspended),
				},
			},
		},
		{
			name:        "subscription is suspended - PATCH is not allowed",
			cachedState: coreapi.SubscriptionStateSuspended,
			httpMethod:  http.MethodPatch,
			requestPath: defaultRequestPath,
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusConflict,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInvalidSubscriptionState,
					Message: fmt.Sprintf(InvalidSubscriptionStateMessage, coreapi.SubscriptionStateSuspended),
				},
			},
		},
		{
			name:        "subscription state value is invalid",
			cachedState: coreapi.SubscriptionState("Invalid"),
			httpMethod:  http.MethodGet,
			requestPath: defaultRequestPath,
			expectedError: &coreapi.CloudError{
				StatusCode: http.StatusInternalServerError,
				CloudErrorBody: &coreapi.CloudErrorBody{
					Code:    coreapi.CloudErrorCodeInternalServerError,
					Message: "Internal server error.",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockResourcesDBClient := corecosmosstoragetesting.NewMockResourcesDBClient()

			var subscription *coreapi.Subscription

			if tt.cachedState != "" {
				resourceID := metadataapi.Must(coreapi.ToSubscriptionResourceID(subscriptionId))
				subscription = &coreapi.Subscription{
					CosmosMetadata: coreapi.CosmosMetadata{
						ResourceID:   resourceID,
						PartitionKey: strings.ToLower(resourceID.SubscriptionID),
					},
					ResourceID: resourceID,
					State:      tt.cachedState,
					Properties: &coreapi.SubscriptionProperties{
						TenantId: &tenantId,
					},
				}
				// Pre-populate the subscription in the mock database
				ctx := t.Context()
				_, err := mockResourcesDBClient.Subscriptions().Create(ctx, subscription, nil)
				require.NoError(t, err)
			}

			writer := httptest.NewRecorder()

			request, err := http.NewRequest(tt.httpMethod, tt.requestPath, nil)
			assert.NoError(t, err)

			// Add a logger to the context so parsing errors will be logged.
			ctx := request.Context()
			ctx = utils.ContextWithLogger(ctx, testr.New(t))
			ctx, sr := initSpanRecorder(ctx)
			request = request.WithContext(ctx)

			next := func(w http.ResponseWriter, r *http.Request) {
				request = r // capture modified request
			}

			if tt.requestPath == defaultRequestPath {
				request.SetPathValue(PathSegmentSubscriptionID, subscriptionId)
			}

			newMiddlewareValidateSubscriptionState(mockResourcesDBClient).handleRequest(writer, request, next)

			res := writer.Result()
			if tt.expectedError != nil {
				var actualError coreapi.CloudError
				err = json.NewDecoder(res.Body).Decode(&actualError)
				assert.NoError(t, err)

				assert.Equal(t, tt.expectedError.StatusCode, res.StatusCode)
				assert.Equal(t, tt.expectedError.Code, actualError.Code)
				assert.Equal(t, tt.expectedError.Message, actualError.Message)
				return
			}

			assert.Equal(t, tt.expectedState, subscription.State)

			// Verify the subscription was stashed in the request context.
			stashedSubscription, err := SubscriptionFromContext(request.Context())
			assert.NoError(t, err)
			assert.Equal(t, subscription.ResourceID, stashedSubscription.ResourceID)

			// Check that the attributes have been added to the span too.
			ss := sr.collect()
			require.Len(t, ss, 1)
			span := ss[0]
			equalSpanAttributes(t, span, map[string]string{"aro.subscription.state": string(subscription.State)})
		})
	}

	t.Run("nil DB client in the context", func(t *testing.T) {
		writer := httptest.NewRecorder()
		writer.Code = 0 // set the zero value so we can distinguish between a written result and default

		request, err := http.NewRequest(http.MethodGet, defaultRequestPath, nil)
		assert.NoError(t, err)
		request.SetPathValue(PathSegmentSubscriptionID, subscriptionId)

		ctx := request.Context()
		ctx = utils.ContextWithLogger(ctx, testr.New(t))
		request = request.WithContext(ctx)

		next := func(w http.ResponseWriter, r *http.Request) {
			t.Fatalf("should not reach this call")
		}

		// this is a coding error (client cannot trigger this) from which the request cannot recover.  A panic appropriate.
		func() {
			defer func() {
				if r := recover(); r != nil {
					fmt.Println("Recovery as expected", r)
				}
			}()

			newMiddlewareValidateSubscriptionState(nil).handleRequest(writer, request, next)
		}()

		// 0 indicates that no result was written by this handler.  In a real chain, our panic handler writes the result
		assert.Equal(t, 0, writer.Code)
	})
}
