package regionalservice

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

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	regionalpb "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	"github.com/tidbcloud/ticdc-service-ng/pkg/config"
	"github.com/tidbcloud/ticdc-service-ng/pkg/util"

	"github.com/tidbcloud/ticdc-service-ng/pkg/regionalservice/mocks"
)

const clusterID = "test-cluster"

func TestGetCluster(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.clusterServiceCli.SetTransport(roundTripper)

	ctx := context.Background()

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			assert.Equal(t, "GET", req.Method)
			assert.Equal(t, "/api/v1beta/sharedPools/"+clusterID, req.URL.Path)
			return &http.Response{
				Status:     "ok",
				StatusCode: 200,
				Body:       io.NopCloser(strings.NewReader("{\"tenantId\":\"testID\"}")),
				Request:    req,
			}, nil
		},
	).Times(1)

	_, err := serviceClient.GetSharedPoolByID(ctx, clusterID)
	assert.NoError(t, err)
}

func TestGetOneTiDBGroupByClusterID(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.clusterServiceCli.SetTransport(roundTripper)

	ctx := context.Background()

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			assert.Equal(t, "GET", req.Method)
			assert.Equal(t, "http://example.com/api/v1beta/clusters/test-cluster/tidbGroups",
				req.URL.String())
			return &http.Response{
				Status:     "ok",
				StatusCode: 200,
				Body: io.NopCloser(strings.NewReader("" +
					"{" +
					"\"tidbGroups\": [{\"gatewayBaseUrl\":\"testurl\"}]" +
					"}")),
				Request: req,
			}, nil
		},
	).Times(1)

	_, err := serviceClient.GetOneTiDBGroupByClusterID(ctx, clusterID)
	assert.NoError(t, err)
}

func TestCallAPIUnmarshalError(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.clusterServiceCli.SetTransport(roundTripper)

	ctx := context.Background()

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			return &http.Response{
				Status:     "OK",
				StatusCode: 200,
				Body:       io.NopCloser(strings.NewReader("invalid json")),
				Request:    req,
			}, nil
		},
	).Times(1)

	_, err := serviceClient.GetSharedPoolByID(ctx, clusterID)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "syntax error")
}

func TestCallAPINonSuccessStatus(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.clusterServiceCli.SetTransport(roundTripper)

	ctx := context.Background()

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			largeBody := strings.Repeat("x", util.MaxBodySize+128)
			return &http.Response{
				Status:     "Not Found",
				StatusCode: 404,
				Body:       io.NopCloser(strings.NewReader(largeBody)),
				Request:    req,
			}, nil
		},
	).Times(1)

	_, err := serviceClient.GetSharedPoolByID(ctx, clusterID)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "Not Found")
	assert.True(t, IsNotFound(err))

	var statusErr *util.StatusError
	ok := errors.As(err, &statusErr)
	assert.True(t, ok)
	assert.Equal(t, util.MaxBodySize, len(statusErr.Body))
}

func TestGetOneTiDBGroupByClusterIDEmptyResult(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.clusterServiceCli.SetTransport(roundTripper)

	ctx := context.Background()

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			assert.Equal(t, "GET", req.Method)
			assert.Equal(t, "http://example.com/api/v1beta/clusters/test-cluster/tidbGroups",
				req.URL.String())
			return &http.Response{
				Status:     "ok",
				StatusCode: 200,
				Body:       io.NopCloser(strings.NewReader("{\"tidbGroups\": []}")),
				Request:    req,
			}, nil
		},
	).Times(1)

	_, err := serviceClient.GetOneTiDBGroupByClusterID(ctx, clusterID)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "no TiDB group found for cluster")
	assert.True(t, IsNotFound(err))
}

func TestGetDataPlaneByID(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.dataPlaneServiceCli.SetTransport(roundTripper)

	ctx := context.Background()
	dataPlaneID := "test-dataplane"

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			assert.Equal(t, "GET", req.Method)
			assert.Equal(t, "http://example.com/api/v1/dataplanes/test-dataplane",
				req.URL.String())
			return &http.Response{
				Status:     "ok",
				StatusCode: 200,
				Body: io.NopCloser(strings.NewReader("" +
					"{" +
					"\"id\":\"test-dataplane\"," +
					"\"spec\":{\"regionId\":\"us-west-1\"}" +
					"}")),
				Request: req,
			}, nil
		},
	).Times(1)

	dataPlane, err := serviceClient.GetDataPlaneByID(ctx, dataPlaneID)
	assert.NoError(t, err)
	assert.Equal(t, "test-dataplane", dataPlane.Id)
}

func TestDescribePrivateLinkEndpointServiceZones(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	roundTripper := mocks.NewMockRoundTripper(ctrl)
	cfg := &config.Config{
		ClusterServiceBaseURL:          "http://example.com",
		DataPlaneManagerServiceBaseURL: "http://example.com",
	}
	serviceClient := NewRegionalServiceClient(cfg).(*client)
	serviceClient.dataPlaneServiceCli.SetTransport(roundTripper)

	ctx := context.Background()

	roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(
		func(req *http.Request) (*http.Response, error) {
			assert.Equal(t, "GET", req.Method)
			assert.Equal(t, "/api/v1/dataplanes/dp-1/privateLinkEndpointServices:describeZones", req.URL.Path)
			assert.Equal(t, "com.amazonaws.vpce.us-east-1.vpce-svc-123", req.URL.Query().Get("serviceName"))
			assert.Equal(t, "us-east-1", req.URL.Query().Get("serviceRegion"))
			return &http.Response{
				Status:     "ok",
				StatusCode: 200,
				Body: io.NopCloser(strings.NewReader("" +
					"{" +
					"\"zones\": [" +
					"{\"availabilityZone\":\"us-east-1a\",\"availabilityZoneId\":\"use1-az1\"}," +
					"{\"availabilityZone\":\"us-east-1c\",\"availabilityZoneId\":\"use1-az2\"}" +
					"]" +
					"}")),
				Request: req,
			}, nil
		},
	).Times(1)

	resp, err := serviceClient.DescribePrivateLinkEndpointServiceZones(ctx, &regionalpb.DescribePrivateLinkEndpointServiceZonesRequest{
		DataPlaneId:   "dp-1",
		ServiceName:   "com.amazonaws.vpce.us-east-1.vpce-svc-123",
		ServiceRegion: "us-east-1",
	})
	assert.NoError(t, err)
	assert.Equal(t, []string{"us-east-1a:use1-az1", "us-east-1c:use1-az2"}, endpointServiceZoneStrings(resp.Zones))
}

func endpointServiceZoneStrings(zones []*regionalpb.EndpointServiceAvailabilityZone) []string {
	ret := make([]string, 0, len(zones))
	for _, zone := range zones {
		ret = append(ret, zone.AvailabilityZone+":"+zone.AvailabilityZoneId)
	}
	return ret
}
