package handlers

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/clustercontext"
	"github.com/stretchr/testify/require"
)

type fakeClusterMetadataRepository struct {
	getByClusterID       func(context.Context, string) (*clustercontext.Cluster, error)
	getByOrgAndClusterID func(context.Context, uint64, string) (*clustercontext.Cluster, error)
	listByOrg            func(context.Context, uint64, clustercontext.ListOptions) (*clustercontext.Page, error)
}

func (f fakeClusterMetadataRepository) GetByClusterID(ctx context.Context, clusterID string) (*clustercontext.Cluster, error) {
	return f.getByClusterID(ctx, clusterID)
}

func (f fakeClusterMetadataRepository) GetByOrgAndClusterID(ctx context.Context, orgID uint64, clusterID string) (*clustercontext.Cluster, error) {
	return f.getByOrgAndClusterID(ctx, orgID, clusterID)
}

func (f fakeClusterMetadataRepository) ListByOrg(ctx context.Context, orgID uint64, opts clustercontext.ListOptions) (*clustercontext.Page, error) {
	return f.listByOrg(ctx, orgID, opts)
}

func metadataTestRouter(repository clustercontext.Repository) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	NewClusterMetadataHandler(repository).RegisterRoutes(router)
	return router
}

type fakeUserClusterMetadataRepository struct {
	listByOrgForUser func(context.Context, uint64, clustercontext.ListOptions, string, map[string][]string) (*clustercontext.Page, error)
}

func (f fakeUserClusterMetadataRepository) ListByOrgForUser(ctx context.Context, orgID uint64, opts clustercontext.ListOptions, authorization string, headers map[string][]string) (*clustercontext.Page, error) {
	return f.listByOrgForUser(ctx, orgID, opts, authorization, headers)
}

func metadataTestRouterWithUser(repository clustercontext.Repository, userRepository clustercontext.UserRepository) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	NewClusterMetadataHandlerWithUserRepository(repository, userRepository).RegisterRoutes(router)
	return router
}

func TestClusterMetadataHandlerExactLookupAcceptsOpaqueClusterID(t *testing.T) {
	repository := fakeClusterMetadataRepository{
		getByClusterID: func(_ context.Context, clusterID string) (*clustercontext.Cluster, error) {
			require.Equal(t, "cluster-01_A", clusterID)
			return &clustercontext.Cluster{
				ClusterID:    clusterID,
				OrgID:        "1234567890123456789",
				Name:         "production",
				DeployTypeV2: "dedicated",
			}, nil
		},
	}
	router := metadataTestRouter(repository)

	req := httptest.NewRequest(http.MethodGet, "/data-proxy/api/v1/clusters/cluster-01_A", nil)
	req.Header.Set("X-Provider", "attacker-controlled")
	req.Header.Set("X-Region", "attacker-controlled")
	response := httptest.NewRecorder()
	router.ServeHTTP(response, req)

	require.Equal(t, http.StatusOK, response.Code)
	var got clustercontext.Cluster
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got))
	require.Equal(t, "cluster-01_A", got.ClusterID)
	require.Equal(t, "1234567890123456789", got.OrgID)
}

func TestClusterMetadataHandlerOrgOwnershipAndNotFound(t *testing.T) {
	repository := fakeClusterMetadataRepository{
		getByOrgAndClusterID: func(_ context.Context, orgID uint64, clusterID string) (*clustercontext.Cluster, error) {
			require.Equal(t, uint64(123), orgID)
			require.Equal(t, "cluster-a", clusterID)
			return nil, clustercontext.ErrNotFound
		},
	}
	router := metadataTestRouter(repository)

	response := httptest.NewRecorder()
	router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/data-proxy/api/v1/orgs/123/clusters/cluster-a", nil))

	require.Equal(t, http.StatusNotFound, response.Code)
	var got map[string]any
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got))
	require.Equal(t, "NotFound", got["code"])
}

func TestClusterMetadataHandlerListOptions(t *testing.T) {
	repository := fakeClusterMetadataRepository{
		listByOrg: func(_ context.Context, orgID uint64, opts clustercontext.ListOptions) (*clustercontext.Page, error) {
			require.Equal(t, uint64(18446744073709551615), orgID)
			require.Equal(t, 2, opts.Page)
			require.Equal(t, 25, opts.PageSize)
			require.Equal(t, "prod", opts.Query)
			require.True(t, opts.ShowDeleted)
			return &clustercontext.Page{Items: []clustercontext.Cluster{}, Page: 2, PageSize: 25, Total: 30}, nil
		},
	}
	router := metadataTestRouter(repository)

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/data-proxy/api/v1/orgs/18446744073709551615/clusters?page=2&pageSize=25&query=prod&showDeleted=true", nil)
	router.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t, `{"items":[],"page":2,"pageSize":25,"total":30}`, response.Body.String())
}

func TestClusterMetadataHandlerRegistersPublicAliases(t *testing.T) {
	repository := fakeClusterMetadataRepository{
		getByOrgAndClusterID: func(_ context.Context, orgID uint64, clusterID string) (*clustercontext.Cluster, error) {
			require.Equal(t, uint64(123), orgID)
			require.Equal(t, "cluster-a", clusterID)
			return &clustercontext.Cluster{ClusterID: clusterID, OrgID: "123"}, nil
		},
		listByOrg: func(_ context.Context, orgID uint64, _ clustercontext.ListOptions) (*clustercontext.Page, error) {
			require.Equal(t, uint64(123), orgID)
			return &clustercontext.Page{Items: []clustercontext.Cluster{}}, nil
		},
	}
	userRepository := fakeUserClusterMetadataRepository{
		listByOrgForUser: func(_ context.Context, orgID uint64, _ clustercontext.ListOptions, authorization string, _ map[string][]string) (*clustercontext.Page, error) {
			require.Equal(t, uint64(123), orgID)
			require.Equal(t, "Bearer test-token", authorization)
			return &clustercontext.Page{Items: []clustercontext.Cluster{}}, nil
		},
	}
	router := metadataTestRouterWithUser(repository, userRepository)

	listResponse := httptest.NewRecorder()
	listRequest := httptest.NewRequest(http.MethodGet, "/api/v1/orgs/123/data-proxy/clusters", nil)
	listRequest.Header.Set("Authorization", "Bearer test-token")
	router.ServeHTTP(listResponse, listRequest)
	require.Equal(t, http.StatusOK, listResponse.Code)

	clusterResponse := httptest.NewRecorder()
	router.ServeHTTP(clusterResponse, httptest.NewRequest(http.MethodGet, "/api/v1/orgs/123/data-proxy/clusters/cluster-a", nil))
	require.Equal(t, http.StatusOK, clusterResponse.Code)
}

func TestClusterMetadataHandlerUserListRejectsUnsupportedFiltersAndMissingAuthorization(t *testing.T) {
	router := metadataTestRouterWithUser(fakeClusterMetadataRepository{}, fakeUserClusterMetadataRepository{
		listByOrgForUser: func(context.Context, uint64, clustercontext.ListOptions, string, map[string][]string) (*clustercontext.Page, error) {
			return &clustercontext.Page{}, nil
		},
	})
	for _, path := range []string{
		"/api/v1/orgs/123/data-proxy/clusters?query=prod",
		"/api/v1/orgs/123/data-proxy/clusters?showDeleted=true",
	} {
		response := httptest.NewRecorder()
		router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
		require.Equal(t, http.StatusBadRequest, response.Code, path)
	}
	response := httptest.NewRecorder()
	router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/v1/orgs/123/data-proxy/clusters", nil))
	require.Equal(t, http.StatusUnauthorized, response.Code)
}

func TestClusterMetadataHandlerRejectsInvalidParameters(t *testing.T) {
	repository := fakeClusterMetadataRepository{}
	router := metadataTestRouter(repository)

	tests := []string{
		"/data-proxy/api/v1/orgs/not-a-number/clusters/cluster-a",
		"/data-proxy/api/v1/orgs/123/clusters?page=1000001",
		"/data-proxy/api/v1/orgs/123/clusters?pageSize=201",
		"/data-proxy/api/v1/orgs/123/clusters?showDeleted=maybe",
	}
	for _, path := range tests {
		t.Run(path, func(t *testing.T) {
			response := httptest.NewRecorder()
			router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
			require.Equal(t, http.StatusBadRequest, response.Code)
		})
	}
}

func TestClusterMetadataHandlerUnavailable(t *testing.T) {
	router := metadataTestRouter(clustercontext.NewUnavailableRepository())

	response := httptest.NewRecorder()
	router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/data-proxy/api/v1/clusters/cluster-a", nil))

	require.Equal(t, http.StatusServiceUnavailable, response.Code)
	var got map[string]any
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got))
	require.Equal(t, "Unavailable", got["code"])
}

func TestClusterMetadataHandlerTimeout(t *testing.T) {
	repository := fakeClusterMetadataRepository{
		getByClusterID: func(context.Context, string) (*clustercontext.Cluster, error) {
			return nil, clustercontext.ErrTimeout
		},
	}
	router := metadataTestRouter(repository)

	response := httptest.NewRecorder()
	router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/data-proxy/api/v1/clusters/cluster-a", nil))

	require.Equal(t, http.StatusGatewayTimeout, response.Code)
	var got map[string]any
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got))
	require.Equal(t, "DeadlineExceeded", got["code"])
}
