package internal_api

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

	"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/config"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

type fakeDataProxyInternalReadTargetResolver struct {
	target     *o11yurls.InternalReadTarget
	err        error
	calls      int
	gotCluster *db.Cluster
}

func (f *fakeDataProxyInternalReadTargetResolver) ResolveInternalReadTarget(
	_ context.Context,
	cluster *db.Cluster,
) (*o11yurls.InternalReadTarget, error) {
	f.calls++
	f.gotCluster = cluster
	return f.target, f.err
}

func TestDataProxyTopRUTargetPremiumAndEssential(t *testing.T) {
	tests := []struct {
		name         string
		env          string
		deployType   string
		deployTypeV2 string
		targetURL    string
		wantBaseURL  string
		wantBizType  string
	}{
		{
			name:         "premium",
			env:          "staging",
			deployType:   o11y.PremiumType,
			deployTypeV2: o11y.DeployTypeV2Premium,
			targetURL:    "https://o11y-premium.example.com/internal/read",
			wantBaseURL:  "https://o11y-premium.example.com",
			wantBizType:  "staging-tidbcloud-nextgen",
		},
		{
			name:         "essential",
			env:          "prod",
			deployType:   o11y.SharedType,
			deployTypeV2: o11y.DeployTypeV2Essential,
			targetURL:    "http://o11y-essential.example.com:8481/internal/read/path",
			wantBaseURL:  "http://o11y-essential.example.com:8481",
			wantBizType:  "tidbcloud",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxyTopRUTargetSource(tt.deployType, tt.deployTypeV2)
			resolver := &fakeDataProxyInternalReadTargetResolver{target: &o11yurls.InternalReadTarget{
				InternalReadURL: tt.targetURL,
				O11yCluster:     &o11y.ClusterInfo{ID: "o11y-cluster"},
			}}
			router := dataProxyTopRUTargetTestRouter(tt.env, source, resolver, true)

			response := performClusterMetadataRequest(router,
				"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-prod/targets/top-ru")

			require.Equal(t, http.StatusOK, response.Code)
			require.JSONEq(t, `{
				"baseUrl":"`+tt.wantBaseURL+`",
				"headers":{
					"env":"`+tt.env+`",
					"bizType":"`+tt.wantBizType+`",
					"provider":"aws",
					"region":"us-west-2",
					"orgId":"123",
					"projectId":"project-prod",
					"clusterId":"cluster-prod",
					"deployType":"`+tt.deployType+`",
					"deployTypeV2":"`+tt.deployTypeV2+`"
				}
			}`, response.Body.String())
			require.Equal(t, 1, resolver.calls)
			require.Equal(t, uint(123), resolver.gotCluster.Organization.DbaasOrgID)
			require.Equal(t, uint(7), resolver.gotCluster.OrgID)
			// resolveOwnedCluster populates Organization on a copy and does not
			// mutate the existing metadata datasource result.
			require.Zero(t, source.cluster.Organization.DbaasOrgID)
		})
	}
}

func TestDataProxyTopRUTargetRejectsWrongOrgAndUnsupportedTiers(t *testing.T) {
	t.Run("wrong org", func(t *testing.T) {
		source := dataProxyTopRUTargetSource(o11y.PremiumType, o11y.DeployTypeV2Premium)
		resolver := &fakeDataProxyInternalReadTargetResolver{}
		router := dataProxyTopRUTargetTestRouter("staging", source, resolver, true)

		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/999/clusters/cluster-prod/targets/top-ru")

		require.Equal(t, http.StatusNotFound, response.Code)
		require.Zero(t, resolver.calls)
	})

	t.Run("wrong cluster", func(t *testing.T) {
		source := dataProxyTopRUTargetSource(o11y.PremiumType, o11y.DeployTypeV2Premium)
		source.clusterErr = gorm.ErrRecordNotFound
		resolver := &fakeDataProxyInternalReadTargetResolver{}
		router := dataProxyTopRUTargetTestRouter("staging", source, resolver, true)

		response := performClusterMetadataRequest(router,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/unknown/targets/top-ru")

		require.Equal(t, http.StatusNotFound, response.Code)
		require.Zero(t, resolver.calls)
	})

	for _, tt := range []struct {
		name         string
		deployType   string
		deployTypeV2 string
	}{
		{name: "byoc host", deployType: o11y.ByocHostType, deployTypeV2: o11y.DeployTypeV2Byoc},
		{name: "dedicated", deployType: o11y.DedicatedType, deployTypeV2: o11y.DeployTypeV2Dedicated},
		{name: "starter", deployType: o11y.SharedType, deployTypeV2: o11y.DeployTypeV2Starter},
		{name: "resource pool", deployType: o11y.NgHostType, deployTypeV2: o11y.DeployTypeV2PremiumResourcePool},
		{name: "conflicting legacy premium", deployType: o11y.PremiumType, deployTypeV2: o11y.DeployTypeV2Byoc},
	} {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxyTopRUTargetSource(tt.deployType, tt.deployTypeV2)
			resolver := &fakeDataProxyInternalReadTargetResolver{}
			router := dataProxyTopRUTargetTestRouter("staging", source, resolver, true)

			response := performClusterMetadataRequest(router,
				"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-prod/targets/top-ru")

			require.Equal(t, http.StatusBadRequest, response.Code)
			require.Contains(t, response.Body.String(), "unsupported for this cluster")
			require.Zero(t, resolver.calls)
		})
	}
}

func TestDataProxyTopRUTargetResolverFailures(t *testing.T) {
	tests := []struct {
		name       string
		resolver   dataProxyInternalReadTargetResolver
		wantStatus int
	}{
		{
			name: "invalid target URL",
			resolver: &fakeDataProxyInternalReadTargetResolver{target: &o11yurls.InternalReadTarget{
				InternalReadURL: "https://user:secret@o11y.example.com/internal/read",
			}},
			wantStatus: http.StatusBadGateway,
		},
		{
			name:       "resolver error",
			resolver:   &fakeDataProxyInternalReadTargetResolver{err: errors.New("global service rejected request")},
			wantStatus: http.StatusBadGateway,
		},
		{
			name:       "resolver unavailable",
			resolver:   &fakeDataProxyInternalReadTargetResolver{err: errClusterDataURLsResolverUnavailable},
			wantStatus: http.StatusServiceUnavailable,
		},
		{
			name:       "context timeout",
			resolver:   &fakeDataProxyInternalReadTargetResolver{err: context.DeadlineExceeded},
			wantStatus: http.StatusGatewayTimeout,
		},
		{
			name:       "missing resolver",
			resolver:   nil,
			wantStatus: http.StatusServiceUnavailable,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxyTopRUTargetSource(o11y.PremiumType, o11y.DeployTypeV2Premium)
			router := dataProxyTopRUTargetTestRouter("staging", source, tt.resolver, true)

			response := performClusterMetadataRequest(router,
				"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-prod/targets/top-ru")

			require.Equal(t, tt.wantStatus, response.Code)
		})
	}
}

func TestDataProxyTopRUTargetRequiresInternalUser(t *testing.T) {
	source := dataProxyTopRUTargetSource(o11y.PremiumType, o11y.DeployTypeV2Premium)
	resolver := &fakeDataProxyInternalReadTargetResolver{}
	router := dataProxyTopRUTargetTestRouter("staging", source, resolver, false)

	response := performClusterMetadataRequest(router,
		"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster-prod/targets/top-ru")

	require.Equal(t, http.StatusForbidden, response.Code)
	require.Zero(t, resolver.calls)
}

func TestDataProxyTopRUBaseURLValidation(t *testing.T) {
	tests := []struct {
		name    string
		rawURL  string
		wantURL string
	}{
		{name: "https with path", rawURL: "https://o11y.example.com/internal/read", wantURL: "https://o11y.example.com"},
		{name: "http with port", rawURL: "http://o11y.example.com:8481/a/b", wantURL: "http://o11y.example.com:8481"},
		{name: "relative", rawURL: "/internal/read"},
		{name: "non http", rawURL: "ftp://o11y.example.com/internal/read"},
		{name: "userinfo", rawURL: "https://user:secret@o11y.example.com/internal/read"},
		{name: "query", rawURL: "https://o11y.example.com/internal/read?token=secret"},
		{name: "empty query marker", rawURL: "https://o11y.example.com/internal/read?"},
		{name: "fragment", rawURL: "https://o11y.example.com/internal/read#fragment"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := dataProxyTopRUBaseURL(tt.rawURL)
			if tt.wantURL == "" {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tt.wantURL, got)
		})
	}
}

func TestNewDataProxyTopRUTargetServiceUsesClinicEnvironment(t *testing.T) {
	service := NewDataProxyTopRUTargetService(&config.Config{
		Env: "staging",
		CentralO11yConfig: &config.CentralO11yConfig{
			Env: "dev",
		},
	}, nil, nil)

	require.Equal(t, "staging", service.env)
}

func dataProxyTopRUTargetSource(deployType, deployTypeV2 string) *fakeClusterMetadataDataSource {
	org := &db.Organization{
		Model:      gorm.Model{ID: 7},
		DbaasOrgID: 123,
		OrgType:    string(view.CloudDataSource),
	}
	return &fakeClusterMetadataDataSource{
		organization: org,
		cluster: &db.Cluster{
			ClusterID:    "cluster-prod",
			OrgID:        org.ID,
			TenantID:     uint64(org.DbaasOrgID),
			Provider:     "aws",
			Region:       "us-west-2",
			ProjectID:    "project-prod",
			DeployType:   deployType,
			DeployTypeV2: deployTypeV2,
		},
	}
}

func dataProxyTopRUTargetTestRouter(
	env string,
	source clusterMetadataDataSource,
	resolver dataProxyInternalReadTargetResolver,
	internalUser bool,
) *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: internalUser,
		})
		c.Next()
	})
	metadata := newDataProxyClusterMetadataService(source)
	registerDataProxyTopRUTargetRoutes(
		router.Group("/clinic/api/v1/internal"),
		newDataProxyTopRUTargetService(env, metadata, resolver),
	)
	return router
}

func TestDataProxyTopRUTargetMetadataDetailCompatibility(t *testing.T) {
	source := dataProxyTopRUTargetSource(o11y.PremiumType, o11y.DeployTypeV2Premium)
	router := clusterMetadataTestRouter(source)

	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(), `"clusterId":"cluster-prod"`)
	require.Contains(t, response.Body.String(), `"orgId":"123"`)
}
