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"
)

const testSlowQueryO11yClusterID = "o11y-cluster_123"

func TestDataProxySlowQueryTargetSupportsOnlyConsistentBYOCClusters(t *testing.T) {
	tests := []struct {
		name             string
		deployType       string
		deployTypeV2     string
		internalReadURL  string
		wantBaseURL      string
		wantDeployTypeV2 string
	}{
		{
			name:             "BYOC logical",
			deployType:       o11y.ByocType,
			deployTypeV2:     o11y.DeployTypeV2Byoc,
			internalReadURL:  "https://byoc-ds.example.com/internal/metrics/ignored/api/v1/",
			wantBaseURL:      "https://byoc-ds.example.com",
			wantDeployTypeV2: o11y.DeployTypeV2Byoc,
		},
		{
			name:             "legacy BYOC logical is normalized",
			deployType:       o11y.ByocType,
			internalReadURL:  "http://byoc-ds.example.com/internal/metrics/ignored/api/v1/",
			wantBaseURL:      "http://byoc-ds.example.com",
			wantDeployTypeV2: o11y.DeployTypeV2Byoc,
		},
		{
			name:             "BYOC host",
			deployType:       o11y.ByocHostType,
			deployTypeV2:     o11y.DeployTypeV2ByocResourcePool,
			internalReadURL:  "http://byoc-ds.example.com/internal/v2/metrics/ignored/api/v1/",
			wantBaseURL:      "http://byoc-ds.example.com",
			wantDeployTypeV2: o11y.DeployTypeV2ByocResourcePool,
		},
		{
			name:             "legacy BYOC host is normalized",
			deployType:       o11y.ByocHostType,
			internalReadURL:  "https://byoc-ds.example.com/internal/v2/metrics/ignored/api/v1/",
			wantBaseURL:      "https://byoc-ds.example.com",
			wantDeployTypeV2: o11y.DeployTypeV2ByocResourcePool,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxySlowQueryTargetSource(tt.deployType, tt.deployTypeV2)
			resolver := &fakeDataProxyInternalReadTargetResolver{target: dataProxySlowQueryResolvedTarget(tt.internalReadURL)}
			router := dataProxySlowQueryTargetTestRouter("prod", "byoc-ds.example.com", source, resolver, true)

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

			require.Equal(t, http.StatusOK, response.Code, response.Body.String())
			require.JSONEq(t, `{
				"baseUrl":"`+tt.wantBaseURL+`",
				"o11yClusterId":"`+testSlowQueryO11yClusterID+`",
				"headers":{
					"env":"prod",
					"bizType":"byoc",
					"orgId":"123",
					"regionId":"aws-us-west-2",
					"applicationId":"cluster-prod"
				},
				"provider":"aws",
				"region":"us-west-2",
				"deployType":"`+tt.deployType+`",
				"deployTypeV2":"`+tt.wantDeployTypeV2+`"
			}`, response.Body.String())
			require.Equal(t, 1, resolver.calls)
			require.Equal(t, uint(123), resolver.gotCluster.Organization.DbaasOrgID)
		})
	}
}

func TestDataProxySlowQueryTargetRejectsOtherTiersAndConflictingBYOCMetadata(t *testing.T) {
	tests := []struct {
		name         string
		deployType   string
		deployTypeV2 string
	}{
		{name: "dedicated", deployType: o11y.DedicatedType, deployTypeV2: o11y.DeployTypeV2Dedicated},
		{name: "premium", deployType: o11y.PremiumType, deployTypeV2: o11y.DeployTypeV2Premium},
		{name: "starter", deployType: o11y.SharedType, deployTypeV2: o11y.DeployTypeV2Starter},
		{name: "essential", deployType: o11y.SharedType, deployTypeV2: o11y.DeployTypeV2Essential},
		{name: "v2 only BYOC", deployTypeV2: o11y.DeployTypeV2Byoc},
		{name: "BYOC marked as resource pool", deployType: o11y.ByocType, deployTypeV2: o11y.DeployTypeV2ByocResourcePool},
		{name: "BYOC host marked as logical", deployType: o11y.ByocHostType, deployTypeV2: o11y.DeployTypeV2Byoc},
		{name: "premium conflicting with BYOC", deployType: o11y.PremiumType, deployTypeV2: o11y.DeployTypeV2Byoc},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxySlowQueryTargetSource(tt.deployType, tt.deployTypeV2)
			resolver := &fakeDataProxyInternalReadTargetResolver{}
			router := dataProxySlowQueryTargetTestRouter("prod", "byoc-ds.example.com", source, resolver, true)

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

			require.Equal(t, http.StatusBadRequest, response.Code)
			require.Contains(t, response.Body.String(), "unsupported for this cluster")
			require.Zero(t, resolver.calls, "unsupported tiers must not reach O11y resolution")
		})
	}
}

func TestDataProxySlowQueryTargetChecksOwnershipBeforeResolution(t *testing.T) {
	source := dataProxySlowQueryTargetSource(o11y.ByocType, o11y.DeployTypeV2Byoc)
	resolver := &fakeDataProxyInternalReadTargetResolver{}
	router := dataProxySlowQueryTargetTestRouter("prod", "byoc-ds.example.com", source, resolver, true)

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

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

func TestDataProxySlowQueryTargetRequiresInternalUser(t *testing.T) {
	source := dataProxySlowQueryTargetSource(o11y.ByocType, o11y.DeployTypeV2Byoc)
	resolver := &fakeDataProxyInternalReadTargetResolver{}
	router := dataProxySlowQueryTargetTestRouter("prod", "byoc-ds.example.com", source, resolver, false)

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

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

func TestDataProxySlowQueryTargetResolverFailuresAreBounded(t *testing.T) {
	tests := []struct {
		name       string
		resolver   dataProxyInternalReadTargetResolver
		wantStatus int
	}{
		{name: "resolver unavailable", resolver: &fakeDataProxyInternalReadTargetResolver{err: errClusterDataURLsResolverUnavailable}, wantStatus: http.StatusServiceUnavailable},
		{name: "resolver timeout", resolver: &fakeDataProxyInternalReadTargetResolver{err: context.DeadlineExceeded}, wantStatus: http.StatusGatewayTimeout},
		{name: "resolver internal error", resolver: &fakeDataProxyInternalReadTargetResolver{err: errors.New("secret resolver detail")}, wantStatus: http.StatusBadGateway},
		{name: "missing resolver", wantStatus: http.StatusServiceUnavailable},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxySlowQueryTargetSource(o11y.ByocType, o11y.DeployTypeV2Byoc)
			router := dataProxySlowQueryTargetTestRouter("prod", "byoc-ds.example.com", source, tt.resolver, true)

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

			require.Equal(t, tt.wantStatus, response.Code)
			require.NotContains(t, response.Body.String(), "secret resolver detail")
		})
	}
}

func TestDataProxySlowQueryTargetRejectsInvalidResolvedTargets(t *testing.T) {
	tests := []struct {
		name     string
		endpoint string
		target   *o11yurls.InternalReadTarget
	}{
		{name: "nil target", endpoint: "byoc-ds.example.com"},
		{name: "URL userinfo", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://user:secret@byoc-ds.example.com/internal/read")},
		{name: "URL query", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read?token=secret")},
		{name: "URL fragment", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read#secret")},
		{name: "URL endpoint mismatch", endpoint: "configured.example.com", target: dataProxySlowQueryResolvedTarget("https://other.example.com/internal/read")},
		{name: "missing configured endpoint", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read")},
		{name: "localhost", endpoint: "localhost", target: dataProxySlowQueryResolvedTarget("http://localhost/internal/read")},
		{name: "localhost trailing dot", endpoint: "localhost.", target: dataProxySlowQueryResolvedTarget("http://localhost./internal/read")},
		{name: "IPv4 loopback", endpoint: "127.0.0.1", target: dataProxySlowQueryResolvedTarget("http://127.0.0.1/internal/read")},
		{name: "invalid O11y ID", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read")},
		{name: "wrong tenant", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read")},
		{name: "wrong application", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read")},
		{name: "wrong placement", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read")},
		{name: "wrong biz type", endpoint: "byoc-ds.example.com", target: dataProxySlowQueryResolvedTarget("https://byoc-ds.example.com/internal/read")},
	}

	for i := range tests {
		tt := &tests[i]
		switch tt.name {
		case "invalid O11y ID":
			tt.target.O11yCluster.ID = "bad/id"
		case "wrong tenant":
			tt.target.O11yCluster.TenantID = "999"
		case "wrong application":
			tt.target.O11yCluster.ApplicationID = "other-cluster"
		case "wrong placement":
			tt.target.O11yCluster.Region = "us-east-1"
		case "wrong biz type":
			tt.target.O11yCluster.BizType = "tidbcloud"
		}
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			source := dataProxySlowQueryTargetSource(o11y.ByocType, o11y.DeployTypeV2Byoc)
			resolver := &fakeDataProxyInternalReadTargetResolver{target: tt.target}
			router := dataProxySlowQueryTargetTestRouter("prod", tt.endpoint, source, resolver, true)

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

			require.Equal(t, http.StatusBadGateway, response.Code, response.Body.String())
		})
	}
}

func TestNewDataProxySlowQueryTargetServiceUsesClinicConfig(t *testing.T) {
	service := NewDataProxySlowQueryTargetService(&config.Config{
		Env: "prod",
		CentralO11yConfig: &config.CentralO11yConfig{
			ByocDSEndpoint: " byoc-ds.example.com ",
		},
	}, nil, nil)

	require.Equal(t, "prod", service.env)
	require.Equal(t, "byoc-ds.example.com", service.byocDSEndpointHost)
}

func dataProxySlowQueryResolvedTarget(internalReadURL string) *o11yurls.InternalReadTarget {
	return &o11yurls.InternalReadTarget{
		InternalReadURL: internalReadURL,
		O11yCluster: &o11y.ClusterInfo{
			ID:            testSlowQueryO11yClusterID,
			TenantID:      "123",
			Vendor:        "aws",
			Region:        "us-west-2",
			ApplicationID: "cluster-prod",
			BizType:       o11y.ByocType,
		},
	}
}

func dataProxySlowQueryTargetSource(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 dataProxySlowQueryTargetTestRouter(
	env, byocDSEndpoint 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)
	registerDataProxySlowQueryTargetRoutes(
		router.Group("/clinic/api/v1/internal"),
		newDataProxySlowQueryTargetService(env, byocDSEndpoint, metadata, resolver),
	)
	return router
}
