package cloud

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

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

func TestDashboardClustersIncludeTotalContract(t *testing.T) {
	tests := []struct {
		name             string
		path             string
		result           *datasource.ListOrganizationAndClusterResult
		wantIncludeTotal bool
		wantTotal        bool
		wantHasMore      any
	}{
		{
			name:             "clusters defaults to no total",
			path:             "/dashboard/clusters?limit=2",
			result:           &datasource.ListOrganizationAndClusterResult{Items: dashboardItems(2), HasMore: true},
			wantIncludeTotal: false,
			wantTotal:        false,
			wantHasMore:      true,
		},
		{
			name:             "clusters accepts explicit total",
			path:             "/dashboard/clusters?limit=2&include_total=true",
			result:           dashboardListResultWithTotal(3),
			wantIncludeTotal: true,
			wantTotal:        true,
			wantHasMore:      nil,
		},
		{
			name:             "clusters omits total and reports more rows",
			path:             "/dashboard/clusters?limit=2&include_total=false",
			result:           &datasource.ListOrganizationAndClusterResult{Items: dashboardItems(2), HasMore: true},
			wantIncludeTotal: false,
			wantTotal:        false,
			wantHasMore:      true,
		},
		{
			name:             "clusters2 defaults to no total",
			path:             "/dashboard/clusters2?limit=2",
			result:           &datasource.ListOrganizationAndClusterResult{Items: dashboardItems(2), HasMore: true},
			wantIncludeTotal: false,
			wantTotal:        false,
			wantHasMore:      true,
		},
		{
			name:             "clusters2 omits total and reports more rows",
			path:             "/dashboard/clusters2?limit=2&include_total=false",
			result:           &datasource.ListOrganizationAndClusterResult{Items: dashboardItems(2), HasMore: true},
			wantIncludeTotal: false,
			wantTotal:        false,
			wantHasMore:      true,
		},
		{
			name:             "clusters2 accepts explicit total",
			path:             "/dashboard/clusters2?limit=2&include_total=true",
			result:           dashboardListResultWithTotal(3),
			wantIncludeTotal: true,
			wantTotal:        true,
			wantHasMore:      nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var gotIncludeTotal bool
			service := &GlobalDashboardService{
				listOrganizationAndCluster: func(
					_ *session.CtxUser,
					_ *view.ListOrganizationAndClusterFilter,
					_, _ int,
					includeTotal bool,
				) (*datasource.ListOrganizationAndClusterResult, error) {
					gotIncludeTotal = includeTotal
					return tt.result, nil
				},
			}
			router := dashboardTestRouter(service)

			response := httptest.NewRecorder()
			router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, tt.path, nil))

			require.Equal(t, http.StatusOK, response.Code)
			assert.Equal(t, tt.wantIncludeTotal, gotIncludeTotal)

			var body map[string]any
			require.NoError(t, json.Unmarshal(response.Body.Bytes(), &body))
			_, hasTotal := body["total"]
			assert.Equal(t, tt.wantTotal, hasTotal)
			if tt.wantTotal {
				assert.Equal(t, float64(3), body["total"])
			}
			assert.Equal(t, tt.wantHasMore, body["has_more"])
			assert.Len(t, body["items"], len(tt.result.Items))
		})
	}
}

func TestDashboardClustersRejectsInvalidIncludeTotal(t *testing.T) {
	for _, path := range []string{
		"/dashboard/clusters?include_total=invalid",
		"/dashboard/clusters2?include_total=invalid",
	} {
		t.Run(path, func(t *testing.T) {
			called := false
			service := &GlobalDashboardService{
				listOrganizationAndCluster: func(
					_ *session.CtxUser,
					_ *view.ListOrganizationAndClusterFilter,
					_, _ int,
					_ bool,
				) (*datasource.ListOrganizationAndClusterResult, error) {
					called = true
					return nil, nil
				},
			}

			response := httptest.NewRecorder()
			dashboardTestRouter(service).ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))

			assert.Equal(t, http.StatusBadRequest, response.Code)
			assert.False(t, called)
		})
	}
}

func TestDashboardClustersRejectsInvalidPagination(t *testing.T) {
	for _, path := range []string{
		"/dashboard/clusters?limit=9223372036854775807",
		"/dashboard/clusters2?limit=9223372036854775807",
	} {
		t.Run(path, func(t *testing.T) {
			service := &GlobalDashboardService{
				listOrganizationAndCluster: func(
					_ *session.CtxUser,
					_ *view.ListOrganizationAndClusterFilter,
					_, _ int,
					_ bool,
				) (*datasource.ListOrganizationAndClusterResult, error) {
					return nil, datasource.ErrInvalidDashboardPagination
				},
			}

			response := httptest.NewRecorder()
			dashboardTestRouter(service).ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))

			assert.Equal(t, http.StatusBadRequest, response.Code)
		})
	}
}

func dashboardTestRouter(service *GlobalDashboardService) *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{Internal: true})
		c.Next()
	})
	router.GET("/dashboard/clusters", service.CloudClusters)
	router.GET("/dashboard/clusters2", service.CloudClusters2)
	return router
}

func dashboardListResultWithTotal(total int64) *datasource.ListOrganizationAndClusterResult {
	return &datasource.ListOrganizationAndClusterResult{
		Items: dashboardItems(2),
		Total: &total,
	}
}

func dashboardItems(count int) []*view.DashboardItemExternal {
	return make([]*view.DashboardItemExternal, count)
}

func TestQPSQuerySuffix(t *testing.T) {
	expected := "{threshold=\"100\",region=~\"us-west-1|us-west-2\"}[2d]"
	actual := getQPSQuerySuffix("100", "2d", []string{"us-west-1", "us-west-2"}, nil, nil, nil, nil, nil)
	assert.Equal(t, expected, actual)

	expected = "{threshold=\"100\"}[2d]"
	actual = getQPSQuerySuffix("100", "2d", nil, nil, nil, nil, nil, nil)
	assert.Equal(t, expected, actual)
}

func TestQPSQueryWithSearch(t *testing.T) {
	expected := "max_over_time(up{threshold=\"100\",region=~\"us-west-1|us-west-2\"}[1d])"
	actual := getQPSQueryWithSearch("max_over_time(up%s)", "100", "", []string{"us-west-1", "us-west-2"}, nil)
	assert.Equal(t, expected, actual)

	expected = `(max_over_time(up{threshold="100",cluster_id=~"(?i).*123.*"}[1d]),max_over_time(up{threshold="100",cluster_name=~"(?i).*123.*"}[1d]),max_over_time(up{threshold="100",tenant_id=~"(?i).*123.*"}[1d]),max_over_time(up{threshold="100",tenant_name=~"(?i).*123.*"}[1d]))`
	actual = getQPSQueryWithSearch("max_over_time(up%s)", "100", "123", nil, nil)
	assert.Equal(t, expected, actual)
}
