package controller

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	userapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreiamv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/iam/v1"
	coreuserv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	"github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/pkg/controller/auth"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"

	"github.com/gin-gonic/gin"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// scopeFromIDs builds a single role scope with the given allowed tenant
// IDs. A nil/empty list represents an unrestricted role.
func scopeFromIDs(allowedTenantIDs []uint64) *userapiv1.PrincipalRoleScope {
	return &userapiv1.PrincipalRoleScope{AllowedTenantIds: allowedTenantIDs}
}

func newTenantTestRouter(scope *userapiv1.PrincipalRoleScope) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.GET("/tenants/:tenant_id/foo",
		func(c *gin.Context) {
			c.Set(consts.CtxOperatorType, consts.CtxOperatorTypeAPIKey)
			c.Set(consts.CtxRoleScope, scope)
		},
		TenantScopeMiddleware(),
		func(c *gin.Context) { c.Status(http.StatusOK) },
	)
	return r
}

func TestCorsMiddlewareAllowsIdempotencyKey(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(CorsMiddleware())
	router.POST("/premiumUpgradePlans", func(ctx *gin.Context) {
		ctx.Status(http.StatusOK)
	})

	recorder := httptest.NewRecorder()
	request, err := http.NewRequest(http.MethodOptions, "/premiumUpgradePlans", nil)
	if err != nil {
		t.Fatalf("create request: %v", err)
	}
	request.Header.Set("Origin", "https://ops-dev.tidbcloud.com")
	request.Header.Set("Access-Control-Request-Method", http.MethodPost)
	request.Header.Set("Access-Control-Request-Headers", "content-type,idempotency-key")

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusNoContent {
		t.Fatalf("got status %d, want %d", recorder.Code, http.StatusNoContent)
	}
	allowedHeaders := strings.ToLower(recorder.Header().Get("Access-Control-Allow-Headers"))
	if !strings.Contains(allowedHeaders, "idempotency-key") {
		t.Fatalf("Access-Control-Allow-Headers %q does not allow Idempotency-Key", allowedHeaders)
	}
}

func TestTenantScopeMiddleware(t *testing.T) {
	tests := []struct {
		name       string
		scope      *userapiv1.PrincipalRoleScope
		tenantID   string
		wantStatus int
	}{
		{"nil scope treated as unrestricted", nil, "1", http.StatusOK},
		{"empty allow list is unrestricted", scopeFromIDs(nil), "1", http.StatusOK},
		{"allowed tenant passes", scopeFromIDs([]uint64{1, 2}), "1", http.StatusOK},
		{"disallowed tenant is denied", scopeFromIDs([]uint64{1, 2}), "3", http.StatusForbidden},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := newTenantTestRouter(tt.scope)
			w := httptest.NewRecorder()
			req, _ := http.NewRequest("GET", "/tenants/"+tt.tenantID+"/foo", nil)
			r.ServeHTTP(w, req)
			if w.Code != tt.wantStatus {
				t.Errorf("got status %d, want %d", w.Code, tt.wantStatus)
			}
		})
	}
}

func TestTenantScopeMiddleware_NonAPIKey(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.GET("/tenants/:tenant_id/foo",
		TenantScopeMiddleware(),
		func(c *gin.Context) { c.Status(http.StatusOK) },
	)
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/tenants/999/foo", nil)
	r.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Errorf("got status %d, want %d", w.Code, http.StatusOK)
	}
}

func TestSetIdentityContext(t *testing.T) {
	gin.SetMode(gin.TestMode)
	ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
	identity := &auth.AuthnResult{
		APIKey: &coreiamv1.APIKey{Name: "key-a", Owner: "owner@pingcap.com"},
		PrincipalProfile: &userapiv1.PrincipalProfile{
			OperatorType: consts.CtxOperatorTypeAPIKey,
			OperatorName: "key-a",
			UserEmail:    "owner@pingcap.com",
			AggregatedRoleScope: &userapiv1.PrincipalRoleScope{
				AllowedTenantIds: []uint64{101, 102},
			},
		},
	}

	(&AuthMiddlewareBuilder{}).setIdentityContext(ctx, identity)

	if got := ctx.GetString(consts.CtxOperatorType); got != consts.CtxOperatorTypeAPIKey {
		t.Fatalf("operator type = %q", got)
	}
	if got := ctx.GetString(consts.CtxOperator); got != "key-a" {
		t.Fatalf("operator = %q", got)
	}
	if got := ctx.GetString(consts.CtxUserEmail); got != "owner@pingcap.com" {
		t.Fatalf("user email = %q", got)
	}
	scope, ok := roleScopeFromCtx(ctx)
	if !ok {
		t.Fatal("role scope is not set")
	}
	if len(scope.AllowedTenantIds) != 2 || scope.AllowedTenantIds[0] != 101 || scope.AllowedTenantIds[1] != 102 {
		t.Fatalf("allowed tenant ids = %v", scope.AllowedTenantIds)
	}
}

func TestAuthnErrorCode(t *testing.T) {
	if got := authnErrorCode(status.Error(codes.PermissionDenied, "denied")); got != api.ErrCodePermissionDenied {
		t.Fatalf("permission denied code = %v", got)
	}
	if got := authnErrorCode(status.Error(codes.Unauthenticated, "invalid")); got != api.ErrCodeUnauthenticated {
		t.Fatalf("unauthenticated code = %v", got)
	}
}

func TestSoc2Middleware(t *testing.T) {
	tests := []struct {
		name       string
		email      string
		mock       func(*opscore.MockUserServiceClient)
		wantStatus int
	}{
		{
			name:       "passes when user email is absent",
			wantStatus: http.StatusOK,
		},
		{
			name:  "passes when user is soc2 compliant",
			email: "user@pingcap.com",
			mock: func(userCli *opscore.MockUserServiceClient) {
				userCli.EXPECT().
					GetUser(gomock.Any(), &userapiv1.GetUserRequest{Email: "user@pingcap.com"}).
					Return(&userapiv1.GetUserResponse{
						User: &coreuserv1.User{
							Email:         "user@pingcap.com",
							Soc2Compliant: true,
						},
					}, nil)
			},
			wantStatus: http.StatusOK,
		},
		{
			name:  "rejects when user is not soc2 compliant",
			email: "user@pingcap.com",
			mock: func(userCli *opscore.MockUserServiceClient) {
				userCli.EXPECT().
					GetUser(gomock.Any(), &userapiv1.GetUserRequest{Email: "user@pingcap.com"}).
					Return(&userapiv1.GetUserResponse{
						User: &coreuserv1.User{
							Email:         "user@pingcap.com",
							Soc2Compliant: false,
						},
					}, nil)
			},
			wantStatus: http.StatusForbidden,
		},
		{
			name:  "fails when user service returns error",
			email: "user@pingcap.com",
			mock: func(userCli *opscore.MockUserServiceClient) {
				userCli.EXPECT().
					GetUser(gomock.Any(), &userapiv1.GetUserRequest{Email: "user@pingcap.com"}).
					Return(nil, errors.New("user service failed"))
			},
			wantStatus: http.StatusInternalServerError,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gin.SetMode(gin.TestMode)
			ctrl := gomock.NewController(t)
			userCli := opscore.NewMockUserServiceClient(ctrl)
			if tt.mock != nil {
				tt.mock(userCli)
			}

			r := gin.New()
			r.GET("/foo",
				func(c *gin.Context) {
					if tt.email != "" {
						c.Set(consts.CtxUserEmail, tt.email)
					}
				},
				Soc2Middleware(userCli),
				func(c *gin.Context) { c.Status(http.StatusOK) },
			)
			w := httptest.NewRecorder()
			req, _ := http.NewRequest(http.MethodGet, "/foo", nil)
			r.ServeHTTP(w, req)
			if w.Code != tt.wantStatus {
				t.Errorf("got status %d, want %d", w.Code, tt.wantStatus)
			}
		})
	}
}

func TestTenantScopeMiddleware_RejectWhenTenantScopeMissingForRestrictedAPIKey(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.GET("/tenants",
		func(c *gin.Context) {
			c.Set(consts.CtxOperatorType, consts.CtxOperatorTypeAPIKey)
			c.Set(consts.CtxRoleScope, scopeFromIDs([]uint64{1}))
		},
		TenantScopeMiddleware(),
		func(c *gin.Context) { c.Status(http.StatusOK) },
	)
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/tenants", nil)
	r.ServeHTTP(w, req)
	if w.Code != http.StatusForbidden {
		t.Errorf("got status %d, want %d", w.Code, http.StatusForbidden)
	}
}

func TestCheckResourcePoolScope(t *testing.T) {
	tests := []struct {
		name         string
		scope        *userapiv1.PrincipalRoleScope
		resourcePool *nextgenv1.ResourcePool
		wantErr      bool
	}{
		{
			name:         "unrestricted api key can access shared pool",
			scope:        scopeFromIDs(nil),
			resourcePool: &nextgenv1.ResourcePool{Id: 123, OwnerType: nextgenv1.ResourcePool_SHARED},
		},
		{
			name:         "tenant restricted api key cannot access shared pool",
			scope:        scopeFromIDs([]uint64{456}),
			resourcePool: &nextgenv1.ResourcePool{Id: 123, OwnerType: nextgenv1.ResourcePool_SHARED},
			wantErr:      true,
		},
		{
			name:         "tenant restricted api key can access owned pool",
			scope:        scopeFromIDs([]uint64{456}),
			resourcePool: &nextgenv1.ResourcePool{Id: 123, OwnerType: nextgenv1.ResourcePool_ORGANIZATION, OwnerId: 456},
		},
		{
			name:         "tenant restricted api key cannot access another tenant pool",
			scope:        scopeFromIDs([]uint64{789}),
			resourcePool: &nextgenv1.ResourcePool{Id: 123, OwnerType: nextgenv1.ResourcePool_ORGANIZATION, OwnerId: 456},
			wantErr:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gin.SetMode(gin.TestMode)
			w := httptest.NewRecorder()
			ctx, _ := gin.CreateTestContext(w)
			ctx.Set(consts.CtxRoleScope, tt.scope)

			coreSvcCli := &coresvccli.CoreSvcCli{
				Mgmt: &coresvccli.MgmtService{
					ResourcePool: fakeMiddlewareResourcePoolClient{resourcePool: tt.resourcePool},
				},
			}

			err := CheckResourcePoolScope(ctx, coreSvcCli, tt.resourcePool.GetId())
			if tt.wantErr && err == nil {
				t.Fatalf("expected error")
			}
			if !tt.wantErr && err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

type fakeMiddlewareResourcePoolClient struct {
	mgmtv1.ResourcePoolServiceClient
	resourcePool *nextgenv1.ResourcePool
}

func (f fakeMiddlewareResourcePoolClient) CreateResourcePool(context.Context, *nextgenv1.CreateResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePoolProfile, error) {
	panic("not implemented")
}

func (f fakeMiddlewareResourcePoolClient) ListResourcePools(context.Context, *nextgenv1.ListResourcePoolsRequest, ...grpc.CallOption) (*nextgenv1.ListResourcePoolsResponse, error) {
	panic("not implemented")
}

func (f fakeMiddlewareResourcePoolClient) UpdateResourcePoolName(context.Context, *nextgenv1.UpdateResourcePoolNameRequest, ...grpc.CallOption) (*nextgenv1.ResourcePool, error) {
	panic("not implemented")
}

func (f fakeMiddlewareResourcePoolClient) UpdateResourcePoolInstanceLimit(context.Context, *nextgenv1.UpdateResourcePoolInstanceLimitRequest, ...grpc.CallOption) (*nextgenv1.ResourcePool, error) {
	panic("not implemented")
}

func (f fakeMiddlewareResourcePoolClient) GetResourcePool(context.Context, *nextgenv1.GetResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePoolProfile, error) {
	return &nextgenv1.ResourcePoolProfile{
		ResourcePool: f.resourcePool,
		Region: &commonv1.Region{
			Id:              commonv1.Region_AWS_US_WEST_2,
			CloudProviderId: commonv1.CloudProvider_AWS,
		},
	}, nil
}

func (f fakeMiddlewareResourcePoolClient) DeleteResourcePool(context.Context, *nextgenv1.DeleteResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePool, error) {
	panic("not implemented")
}

func (f fakeMiddlewareResourcePoolClient) ListOrgResourcePools(context.Context, *mgmtv1.ListOrgResourcePoolsRequest, ...grpc.CallOption) (*nextgenv1.ListOwnerResourcePoolsResponse, error) {
	panic("not implemented")
}

func (f fakeMiddlewareResourcePoolClient) UpdateOrgDefaultResourcePool(context.Context, *mgmtv1.UpdateOrgDefaultResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePoolStrategy, error) {
	panic("not implemented")
}
