package rbac

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	authsdk "github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/mock/gomock"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/adapter/account/mock_account"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/entity"
)

func TestGetResourceForTagBindUnbindPaths(t *testing.T) {
	testCases := []struct {
		name     string
		method   string
		path     string
		expected string
	}{
		{
			name:     "clusterManagement bind tags",
			method:   http.MethodPost,
			path:     "/api/v2023/clusterManagement/v1beta2/tidbs/123:bindTags",
			expected: "/clusterManagement/orgs/{org_id}/tidbs/{tidb_id}/tags/bind",
		},
		{
			name:     "clusterManagement unbind tags",
			method:   http.MethodPost,
			path:     "/api/v2023/clusterManagement/v1beta2/tidbs/123:unbindTags",
			expected: "/clusterManagement/orgs/{org_id}/tidbs/{tidb_id}/tags/unbind",
		},
		{
			name:     "dedicated bind tags",
			method:   http.MethodPost,
			path:     "/api/v2023/dedicated/v1beta1/clusters/123:bindTags",
			expected: "/dedicated/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/tags/bind",
		},
		{
			name:     "dedicated unbind tags",
			method:   http.MethodPost,
			path:     "/api/v2023/dedicated/v1beta1/clusters/123:unbindTags",
			expected: "/dedicated/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/tags/unbind",
		},
		{
			name:     "serverless bind tags",
			method:   http.MethodPost,
			path:     "/api/v2023/serverless/v1beta1/clusters/123:bindTags",
			expected: "/serverless/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/tags/bind",
		},
		{
			name:     "serverless unbind tags",
			method:   http.MethodPost,
			path:     "/api/v2023/serverless/v1beta1/clusters/123:unbindTags",
			expected: "/serverless/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/tags/unbind",
		},
		{
			name:     "serverless batch delete clusters",
			method:   http.MethodPost,
			path:     "/api/v2023/serverless/v1beta1/clusters:batchDelete",
			expected: "/serverless/orgs/{org_id}/clusters/batchDelete",
		},
		{
			name:     "serverless shadow pool get",
			method:   http.MethodGet,
			path:     "/serverless/v1beta1/shadowPools/10241133482228153213",
			expected: "/serverless/orgs/{org_id}/projects/{project_id}/shadowPools/{shadow_pool_id}/get",
		},
		{
			name:     "nextgen bind tags",
			method:   http.MethodPost,
			path:     "/v1beta2/tidbs/123:bindTags",
			expected: "/nextgen/orgs/{org_id}/tidbs/{tidb_id}/tags/bind",
		},
		{
			name:     "nextgen unbind tags",
			method:   http.MethodPost,
			path:     "/v1beta2/tidbs/123:unbindTags",
			expected: "/nextgen/orgs/{org_id}/tidbs/{tidb_id}/tags/unbind",
		},
		{
			name:     "nextgen resume tidb",
			method:   http.MethodPost,
			path:     "/v1beta2/tidbs/123:resumeTidb",
			expected: "/nextgen/orgs/{org_id}/tidbs/{tidb_id}/resume",
		},
		{
			name:     "nextgen estimate price",
			method:   http.MethodPost,
			path:     "/v1beta2/tidbs:estimatePrice",
			expected: "/nextgen/orgs/{org_id}/tidbs/estimatePrice/calculate",
		},
		{
			name:     "clusterManagement get inventory",
			method:   http.MethodGet,
			path:     "/api/v2023/clusterManagement/v1beta2/tidbs:inventory",
			expected: "/clusterManagement/orgs/{org_id}/tidbs/inventory",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := &gin.Context{Request: &http.Request{Method: tc.method, URL: &url.URL{Path: tc.path}}}
			assert.Equal(t, tc.expected, getResource(ctx))
		})
	}
}

func TestGetResourceForResourcePoolPaths(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		method   string
		path     string
		expected string
	}{
		{name: "create", method: http.MethodPost, path: "/v1beta2/resourcePools", expected: "/nextgen/orgs/{org_id}/resourcePools/create"},
		{name: "list", method: http.MethodGet, path: "/v1beta2/resourcePools", expected: "/nextgen/orgs/{org_id}/resourcePools/list"},
		{
			name:     "get",
			method:   http.MethodGet,
			path:     "/v1beta2/resourcePools/42",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/get",
		},
		{
			name:     "update",
			method:   http.MethodPatch,
			path:     "/v1beta2/resourcePools/42",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/update",
		},
		{
			name:     "delete",
			method:   http.MethodDelete,
			path:     "/v1beta2/resourcePools/42",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/delete",
		},
		{
			name:     "resume",
			method:   http.MethodPost,
			path:     "/v1beta2/resourcePools/42:resumeResourcePool",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/resume",
		},
		{
			name:     "constraints",
			method:   http.MethodGet,
			path:     "/v1beta2/resourcePools/42/capacityConstraints",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/capacityConstraints/get",
		},
		{
			name:     "resize",
			method:   http.MethodPost,
			path:     "/v1beta2/resourcePools/42:resizeCapacity",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/capacity/resize",
		},
		{
			name:     "tags",
			method:   http.MethodPost,
			path:     "/v1beta2/resourcePools/42:upsertCloudResourceTags",
			expected: "/nextgen/orgs/{org_id}/resourcePools/{resource_pool_id}/cloudResourceTags/upsert",
		},
		{
			name:     "aws node specs",
			method:   http.MethodGet,
			path:     "/v1beta2/regions/aws-us-west-2/nodeSpecs",
			expected: "/nextgen/orgs/{org_id}/regions/nodeSpecs/list",
		},
		{
			name:     "gcp node specs",
			method:   http.MethodGet,
			path:     "/v1beta2/regions/gcp-us-central1/nodeSpecs",
			expected: "/nextgen/orgs/{org_id}/regions/nodeSpecs/list",
		},
		{
			name:     "azure node specs",
			method:   http.MethodGet,
			path:     "/v1beta2/regions/azure-eastus/nodeSpecs",
			expected: "/nextgen/orgs/{org_id}/regions/nodeSpecs/list",
		},
		{
			name:     "alicloud node specs",
			method:   http.MethodGet,
			path:     "/v1beta2/regions/alicloud-cn-hangzhou/nodeSpecs",
			expected: "/nextgen/orgs/{org_id}/regions/nodeSpecs/list",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := &gin.Context{Request: &http.Request{Method: tc.method, URL: &url.URL{Path: tc.path}}}
			assert.Equal(t, tc.expected, getResource(ctx))
		})
	}
}

func TestGetResourceForResourcePoolCIDRPaths(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		method   string
		path     string
		expected string
	}{
		{
			name:     "plan through OpenAPI",
			method:   http.MethodGet,
			path:     "/v1beta2/regions/aws-us-west-2:planResourcePoolCidr",
			expected: "/nextgen/orgs/{org_id}/regions/resourcePoolCidr/plan",
		},
		{
			name:     "plan through UI API",
			method:   http.MethodGet,
			path:     "/api/v2025/cloud/v1beta2/regions/aws-us-west-2:planResourcePoolCidr",
			expected: "/nextgen/orgs/{org_id}/regions/resourcePoolCidr/plan",
		},
		{
			name:     "validate through OpenAPI",
			method:   http.MethodPost,
			path:     "/v1beta2/regions/aws-us-west-2:validateResourcePoolCidr",
			expected: "/nextgen/orgs/{org_id}/regions/resourcePoolCidr/validate",
		},
		{
			name:     "validate through UI API",
			method:   http.MethodPost,
			path:     "/api/v2025/cloud/v1beta2/regions/aws-us-west-2:validateResourcePoolCidr",
			expected: "/nextgen/orgs/{org_id}/regions/resourcePoolCidr/validate",
		},
		{
			name:   "plan rejects POST",
			method: http.MethodPost,
			path:   "/v1beta2/regions/aws-us-west-2:planResourcePoolCidr",
		},
		{
			name:   "validate rejects GET",
			method: http.MethodGet,
			path:   "/v1beta2/regions/aws-us-west-2:validateResourcePoolCidr",
		},
		{
			name:   "plan rejects suffix",
			method: http.MethodGet,
			path:   "/v1beta2/regions/aws-us-west-2:planResourcePoolCidr/unexpected",
		},
		{
			name:   "plan requires region",
			method: http.MethodGet,
			path:   "/v1beta2/regions/:planResourcePoolCidr",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := &gin.Context{Request: &http.Request{Method: tc.method, URL: &url.URL{Path: tc.path}}}
			assert.Equal(t, tc.expected, getResource(ctx))
		})
	}
}

func TestGetResourceForFsRegionPaths(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		method   string
		path     string
		expected string
	}{
		{
			name:     "list",
			method:   http.MethodGet,
			path:     "/fs/v1beta2/regions",
			expected: "/fs/orgs/{org_id}/regions/list",
		},
		{
			name:     "get",
			method:   http.MethodGet,
			path:     "/fs/v1beta2/regions/aws-us-west-2",
			expected: "/fs/orgs/{org_id}/regions/get",
		},
		{
			name:     "list through UI API",
			method:   http.MethodGet,
			path:     "/api/v2025/fs/v1beta2/regions",
			expected: "/fs/orgs/{org_id}/regions/list",
		},
		{
			name:     "get through UI API",
			method:   http.MethodGet,
			path:     "/api/v2025/fs/v1beta2/regions/aws-us-west-2",
			expected: "/fs/orgs/{org_id}/regions/get",
		},
		{
			name:   "list rejects POST",
			method: http.MethodPost,
			path:   "/fs/v1beta2/regions",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := &gin.Context{Request: &http.Request{Method: tc.method, URL: &url.URL{Path: tc.path}}}
			assert.Equal(t, tc.expected, getResource(ctx))
		})
	}
}

func TestGetResource(t *testing.T) {
	ctx := &gin.Context{
		Request: &http.Request{
			URL: &url.URL{
				Path: "/serverless/v1bate1/clusters",
			},
			Method: http.MethodGet,
		},
	}

	res := getResource(ctx)
	if res == "" {
		t.Errorf("get resource failed: %v", res)
	}
}

func TestGetResourceForPrivateLinkIPAddressTypeSetting(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		method   string
		expected string
	}{
		{
			method:   http.MethodGet,
			expected: "/dedicated/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/tidbNodeGroups/get",
		},
		{
			method:   http.MethodPatch,
			expected: "/dedicated/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/tidbNodeGroups/update",
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.method, func(t *testing.T) {
			t.Parallel()

			ctx := &gin.Context{Request: &http.Request{
				Method: testCase.method,
				URL:    &url.URL{Path: "/api/v2023/dedicated/v1beta1/clusters/101/tidbNodeGroups/22/privateLinkIpAddressTypeSetting"},
			}}
			assert.Equal(t, testCase.expected, getResource(ctx))
		})
	}
}

func TestParseClusterIdFromPath(t *testing.T) {
	testCase := map[string]string{
		"/serverless/clusters/10288773166658417668":                        "10288773166658417668",
		"/serverless/clusters/10288773166658417668/password":               "10288773166658417668",
		"/dedicated/clusters/1379661944646412897":                          "1379661944646412897",
		"/dedicated/clusters/1379661944646412897/audit_log":                "1379661944646412897",
		"/dedicated/clusters/1379661944646412897/audit_log/access_records": "1379661944646412897",
	}

	for path, id := range testCase {
		clusterID, err := parseClusterIdFromPath(path)
		if err != nil {
			t.Errorf("parse cluster id from path failed: %v", err)
		}
		if clusterID != id {
			t.Errorf("parse cluster id from path failed: %v", clusterID)
		}
	}
}

func TestParseShadowPoolIdFromPath(t *testing.T) {
	testCase := map[string]string{
		"/serverless/shadowPools/10241133482228153213":                       "10241133482228153213",
		"/serverless/shadowPools/10241133482228153213:replaceInitSqls":       "10241133482228153213",
		"/serverless/shadowPools/10241133482228153213:replaceInstanceDemand": "10241133482228153213",
	}

	for path, id := range testCase {
		shadowPoolID, err := parseShadowPoolIdFromPath(path)
		if err != nil {
			t.Errorf("parse shadow pool id from path failed: %v", err)
		}
		if shadowPoolID != id {
			t.Errorf("parse shadow pool id from path failed: %v", shadowPoolID)
		}
	}
}

func TestParseResourcePoolIdFromPath(t *testing.T) {
	t.Parallel()

	for _, path := range []string{
		"/resourcePools/42",
		"/resourcePools/42/capacityConstraints",
		"/resourcePools/42:resizeCapacity",
		"/resourcePools/42:upsertCloudResourceTags",
		"/resourcePools/42:resumeResourcePool",
	} {
		t.Run(path, func(t *testing.T) {
			t.Parallel()

			resourcePoolID, err := parseResourcePoolIdFromPath(path)
			require.NoError(t, err)
			assert.Equal(t, "42", resourcePoolID)
		})
	}

	_, err := parseResourcePoolIdFromPath("/resourcePools/+42")
	assert.Error(t, err)
}

func TestCheckPermission(t *testing.T) {
	// Init mock dependencies.
	ctrl := gomock.NewController(t)
	accountCli := mock_account.NewMockClient(ctrl)
	rbacService := rbacServiceImpl{account: accountCli, allowList: nil}

	// init auth sdk
	accountApiCli := mock_account.NewMockApiCli(ctrl)
	authsdk.InitAccountClientByClient(accountApiCli, 10*time.Second)

	testCases := []struct {
		name         string
		user         *entity.User
		expectedBool bool
	}{
		{
			name: "Access Token",
			user: &entity.User{
				User: &account.User{
					Id:    10,
					Email: "test@pingcap.com",
				},
				Org:          &account.Org{Id: 100},
				Projects:     []*account.Project{{Id: 200}, {Id: 300}},
				AccessToken:  "testRawAccessToken",
				IdentityType: authsdk.AuthnIdentityTypeAccessToken,
			},
			expectedBool: true,
		},
		{
			name: "User Token",
			user: &entity.User{
				User: &account.User{
					Id:    11,
					Email: "test2@pingcap.com",
				},
				Org:          &account.Org{Id: 100},
				Projects:     []*account.Project{{Id: 200}, {Id: 300}},
				IdentityType: authsdk.AuthnIdentityTypeUserToken,
			},
			expectedBool: true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create a new gin context for testing
			w := httptest.NewRecorder()
			ctx, _ := gin.CreateTestContext(w)
			ctx.Request, _ = http.NewRequest(http.MethodGet, "/serverless/api/v1beta1/clusters", http.NoBody)

			for range tc.user.Projects {
				accountApiCli.EXPECT().HasPermissionForIdentity(gomock.Any(), gomock.Any()).Return(
					&account.RbacIdentityPermissionResponse{AuthResult: true}, nil,
				)
			}

			result := rbacService.checkPermission(ctx, tc.user)

			assert.Equal(t, tc.expectedBool, result)
		})
	}
}
