package proxy_test

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

	"github.com/gin-gonic/gin"
	"github.com/jarcoal/httpmock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/proxy"
	"google.golang.org/protobuf/encoding/protojson"
	"sigs.k8s.io/yaml"

	clusterctlpb "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	regionalpb "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/test/kubemock"
	"github.com/tidbcloud/infra-provider/regional-server/test/testdata"
	"go.uber.org/mock/gomock"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestClusterHijackProxyGet(t *testing.T) {
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()
	defaultAppRoute := new(v1alpha1.APPRoute)
	{
		bs, err := testdata.LoadDefaultAPPRouteYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAppRoute))
	}
	ctrl := gomock.NewController(t)
	kubeClient := kubemock.NewMockClient(ctrl)
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	rp, err := proxy.NewReverseProxy("http://cluster-service.mock")
	require.NoError(t, err)
	rp = rp.WithPathPerfixRewrite("/api/dedicated/v1", "/api/v1")
	hp, err := proxy.NewClusterServiceHijackProxy(rp, kubeClient, nil, nil)
	require.NoError(t, err)
	group := eng.Group("/api/dedicated/v1")
	group.GET("/clusters/:clusterId", hp.GetCluster)
	clusterNotFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		clusterId := path.Base(r.URL.Path)
		t.Logf("not found cluster for: %+v\n", clusterId)
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}
	clusterSyncedHandlerFunc := func(r *http.Request) (*http.Response, error) {
		clusterId := path.Base(r.URL.Path)
		cluster := new(clusterctlpb.Cluster)
		cluster.Id = clusterId
		cluster.ClusterPhase = clusterctlpb.Cluster_SYNCED
		b, err := protojson.Marshal(cluster)
		if err != nil {
			return httpmock.NewJsonResponse(http.StatusInternalServerError, struct{}{})
		}
		return httpmock.NewBytesResponse(http.StatusOK, b), nil
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		t.Logf("not found handler for url: %+v\n", r.URL.String())
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}
	httpmock.RegisterNoResponder(notFoundHandlerFunc)
	{
		// if approtue is not found, proxy to cluster service get cluster api
		httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters/", clusterNotFoundHandlerFunc)
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				list.Items = []v1alpha1.APPRoute{}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters/foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusNotFound, w.Result().StatusCode)
	}
	{
		// if approtue is createing but not active, return the hijacked content
		httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters/", clusterNotFoundHandlerFunc)
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				item := defaultAppRoute.DeepCopy()
				item.Status.Phase = v1alpha1.APPRoutePhaseCreating
				list.Items = []v1alpha1.APPRoute{*item}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters/foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		cluster := new(regionalpb.Cluster)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), cluster))
		require.Equal(t, "foo", cluster.Id)
		require.Equal(t, "SYNCING", cluster.ClusterPhase.String())
	}
	{
		// if approtue is deleting, proxy to cluster service get cluster api
		httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters/", clusterSyncedHandlerFunc)
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				item := defaultAppRoute.DeepCopy()
				item.Status.Phase = v1alpha1.APPRoutePhaseDeleting
				list.Items = []v1alpha1.APPRoute{*item}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters/foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		cluster := new(regionalpb.Cluster)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), cluster))
		require.Equal(t, "foo", cluster.Id)
		require.Equal(t, "SYNCED", cluster.ClusterPhase.String())
	}
	{
		// if approtue is active, proxy to cluster service get cluster api
		httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters/", clusterSyncedHandlerFunc)
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				item := defaultAppRoute.DeepCopy()
				item.Status.Phase = v1alpha1.APPRoutePhaseActive
				list.Items = []v1alpha1.APPRoute{*item}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters/foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		cluster := new(regionalpb.Cluster)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), cluster))
		require.Equal(t, "foo", cluster.Id)
		require.Equal(t, "SYNCED", cluster.ClusterPhase.String())
	}
}

func TestClusterHijackProxyList(t *testing.T) {
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()
	defaultAppRoute := new(v1alpha1.APPRoute)
	{
		bs, err := testdata.LoadDefaultAPPRouteYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAppRoute))
	}
	ctrl := gomock.NewController(t)
	kubeClient := kubemock.NewMockClient(ctrl)
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	rp, err := proxy.NewReverseProxy("http://cluster-service.mock")
	require.NoError(t, err)
	rp = rp.WithPathPerfixRewrite("/api/dedicated/v1", "/api/v1")
	hp, err := proxy.NewClusterServiceHijackProxy(rp, kubeClient, nil, nil)
	require.NoError(t, err)
	group := eng.Group("/api/dedicated/v1")
	group.GET("/clusters", hp.ListClusters)
	emptyListHandlerFunc := func(r *http.Request) (*http.Response, error) {
		resp := new(clusterctlpb.ListClustersResponse)
		resp.Clusters = make([]*clusterctlpb.Cluster, 0)
		b, err := protojson.Marshal(resp)
		if err != nil {
			return httpmock.NewJsonResponse(http.StatusInternalServerError, struct{}{})
		}
		return httpmock.NewBytesResponse(http.StatusOK, b), nil
	}
	twoClustersHandlerFunc := func(r *http.Request) (*http.Response, error) {
		resp := new(clusterctlpb.ListClustersResponse)
		resp.Clusters = make([]*clusterctlpb.Cluster, 0)
		resp.Clusters = append(resp.Clusters, &clusterctlpb.Cluster{
			Id:           "foo-1",
			TenantId:     "foo",
			ProjectId:    "foo",
			ClusterPhase: clusterctlpb.Cluster_SYNCED,
		})
		resp.Clusters = append(resp.Clusters, &clusterctlpb.Cluster{
			Id:           "foo-2",
			TenantId:     "foo",
			ProjectId:    "foo",
			ClusterPhase: clusterctlpb.Cluster_SYNCED,
		})
		b, err := protojson.Marshal(resp)
		if err != nil {
			return httpmock.NewJsonResponse(http.StatusInternalServerError, struct{}{})
		}
		return httpmock.NewBytesResponse(http.StatusOK, b), nil
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		t.Logf("not found handler for url: %+v\n", r.URL.String())
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}
	httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters", emptyListHandlerFunc)
	httpmock.RegisterNoResponder(notFoundHandlerFunc)
	{
		// if approtue is not found, proxy to cluster service list cluster api
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				list.Items = []v1alpha1.APPRoute{}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters?tenant=foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := new(regionalpb.ListClustersResponse)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), out))
		require.Len(t, out.Clusters, 0)
	}
	{
		// if approtue is not active, rewrite the response
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				item := defaultAppRoute.DeepCopy()
				item.Status.Phase = v1alpha1.APPRoutePhaseCreating
				list.Items = []v1alpha1.APPRoute{*item}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters?tenant=foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := new(regionalpb.ListClustersResponse)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), out))
		require.Len(t, out.Clusters, 1)
		require.Equal(t, "SYNCING", out.Clusters[0].ClusterPhase.String())
	}
	{
		// if approtue is deleting, proxy to cluster service list cluster api
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				item := defaultAppRoute.DeepCopy()
				item.Status.Phase = v1alpha1.APPRoutePhaseDeleting
				list.Items = []v1alpha1.APPRoute{*item}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters?tenant=foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := new(regionalpb.ListClustersResponse)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), out))
		require.Len(t, out.Clusters, 0)
	}
	{
		// if approtue is active, proxy to cluster service list cluster api
		httpmock.RegisterResponder(http.MethodGet, "=~^/api/v1/clusters", twoClustersHandlerFunc)
		w := createTestResponseRecorder()
		ctx := gin.CreateTestContextOnly(w, eng)
		kubeClient.EXPECT().
			List(gomock.Any(),
				gomock.AssignableToTypeOf(&v1alpha1.APPRouteList{}),
				gomock.Any()).
			DoAndReturn(func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				item := defaultAppRoute.DeepCopy()
				item.Status.Phase = v1alpha1.APPRoutePhaseActive
				list.Items = []v1alpha1.APPRoute{*item}
				return nil
			})
		ctx.Request, _ = http.NewRequest(http.MethodGet, "/api/dedicated/v1/clusters?tenant=foo", nil)
		eng.HandleContext(ctx)
		require.Equal(t, http.StatusOK, w.Result().StatusCode)
		out := new(regionalpb.ListClustersResponse)
		require.NoError(t, protojson.Unmarshal(w.Body.Bytes(), out))
		require.Len(t, out.Clusters, 2)
		require.Equal(t, "SYNCED", out.Clusters[0].ClusterPhase.String())
		require.Equal(t, "SYNCED", out.Clusters[1].ClusterPhase.String())
	}
}
