package cloudmap

import (
	"context"
	"errors"
	"fmt"
	"testing"

	aboutv1alpha1 "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/apis/about/v1alpha1"

	cloudmapMock "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/mocks/pkg/cloudmap"
	"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/common"
	"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/model"
	"github.com/aws/aws-cloud-map-mcs-controller-for-k8s/test"
	"github.com/aws/aws-sdk-go-v2/aws"
	sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
	"github.com/aws/aws-sdk-go-v2/service/servicediscovery/types"
	"github.com/go-logr/logr/testr"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"k8s.io/apimachinery/pkg/runtime"
)

func TestNewServiceDiscoveryApi(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)

	sdApi := getServiceDiscoveryApi(t, awsFacade)
	assert.NotNil(t, sdApi)
}

func TestServiceDiscoveryApi_GetNamespaceMap_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	id, name := test.DnsNsId, test.DnsNsName
	ns := types.NamespaceSummary{
		Name: &name,
		Id:   &id,
		Type: types.NamespaceTypeDnsPrivate,
	}
	awsFacade.EXPECT().ListNamespaces(context.TODO(), &sd.ListNamespacesInput{}).
		Return(&sd.ListNamespacesOutput{Namespaces: []types.NamespaceSummary{ns}}, nil)

	namespaces, err := sdApi.GetNamespaceMap(context.TODO())
	assert.Nil(t, err, "No error for happy case")
	assert.True(t, len(namespaces) == 1)
	assert.Equal(t, test.GetTestDnsNamespace(), namespaces[test.DnsNsName])
}

func TestServiceDiscoveryApi_GetNamespaceMap_SkipPublicDNSNotSupported(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	id, name := test.DnsNsId, test.DnsNsName
	ns := types.NamespaceSummary{
		Name: &name,
		Id:   &id,
		Type: types.NamespaceTypeDnsPublic,
	}
	awsFacade.EXPECT().ListNamespaces(context.TODO(), &sd.ListNamespacesInput{}).
		Return(&sd.ListNamespacesOutput{Namespaces: []types.NamespaceSummary{ns}}, nil)

	namespaces, err := sdApi.GetNamespaceMap(context.TODO())
	assert.Nil(t, err, "No error for happy case")
	assert.Empty(t, namespaces, "Successfully skipped DNS_PUBLIC from the output")
}

func TestServiceDiscoveryApi_GetServiceIdMap_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	filter := types.ServiceFilter{
		Name:   types.ServiceFilterNameNamespaceId,
		Values: []string{test.HttpNsId},
	}

	awsFacade.EXPECT().ListServices(context.TODO(), &sd.ListServicesInput{Filters: []types.ServiceFilter{filter}}).
		Return(&sd.ListServicesOutput{Services: []types.ServiceSummary{
			{Id: aws.String(test.SvcId), Name: aws.String(test.SvcName)},
		}}, nil)

	svcs, err := sdApi.GetServiceIdMap(context.TODO(), test.HttpNsId)
	assert.Nil(t, err, "No error for happy case")
	assert.True(t, len(svcs) == 1)
	assert.Equal(t, svcs[test.SvcName], test.SvcId)
}

func TestServiceDiscoveryApi_DiscoverInstances_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	awsFacade.EXPECT().DiscoverInstances(context.TODO(),
		&sd.DiscoverInstancesInput{
			NamespaceName: aws.String(test.HttpNsName),
			ServiceName:   aws.String(test.SvcName),
			HealthStatus:  types.HealthStatusFilterAll,
			MaxResults:    aws.Int32(1000),
			QueryParameters: map[string]string{
				model.ClusterSetIdAttr: test.ClusterSet,
			},
		}).
		Return(&sd.DiscoverInstancesOutput{
			Instances: []types.HttpInstanceSummary{
				{InstanceId: aws.String(test.EndptId1)},
				{InstanceId: aws.String(test.EndptId2)},
			},
		}, nil)

	insts, err := sdApi.DiscoverInstances(context.TODO(), test.HttpNsName, test.SvcName, map[string]string{model.ClusterSetIdAttr: test.ClusterSet})
	assert.Nil(t, err, "No error for happy case")
	assert.True(t, len(insts) == 2)
	assert.Equal(t, test.EndptId1, *insts[0].InstanceId)
	assert.Equal(t, test.EndptId2, *insts[1].InstanceId)
}

func TestServiceDiscoveryApi_GetOperation_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	expectedOp := &types.Operation{Id: aws.String(test.OpId1), Status: types.OperationStatusPending}
	awsFacade.EXPECT().GetOperation(context.TODO(), &sd.GetOperationInput{OperationId: aws.String(test.OpId1)}).
		Return(&sd.GetOperationOutput{Operation: expectedOp}, nil)

	op, err := sdApi.GetOperation(context.TODO(), test.OpId1)
	assert.Nil(t, err, "No error for happy case")
	assert.Equal(t, expectedOp, op)
}

func TestServiceDiscoveryApi_CreateHttNamespace_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	awsFacade.EXPECT().CreateHttpNamespace(context.TODO(), &sd.CreateHttpNamespaceInput{Name: aws.String(test.HttpNsName)}).
		Return(&sd.CreateHttpNamespaceOutput{OperationId: aws.String(test.OpId1)}, nil)

	opId, err := sdApi.CreateHttpNamespace(context.TODO(), test.HttpNsName)
	assert.Nil(t, err, "No error for happy case")
	assert.Equal(t, test.OpId1, opId)
}

func TestServiceDiscoveryApi_CreateService_CreateForHttpNamespace(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	nsId, svcId, svcName := test.HttpNsId, test.SvcId, test.SvcName
	awsFacade.EXPECT().CreateService(context.TODO(), &sd.CreateServiceInput{
		Name:        &svcName,
		NamespaceId: &nsId,
	}).
		Return(&sd.CreateServiceOutput{
			Service: &types.Service{
				Id: &svcId,
			},
		}, nil)

	retSvcId, err := sdApi.CreateService(context.TODO(), *test.GetTestHttpNamespace(), svcName)
	assert.Nil(t, err)
	assert.Equal(t, svcId, retSvcId, "Successfully created service")
}

func TestServiceDiscoveryApi_CreateService_CreateForDnsNamespace(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	nsId, svcId, svcName := test.DnsNsId, test.SvcId, test.SvcName
	awsFacade.EXPECT().CreateService(context.TODO(), &sd.CreateServiceInput{
		Name:        &svcName,
		NamespaceId: &nsId,
		DnsConfig: &types.DnsConfig{
			DnsRecords: []types.DnsRecord{{
				TTL:  aws.Int64(60),
				Type: "SRV",
			}},
		},
	}).
		Return(&sd.CreateServiceOutput{
			Service: &types.Service{
				Id: &svcId,
			},
		}, nil)

	retSvcId, err := sdApi.CreateService(context.TODO(), *test.GetTestDnsNamespace(), svcName)
	assert.Nil(t, err)
	assert.Equal(t, svcId, retSvcId, "Successfully created service")
}

func TestServiceDiscoveryApi_CreateService_ThrowError(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	sdApi := getServiceDiscoveryApi(t, awsFacade)

	nsId, svcName := test.HttpNsId, test.SvcName
	awsFacade.EXPECT().CreateService(context.TODO(), &sd.CreateServiceInput{
		Name:        &svcName,
		NamespaceId: &nsId,
	}).
		Return(nil, fmt.Errorf("dummy error"))

	retSvcId, err := sdApi.CreateService(context.TODO(), *test.GetTestHttpNamespace(), svcName)
	assert.Empty(t, retSvcId)
	assert.Equal(t, "dummy error", err.Error(), "Got error")
}

func TestServiceDiscoveryApi_RegisterInstance_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	attrs := map[string]string{"a": "b"}

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	awsFacade.EXPECT().RegisterInstance(context.TODO(),
		&sd.RegisterInstanceInput{
			ServiceId:  aws.String(test.SvcId),
			InstanceId: aws.String(test.EndptId1),
			Attributes: attrs}).
		Return(&sd.RegisterInstanceOutput{OperationId: aws.String(test.OpId1)}, nil)

	sdApi := getServiceDiscoveryApi(t, awsFacade)
	opId, err := sdApi.RegisterInstance(context.TODO(), test.SvcId, test.EndptId1, attrs)
	assert.Nil(t, err)
	assert.Equal(t, test.OpId1, opId)
}

func TestServiceDiscoveryApi_RegisterInstance_Error(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	sdkErr := errors.New("fail")
	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	awsFacade.EXPECT().RegisterInstance(context.TODO(), gomock.Any()).Return(nil, sdkErr)

	sdApi := getServiceDiscoveryApi(t, awsFacade)
	_, err := sdApi.RegisterInstance(context.TODO(), test.SvcId, test.EndptId1, map[string]string{})
	assert.Equal(t, sdkErr, err)
}

func TestServiceDiscoveryApi_DeregisterInstance_HappyCase(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	awsFacade.EXPECT().DeregisterInstance(context.TODO(),
		&sd.DeregisterInstanceInput{
			ServiceId:  aws.String(test.SvcId),
			InstanceId: aws.String(test.EndptId1)}).
		Return(&sd.DeregisterInstanceOutput{OperationId: aws.String(test.OpId1)}, nil)

	sdApi := getServiceDiscoveryApi(t, awsFacade)
	opId, err := sdApi.DeregisterInstance(context.TODO(), test.SvcId, test.EndptId1)
	assert.Nil(t, err)
	assert.Equal(t, test.OpId1, opId)
}

func TestServiceDiscoveryApi_DeregisterInstance_Error(t *testing.T) {
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	sdkErr := errors.New("fail")
	awsFacade := cloudmapMock.NewMockAwsFacade(mockController)
	awsFacade.EXPECT().DeregisterInstance(context.TODO(), gomock.Any()).Return(nil, sdkErr)

	sdApi := getServiceDiscoveryApi(t, awsFacade)
	_, err := sdApi.DeregisterInstance(context.TODO(), test.SvcId, test.EndptId1)
	assert.Equal(t, sdkErr, err)
}

func getServiceDiscoveryApi(t *testing.T, awsFacade *cloudmapMock.MockAwsFacade) ServiceDiscoveryApi {
	scheme := runtime.NewScheme()
	scheme.AddKnownTypes(aboutv1alpha1.GroupVersion, &aboutv1alpha1.ClusterProperty{})
	return &serviceDiscoveryApi{
		log:         common.NewLoggerWithLogr(testr.New(t)),
		awsFacade:   awsFacade,
		rateLimiter: common.NewDefaultRateLimiter(),
	}
}
