package jump

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/onsi/ginkgo/v2"
	"github.com/onsi/gomega"
	"go.uber.org/mock/gomock"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/clientcmd"

	itkent "github.com/tidbcloud/devops/entity/itoken"
	jmpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	itkapi "github.com/tidbcloud/devops/pkg/service/itoken/api"
	jmpsvcapi "github.com/tidbcloud/devops/pkg/service/jump/api"
	"github.com/tidbcloud/devops/pkg/service/jump/authactor"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod"
	"github.com/tidbcloud/devops/pkg/service/jump/repo"
	ngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/grpc"
)

func TestJumpService(t *testing.T) {
	gomega.RegisterFailHandler(ginkgo.Fail)
	ginkgo.RunSpecs(t, "Jump Service Suite", test.ReporterConfig())
}

var _ = ginkgo.Describe("[Jump Service]", func() {
	ginkgo.Describe("[validateParam]", func() {
		var (
			ctx                context.Context
			ctrl               *gomock.Controller
			clusterClient      *coresvccli.MockClusterServiceClient
			resourcePoolClient *coresvccli.MockResourcePoolServiceClient
			s                  *svc
		)

		newParam := func(dest *jmpent.DestinationUnion) jmpsvcapi.CreateAuthorizationParam {
			return jmpsvcapi.CreateAuthorizationParam{
				Expiration:   jmpent.JumpExpiration1Hour,
				Destinations: []*jmpent.DestinationUnion{dest},
			}
		}

		ginkgo.BeforeEach(func() {
			ctx = context.Background()
			ctrl = gomock.NewController(ginkgo.GinkgoT())
			clusterClient = coresvccli.NewMockClusterServiceClient(ctrl)
			resourcePoolClient = coresvccli.NewMockResourcePoolServiceClient(ctrl)
			s = &svc{
				coresvcCli: &coresvccli.CoreSvcCli{
					Mgmt: &coresvccli.MgmtService{
						Cluster:      clusterClient,
						ResourcePool: resourcePoolClient,
					},
				},
			}
		})

		ginkgo.AfterEach(func() {
			ctrl.Finish()
		})

		ginkgo.It("validates premium cluster through core service", func() {
			clusterClient.EXPECT().
				ListClusters(gomock.Any(), gomock.Any()).
				DoAndReturn(func(_ context.Context, req *mgmtv1.ListClustersRequest, _ ...grpc.CallOption) (*mgmtv1.ListClustersResponse, error) {
					gomega.Expect(req.Filter.ClusterIds).To(gomega.Equal([]uint64{123}))
					return &mgmtv1.ListClustersResponse{
						Clusters: []*mgmtv1.Cluster{
							{
								ClusterInfo: &mgmtv1.Cluster_NextGenCluster{
									NextGenCluster: &ngv1.Cluster{},
								},
							},
						},
					}, nil
				})

			err := s.validateParam(ctx, newParam(&jmpent.DestinationUnion{
				PremiumCluster: &jmpent.DestinationPremiumCluster{ClusterID: 123},
			}))
			gomega.Expect(err).To(gomega.BeNil())
		})

		ginkgo.It("returns premium cluster lookup error", func() {
			clusterClient.EXPECT().
				ListClusters(gomock.Any(), gomock.Any()).
				Return(nil, fmt.Errorf("boom"))

			err := s.validateParam(ctx, newParam(&jmpent.DestinationUnion{
				PremiumCluster: &jmpent.DestinationPremiumCluster{ClusterID: 123},
			}))
			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("fail to get premium cluster '123' info: boom")))
		})

		ginkgo.It("rejects non next-gen premium cluster", func() {
			clusterClient.EXPECT().
				ListClusters(gomock.Any(), gomock.Any()).
				Return(&mgmtv1.ListClustersResponse{Clusters: []*mgmtv1.Cluster{{}}}, nil)

			err := s.validateParam(ctx, newParam(&jmpent.DestinationUnion{
				PremiumCluster: &jmpent.DestinationPremiumCluster{ClusterID: 123},
			}))
			gomega.Expect(err).To(gomega.MatchError("the cluster '123' is not a premium cluster"))
		})

		ginkgo.It("validates premium resource pool through core service", func() {
			resourcePoolClient.EXPECT().
				GetResourcePool(gomock.Any(), gomock.Any()).
				DoAndReturn(func(_ context.Context, req *ngv1.GetResourcePoolRequest, _ ...grpc.CallOption) (*ngv1.ResourcePoolProfile, error) {
					gomega.Expect(req.ResourcePoolId).To(gomega.Equal(uint64(456)))
					return &ngv1.ResourcePoolProfile{
						ResourcePool: &ngv1.ResourcePool{Id: 456},
					}, nil
				})

			err := s.validateParam(ctx, newParam(&jmpent.DestinationUnion{
				PremiumResourcePool: &jmpent.DestinationPremiumResourcePool{ResourcePoolID: 456},
			}))
			gomega.Expect(err).To(gomega.BeNil())
		})

		ginkgo.It("returns premium resource pool lookup error", func() {
			resourcePoolClient.EXPECT().
				GetResourcePool(gomock.Any(), gomock.Any()).
				Return(nil, fmt.Errorf("boom"))

			err := s.validateParam(ctx, newParam(&jmpent.DestinationUnion{
				PremiumResourcePool: &jmpent.DestinationPremiumResourcePool{ResourcePoolID: 456},
			}))
			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("fail to get premium resource pool '456' info: boom")))
		})
	})

	ginkgo.Describe("[GetConnection]", func() {
		var (
			ctx                context.Context
			ctrl               *gomock.Controller
			itkMock            *itkapi.MockInternalTokenServiceAPI
			jumpRepoMock       *repo.MockJumpRepo
			jumpPodManagerMock *jumppod.MockJumpPodManager
			jumpPodMock        *jumppod.MockJumpPod
			auth               *jmpent.Authorization
			s                  *svc
		)

		newJump := func(typ jmpent.DestinationType, id string, method jmpent.JumpMethod) *jmpent.Jump {
			var dest *jmpent.DestinationUnion
			switch typ {
			case jmpent.DestinationTypeDedicated:
				dest = &jmpent.DestinationUnion{
					DedicatedCluster: &jmpent.DestinationDedicatedCluster{ClusterID: 123},
				}
			case jmpent.DestinationTypeControlPlane:
				dest = &jmpent.DestinationUnion{
					ControlPlane: &jmpent.DestinationControlPlane{Name: id},
				}
			default:
				dest = &jmpent.DestinationUnion{
					ControlPlane: &jmpent.DestinationControlPlane{Name: id},
				}
			}

			return &jmpent.Jump{
				DestinationType: typ,
				Destination:     dest,
				Method:          method,
			}
		}

		ginkgo.BeforeEach(func() {
			ctx = context.Background()
			ctrl = gomock.NewController(ginkgo.GinkgoT())
			itkMock = itkapi.NewMockInternalTokenServiceAPI(ctrl)
			jumpRepoMock = repo.NewMockJumpRepo(ctrl)
			jumpPodManagerMock = jumppod.NewMockJumpPodManager(ctrl)
			jumpPodMock = jumppod.NewMockJumpPod(ctrl)

			expireAt := time.Now().Add(30 * time.Minute).Unix()
			auth = &jmpent.Authorization{
				ID:                  9527,
				Applicant:           "alice@pingcap.com",
				Phase:               jmpent.AuthorizationPhaseCreated,
				ExpireAt:            &expireAt,
				Privilege:           jmpent.JumpPrivilegeReadOnly,
				EnableGatewayAccess: true,
				Jumps: []*jmpent.Jump{
					newJump(jmpent.DestinationTypeDedicated, "123", jmpent.JumpMethodK8s),
					newJump(jmpent.DestinationTypeControlPlane, "infra-apiserver", jmpent.JumpMethodK8s),
					newJump(jmpent.DestinationTypeControlPlane, "ignored", jmpent.JumpMethodTailscale),
				},
			}

			jumpRepoMock.EXPECT().
				GetAuthorization(gomock.Any(), uint64(9527)).
				DoAndReturn(func(_ context.Context, _ uint64) (*jmpent.Authorization, error) {
					return auth, nil
				}).
				AnyTimes()

			jumpPodManagerMock.EXPECT().
				GetJumpPod(gomock.Any(), authactor.EmailToJumpPodName("alice@pingcap.com")).
				Return(jumpPodMock, nil).
				AnyTimes()

			jumpPodMock.EXPECT().Pod().Return(&corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "jump-pod-alice",
					Namespace: "jump-service",
				},
			}).AnyTimes()

			s = &svc{
				conf: config.JumpServiceConfig{
					Gateway: config.JumpServiceGatewayConfig{
						AccessURL: "https://jump-gateway.internal",
						Audience:  "jump-gateway",
					},
				},
				jmpRepo:        jumpRepoMock,
				jumpPodManager: jumpPodManagerMock,
				itkSvc:         itkMock,
			}
		})

		ginkgo.AfterEach(func() {
			ctrl.Finish()
		})

		ginkgo.It("returns gateway connection with server urls and kubeconfig", func() {
			itkMock.EXPECT().
				IssueJWTToken(gomock.Any(), gomock.Any()).
				DoAndReturn(func(_ context.Context, p itkapi.IssueJWTTokenParam) (*itkent.IssuedJWTToken, error) {
					gomega.Expect(p.Subject).To(gomega.Equal("alice@pingcap.com"))
					gomega.Expect(p.Audience).To(gomega.Equal("jump-gateway"))
					gomega.Expect(p.ExpiresAt).ToNot(gomega.BeNil())
					gomega.Expect(p.CustomClaims["auth_id"]).To(gomega.BeNumerically("==", 9527))
					gomega.Expect(p.CustomClaims["user_email"]).To(gomega.Equal("alice@pingcap.com"))
					gomega.Expect(p.CustomClaims["jump_pod_namespace"]).To(gomega.Equal("jump-service"))
					gomega.Expect(p.CustomClaims["jump_pod_name"]).To(gomega.Equal("jump-pod-alice"))
					dests, ok := p.CustomClaims["destinations"].([]interface{})
					gomega.Expect(ok).To(gomega.BeTrue())
					gomega.Expect(dests).To(gomega.HaveLen(2))
					gomega.Expect(dests).To(gomega.ContainElements(
						gomega.HaveKeyWithValue("dedicated_cluster", gomega.HaveKeyWithValue("cluster_id", "123")),
						gomega.HaveKeyWithValue("control_plane", gomega.HaveKeyWithValue("name", "infra-apiserver")),
					))
					return &itkent.IssuedJWTToken{Token: "jwt-token"}, nil
				})

			conn, err := s.GetConnection(ctx, 9527, "alice@pingcap.com", jmpent.ConnectionMethodGateway)
			gomega.Expect(err).To(gomega.BeNil())
			gomega.Expect(conn).ToNot(gomega.BeNil())
			gomega.Expect(conn.Gateway).ToNot(gomega.BeNil())
			gomega.Expect(conn.Gateway.ConnectionToken).To(gomega.Equal("jwt-token"))
			gomega.Expect(conn.Gateway.ServerURLs).To(gomega.Equal(map[string]string{
				"gateway_dedicated.123":                "https://jump-gateway.internal/k8s/clusters/Z2F0ZXdheV9kZWRpY2F0ZWQuMTIz",
				"gateway_controlplane.infra-apiserver": "https://jump-gateway.internal/k8s/clusters/Z2F0ZXdheV9jb250cm9scGxhbmUuaW5mcmEtYXBpc2VydmVy",
			}))

			cfg, err := clientcmd.Load([]byte(conn.Gateway.Kubeconfig))
			gomega.Expect(err).To(gomega.BeNil())
			gomega.Expect(cfg.AuthInfos).To(gomega.HaveKey("alice@pingcap.com"))
			gomega.Expect(cfg.AuthInfos["alice@pingcap.com"].Token).To(gomega.Equal("jwt-token"))
			gomega.Expect(cfg.Contexts).To(gomega.HaveKey("gateway_dedicated.123"))
			gomega.Expect(cfg.Contexts).To(gomega.HaveKey("gateway_controlplane.infra-apiserver"))
		})

		ginkgo.It("returns error when authorization is not ready", func() {
			auth.Phase = jmpent.AuthorizationPhaseCreating
			_, err := s.GetConnection(ctx, 9527, "alice@pingcap.com", jmpent.ConnectionMethodGateway)
			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("is not ready for connection")))
		})

		ginkgo.It("returns error when authorization expiration is missing", func() {
			auth.ExpireAt = nil
			_, err := s.GetConnection(ctx, 9527, "alice@pingcap.com", jmpent.ConnectionMethodGateway)
			gomega.Expect(err).To(gomega.MatchError("authorization expire_at is required for connection"))
		})

		ginkgo.It("returns error when authorization is expired", func() {
			expired := time.Now().Add(-time.Minute).Unix()
			auth.ExpireAt = &expired
			_, err := s.GetConnection(ctx, 9527, "alice@pingcap.com", jmpent.ConnectionMethodGateway)
			gomega.Expect(err).To(gomega.MatchError("authorization has expired"))
		})

		ginkgo.It("returns token issue error", func() {
			itkMock.EXPECT().
				IssueJWTToken(gomock.Any(), gomock.Any()).
				Return(nil, fmt.Errorf("boom"))
			_, err := s.GetConnection(ctx, 9527, "alice@pingcap.com", jmpent.ConnectionMethodGateway)
			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("generate gateway connection token failed: boom")))
		})

		ginkgo.It("returns error when no kubernetes destination exists", func() {
			auth.Jumps = []*jmpent.Jump{
				newJump(jmpent.DestinationTypeControlPlane, "cp", jmpent.JumpMethodTailscale),
			}
			_, err := s.GetConnection(ctx, 9527, "alice@pingcap.com", jmpent.ConnectionMethodGateway)
			gomega.Expect(err).To(gomega.MatchError("no kubernetes destinations available for gateway connection"))
		})
	})

	ginkgo.Describe("[genJumpParam]", func() {
		ginkgo.It("propagates enable gateway access as true", func() {
			auth := &jmpent.Authorization{
				Privilege:           jmpent.JumpPrivilegeReadOnly,
				Expiration:          jmpent.JumpExpiration1Hour,
				EnableGatewayAccess: true,
			}

			before := time.Now()
			param, err := authactor.GenJumpParam(auth)
			gomega.Expect(err).To(gomega.BeNil())
			gomega.Expect(param.EnableGatewayAccess).To(gomega.BeTrue())
			gomega.Expect(param.Privilege).To(gomega.Equal(jmpent.JumpPrivilegeReadOnly))
			gomega.Expect(param.ExpireAt.After(before)).To(gomega.BeTrue())
		})

		ginkgo.It("propagates enable gateway access as false", func() {
			auth := &jmpent.Authorization{
				Privilege:           jmpent.JumpPrivilegeReadWrite,
				Expiration:          jmpent.JumpExpiration3Hour,
				EnableGatewayAccess: false,
			}

			before := time.Now()
			param, err := authactor.GenJumpParam(auth)
			gomega.Expect(err).To(gomega.BeNil())
			gomega.Expect(param.EnableGatewayAccess).To(gomega.BeFalse())
			gomega.Expect(param.Privilege).To(gomega.Equal(jmpent.JumpPrivilegeReadWrite))
			gomega.Expect(param.ExpireAt.After(before)).To(gomega.BeTrue())
		})
	})
})
