package internal_api

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource/clustersearch"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
)

type fakeClusterMetadataDataSource struct {
	dashboardItems    []*view.DashboardItemExternal
	dashboardTotal    int64
	dashboardErr      error
	dashboardFilter   *view.ListOrganizationAndClusterFilter
	dashboardPage     int
	dashboardPageSize int

	organization          *db.Organization
	organizationErr       error
	resolvedExternalOrgID uint

	clusters          []db.Cluster
	clustersTotal     int64
	listErr           error
	listedQuery       string
	listedOrgID       uint
	listedProjectID   uint
	listedPage        int
	listedPageSize    int
	listedHideShared  bool
	listedHideDeleted bool

	cluster            *db.Cluster
	clusterErr         error
	requestedClusterID string
	requestedOrgID     uint
}

func (f *fakeClusterMetadataDataSource) ListOrganizationAndCluster(
	_ *session.CtxUser,
	filter *view.ListOrganizationAndClusterFilter,
	page, pageSize int,
) ([]*view.DashboardItemExternal, int64, error) {
	f.dashboardFilter = filter
	f.dashboardPage = page
	f.dashboardPageSize = pageSize
	return f.dashboardItems, f.dashboardTotal, f.dashboardErr
}

func (f *fakeClusterMetadataDataSource) GetOrganizationByCloudID(_ *session.CtxUser, orgID uint) (*db.Organization, error) {
	f.resolvedExternalOrgID = orgID
	return f.organization, f.organizationErr
}

func (f *fakeClusterMetadataDataSource) ListClusters(
	_ *session.CtxUser,
	query string,
	orgID, projectID uint,
	page, pageSize int,
	hideShared, hideDeleted bool,
) ([]db.Cluster, int64, error) {
	f.listedQuery = query
	f.listedOrgID = orgID
	f.listedProjectID = projectID
	f.listedPage = page
	f.listedPageSize = pageSize
	f.listedHideShared = hideShared
	f.listedHideDeleted = hideDeleted
	return f.clusters, f.clustersTotal, f.listErr
}

func (f *fakeClusterMetadataDataSource) GetCluster(_ *session.CtxUser, clusterID string, orgID uint) (*db.Cluster, error) {
	f.requestedClusterID = clusterID
	f.requestedOrgID = orgID
	return f.cluster, f.clusterErr
}

func TestDataProxyClusterMetadataRoutesDelegateToExistingClinicDatasources(t *testing.T) {
	createdAt := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
	org := &db.Organization{
		Model:      gorm.Model{ID: 7},
		DbaasOrgID: 123,
		OrgType:    string(view.CloudDataSource),
	}
	cluster := db.Cluster{
		Model:        gorm.Model{ID: 11, CreatedAt: createdAt},
		ClusterID:    "cluster-prod",
		OrgID:        org.ID,
		TenantID:     uint64(org.DbaasOrgID),
		Name:         "production",
		Status:       "available",
		Version:      "v8.5.0",
		Provider:     "aws",
		Region:       "us-west-2",
		DeployType:   "dedicated",
		DeployTypeV2: "dedicated",
		ProjectID:    "project-prod",
		ParentID:     "parent-prod",
		ShootName:    "shoot-prod",
		KeyspaceID:   42,
	}
	source := &fakeClusterMetadataDataSource{
		dashboardItems: []*view.DashboardItemExternal{{
			DashboardItemInternal: &view.DashboardItemInternal{
				TenantID:            "123",
				ClusterID:           cluster.ClusterID,
				ClusterName:         cluster.Name,
				ClusterCreatedAt:    createdAt.UnixMilli(),
				ClusterStatus:       cluster.Status,
				ClusterDeployType:   cluster.DeployType,
				ClusterDeployTypeV2: cluster.DeployTypeV2,
			},
			ClusterVersion:      cluster.Version,
			ClusterProviderName: cluster.Provider,
			ClusterRegionName:   cluster.Region,
			ClusterShootName:    cluster.ShootName,
			ProjectID:           cluster.ProjectID,
			ParentID:            cluster.ParentID,
			OrgID:               "7",
		}},
		dashboardTotal: 1,
		organization:   org,
		clusters:       []db.Cluster{cluster},
		clustersTotal:  3,
		cluster:        &cluster,
	}
	router := clusterMetadataTestRouter(source)

	response := performClusterMetadataRequest(router,
		"/clinic/api/v1/internal/data-proxy/v1/clusters/cluster-prod")
	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t, `{
		"clusterId":"cluster-prod",
		"orgId":"123",
		"name":"production",
		"status":"available",
		"deleted":false,
		"version":"v8.5.0",
		"provider":"aws",
		"region":"us-west-2",
		"deployType":"dedicated",
		"deployTypeV2":"dedicated",
		"createdAt":"2024-01-02T03:04:05Z",
		"diagnosticContext":{"projectId":"project-prod","parentId":"parent-prod","shootName":"shoot-prod"}
	}`, response.Body.String())
	require.Equal(t, []string{"cluster-prod"}, source.dashboardFilter.ClusterIDs)
	require.False(t, source.dashboardFilter.Hide_deleted)
	require.Equal(t, 1, source.dashboardPage)
	require.Equal(t, 2, source.dashboardPageSize)

	response = performClusterMetadataRequest(router,
		"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters?page=2&pageSize=1&query=prod&showDeleted=true")
	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t, `{
		"items":[{
			"clusterId":"cluster-prod",
			"orgId":"123",
			"name":"production",
			"status":"available",
			"deleted":false,
			"version":"v8.5.0",
			"provider":"aws",
			"region":"us-west-2",
			"deployType":"dedicated",
			"deployTypeV2":"dedicated",
			"createdAt":"2024-01-02T03:04:05Z",
			"diagnosticContext":{"projectId":"project-prod","parentId":"parent-prod","shootName":"shoot-prod","keyspaceId":"42"}
		}],
		"page":2,
		"pageSize":1,
		"total":3
	}`, response.Body.String())
	require.Equal(t, uint(123), source.resolvedExternalOrgID)
	require.Equal(t, "prod", source.listedQuery)
	require.Equal(t, org.ID, source.listedOrgID)
	require.Zero(t, source.listedProjectID)
	require.Equal(t, 2, source.listedPage)
	require.Equal(t, 1, source.listedPageSize)
	require.False(t, source.listedHideShared)
	require.False(t, source.listedHideDeleted)

	response = performClusterMetadataRequest(router,
		"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-prod")
	require.Equal(t, http.StatusOK, response.Code)
	require.Contains(t, response.Body.String(), `"keyspaceId":"42"`)
	require.Equal(t, "cluster-prod", source.requestedClusterID)
	require.Equal(t, org.ID, source.requestedOrgID)
}

func TestDataProxyClusterMetadataRoutesPreserveTransportBoundaries(t *testing.T) {
	t.Run("invalid parameters", func(t *testing.T) {
		router := clusterMetadataTestRouter(&fakeClusterMetadataDataSource{})

		for _, target := range []string{
			"/clinic/api/v1/internal/data-proxy/v1/orgs/not-an-org/clusters",
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters?page=0",
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters?pageSize=201",
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters?showDeleted=maybe",
		} {
			response := performClusterMetadataRequest(router, target)
			require.Equal(t, http.StatusBadRequest, response.Code, target)
		}
	})

	t.Run("missing cloud organization", func(t *testing.T) {
		router := clusterMetadataTestRouter(&fakeClusterMetadataDataSource{organizationErr: gorm.ErrRecordNotFound})
		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters")
		require.Equal(t, http.StatusNotFound, response.Code)
	})

	t.Run("cluster ownership mismatch", func(t *testing.T) {
		source := &fakeClusterMetadataDataSource{
			organization: &db.Organization{
				Model:      gorm.Model{ID: 7},
				DbaasOrgID: 123,
				OrgType:    string(view.CloudDataSource),
			},
			cluster: &db.Cluster{
				ClusterID: "cluster-other",
				OrgID:     7,
				TenantID:  456,
			},
		}
		router := clusterMetadataTestRouter(source)
		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-other")
		require.Equal(t, http.StatusNotFound, response.Code)
	})

	t.Run("unknown detail is not found", func(t *testing.T) {
		source := &fakeClusterMetadataDataSource{
			organization: &db.Organization{
				Model:      gorm.Model{ID: 7},
				DbaasOrgID: 123,
				OrgType:    string(view.CloudDataSource),
			},
			clusterErr: errors.New("cluster is not found from management service"),
		}
		router := clusterMetadataTestRouter(source)
		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/unknown")
		require.Equal(t, http.StatusNotFound, response.Code)
	})

	t.Run("known detail datasource failure remains server error", func(t *testing.T) {
		source := &fakeClusterMetadataDataSource{
			organization: &db.Organization{
				Model:      gorm.Model{ID: 7},
				DbaasOrgID: 123,
				OrgType:    string(view.CloudDataSource),
			},
			clusterErr: errors.New("management service timeout"),
			dashboardItems: []*view.DashboardItemExternal{{
				DashboardItemInternal: &view.DashboardItemInternal{
					TenantID:  "123",
					ClusterID: "cluster-prod",
				},
				OrgID: "7",
			}},
		}
		router := clusterMetadataTestRouter(source)
		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-prod")
		require.Equal(t, http.StatusInternalServerError, response.Code)
	})

	t.Run("global exact rejects a non-cloud organization row", func(t *testing.T) {
		source := &fakeClusterMetadataDataSource{
			organization: &db.Organization{
				Model:      gorm.Model{ID: 7},
				DbaasOrgID: 123,
				OrgType:    string(view.CloudDataSource),
			},
			dashboardItems: []*view.DashboardItemExternal{{
				DashboardItemInternal: &view.DashboardItemInternal{
					TenantID:  "123",
					ClusterID: "cluster-op",
				},
				OrgID: "8",
			}},
		}
		router := clusterMetadataTestRouter(source)
		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/clusters/cluster-op")
		require.Equal(t, http.StatusNotFound, response.Code)
	})

	t.Run("existing text search policy unavailable", func(t *testing.T) {
		source := &fakeClusterMetadataDataSource{
			organization: &db.Organization{
				Model:      gorm.Model{ID: 7},
				DbaasOrgID: 123,
				OrgType:    string(view.CloudDataSource),
			},
			listErr: clustersearch.ErrTextSearchPolicyUnavailable,
		}
		router := clusterMetadataTestRouter(source)
		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters?query=prod")
		require.Equal(t, http.StatusServiceUnavailable, response.Code)
	})
}

func TestDataProxyClusterMetadataDeletedMapping(t *testing.T) {
	deletedAt := time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC)
	org := &db.Organization{DbaasOrgID: 123}
	cluster := &db.Cluster{
		Model:     gorm.Model{DeletedAt: gorm.DeletedAt{Time: deletedAt, Valid: true}},
		ClusterID: "cluster-deleted",
		Status:    "available",
	}
	require.True(t, dataProxyClusterMetadataFromCluster(cluster, org).Deleted)

	item := &view.DashboardItemExternal{DashboardItemInternal: &view.DashboardItemInternal{
		TenantID:      "123",
		ClusterID:     "cluster-deleted-status",
		ClusterStatus: "DeleteComplete",
	}}
	result, err := dataProxyClusterMetadataFromDashboard(item)
	require.NoError(t, err)
	require.True(t, result.Deleted)
}

func clusterMetadataTestRouter(source clusterMetadataDataSource) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(error2.ErrorHandlerFn)
	router.Use(func(c *gin.Context) {
		c.Set(session.CtxUserKey, &session.CtxUser{
			UserID:   3,
			Email:    "clinic@pingcap.com",
			Internal: true,
		})
		c.Next()
	})
	registerDataProxyClusterMetadataRoutes(
		router.Group("/clinic/api/v1/internal"),
		newDataProxyClusterMetadataService(source),
	)
	return router
}

func performClusterMetadataRequest(router http.Handler, target string) *httptest.ResponseRecorder {
	request := httptest.NewRequest(http.MethodGet, target, nil)
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	return response
}
