/*
Copyright 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awssd

import (
	"context"
	"errors"
	"math/rand"
	"reflect"
	"strconv"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/request"
	sd "github.com/aws/aws-sdk-go/service/servicediscovery"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sigs.k8s.io/external-dns/endpoint"
	"sigs.k8s.io/external-dns/internal/testutils"
	"sigs.k8s.io/external-dns/plan"
)

// Compile time check for interface conformance
var _ AWSSDClient = &AWSSDClientStub{}

var (
	ErrNamespaceNotFound = errors.New("Namespace not found")
)

type AWSSDClientStub struct {
	// map[namespace_id]namespace
	namespaces map[string]*sd.Namespace

	// map[namespace_id] => map[service_id]instance
	services map[string]map[string]*sd.Service

	// map[service_id] => map[inst_id]instance
	instances map[string]map[string]*sd.Instance
}

func (s *AWSSDClientStub) CreateService(input *sd.CreateServiceInput) (*sd.CreateServiceOutput, error) {
	srv := &sd.Service{
		Id:               aws.String(strconv.Itoa(rand.Intn(10000))),
		DnsConfig:        input.DnsConfig,
		Name:             input.Name,
		Description:      input.Description,
		CreateDate:       aws.Time(time.Now()),
		CreatorRequestId: input.CreatorRequestId,
	}

	nsServices, ok := s.services[*input.NamespaceId]
	if !ok {
		nsServices = make(map[string]*sd.Service)
		s.services[*input.NamespaceId] = nsServices
	}
	nsServices[*srv.Id] = srv

	return &sd.CreateServiceOutput{
		Service: srv,
	}, nil
}

func (s *AWSSDClientStub) DeregisterInstance(input *sd.DeregisterInstanceInput) (*sd.DeregisterInstanceOutput, error) {
	serviceInstances := s.instances[*input.ServiceId]
	delete(serviceInstances, *input.InstanceId)

	return &sd.DeregisterInstanceOutput{}, nil
}

func (s *AWSSDClientStub) GetService(input *sd.GetServiceInput) (*sd.GetServiceOutput, error) {
	for _, entry := range s.services {
		srv, ok := entry[*input.Id]
		if ok {
			return &sd.GetServiceOutput{
				Service: srv,
			}, nil
		}
	}

	return nil, errors.New("service not found")
}

func (s *AWSSDClientStub) DiscoverInstancesWithContext(ctx context.Context, input *sd.DiscoverInstancesInput, opts ...request.Option) (*sd.DiscoverInstancesOutput, error) {
	instances := make([]*sd.HttpInstanceSummary, 0)

	var foundNs bool
	for _, ns := range s.namespaces {
		if aws.StringValue(ns.Name) == aws.StringValue(input.NamespaceName) {
			foundNs = true

			for _, srv := range s.services[*ns.Id] {
				if aws.StringValue(srv.Name) == aws.StringValue(input.ServiceName) {
					for _, inst := range s.instances[*srv.Id] {
						instances = append(instances, instanceToHTTPInstanceSummary(inst))
					}
				}
			}
		}
	}

	if !foundNs {
		return nil, ErrNamespaceNotFound
	}

	return &sd.DiscoverInstancesOutput{
		Instances: instances,
	}, nil
}

func (s *AWSSDClientStub) ListNamespacesPages(input *sd.ListNamespacesInput, fn func(*sd.ListNamespacesOutput, bool) bool) error {
	namespaces := make([]*sd.NamespaceSummary, 0)

	filter := input.Filters[0]

	for _, ns := range s.namespaces {
		if filter != nil && *filter.Name == sd.NamespaceFilterNameType {
			if *ns.Type != *filter.Values[0] {
				// skip namespaces not matching filter
				continue
			}
		}
		namespaces = append(namespaces, namespaceToNamespaceSummary(ns))
	}

	fn(&sd.ListNamespacesOutput{
		Namespaces: namespaces,
	}, true)

	return nil
}

func (s *AWSSDClientStub) ListServicesPages(input *sd.ListServicesInput, fn func(*sd.ListServicesOutput, bool) bool) error {
	services := make([]*sd.ServiceSummary, 0)

	// get namespace filter
	filter := input.Filters[0]
	if filter == nil || *filter.Name != sd.ServiceFilterNameNamespaceId {
		return errors.New("missing namespace filter")
	}
	nsID := filter.Values[0]

	for _, srv := range s.services[*nsID] {
		services = append(services, serviceToServiceSummary(srv))
	}

	fn(&sd.ListServicesOutput{
		Services: services,
	}, true)

	return nil
}

func (s *AWSSDClientStub) RegisterInstance(input *sd.RegisterInstanceInput) (*sd.RegisterInstanceOutput, error) {
	srvInstances, ok := s.instances[*input.ServiceId]
	if !ok {
		srvInstances = make(map[string]*sd.Instance)
		s.instances[*input.ServiceId] = srvInstances
	}

	srvInstances[*input.InstanceId] = &sd.Instance{
		Id:               input.InstanceId,
		Attributes:       input.Attributes,
		CreatorRequestId: input.CreatorRequestId,
	}

	return &sd.RegisterInstanceOutput{}, nil
}

func (s *AWSSDClientStub) UpdateService(input *sd.UpdateServiceInput) (*sd.UpdateServiceOutput, error) {
	out, err := s.GetService(&sd.GetServiceInput{Id: input.Id})
	if err != nil {
		return nil, err
	}

	origSrv := out.Service
	updateSrv := input.Service

	origSrv.Description = updateSrv.Description
	origSrv.DnsConfig.DnsRecords = updateSrv.DnsConfig.DnsRecords

	return &sd.UpdateServiceOutput{}, nil
}

func (s *AWSSDClientStub) DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error) {
	out, err := s.GetService(&sd.GetServiceInput{Id: input.Id})
	if err != nil {
		return nil, err
	}

	service := out.Service
	namespace := s.services[*service.NamespaceId]
	delete(namespace, *input.Id)

	return &sd.DeleteServiceOutput{}, nil
}

func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, namespaceTypeFilter, ownerID string) *AWSSDProvider {
	return &AWSSDProvider{
		client:              api,
		dryRun:              false,
		namespaceFilter:     domainFilter,
		namespaceTypeFilter: newSdNamespaceFilter(namespaceTypeFilter),
		cleanEmptyService:   true,
		ownerID:             ownerID,
	}
}

// nolint: deadcode
// used for unit test
func instanceToHTTPInstanceSummary(instance *sd.Instance) *sd.HttpInstanceSummary {
	if instance == nil {
		return nil
	}

	return &sd.HttpInstanceSummary{
		InstanceId: instance.Id,
		Attributes: instance.Attributes,
	}
}

func TestAWSSDProvider_Records(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	services := map[string]map[string]*sd.Service{
		"private": {
			"a-srv": {
				Id:          aws.String("a-srv"),
				Name:        aws.String("service1"),
				Description: aws.String("owner-id"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeA),
						TTL:  aws.Int64(100),
					}},
				},
			},
			"alias-srv": {
				Id:          aws.String("alias-srv"),
				Name:        aws.String("service2"),
				Description: aws.String("owner-id"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeA),
						TTL:  aws.Int64(100),
					}},
				},
			},
			"cname-srv": {
				Id:          aws.String("cname-srv"),
				Name:        aws.String("service3"),
				Description: aws.String("owner-id"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeCname),
						TTL:  aws.Int64(80),
					}},
				},
			},
		},
	}

	instances := map[string]map[string]*sd.Instance{
		"a-srv": {
			"1.2.3.4": {
				Id: aws.String("1.2.3.4"),
				Attributes: map[string]*string{
					sdInstanceAttrIPV4: aws.String("1.2.3.4"),
				},
			},
			"1.2.3.5": {
				Id: aws.String("1.2.3.5"),
				Attributes: map[string]*string{
					sdInstanceAttrIPV4: aws.String("1.2.3.5"),
				},
			},
		},
		"alias-srv": {
			"load-balancer.us-east-1.elb.amazonaws.com": {
				Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
				Attributes: map[string]*string{
					sdInstanceAttrAlias: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
				},
			},
		},
		"cname-srv": {
			"cname.target.com": {
				Id: aws.String("cname.target.com"),
				Attributes: map[string]*string{
					sdInstanceAttrCname: aws.String("cname.target.com"),
				},
			},
		},
	}

	expectedEndpoints := []*endpoint.Endpoint{
		{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
		{DNSName: "service2.private.com", Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
		{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 80, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   services,
		instances:  instances,
	}

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

	endpoints, _ := provider.Records(context.Background())

	assert.True(t, testutils.SameEndpoints(expectedEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", expectedEndpoints, endpoints)
}

func TestAWSSDProvider_ApplyChanges(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   make(map[string]map[string]*sd.Service),
		instances:  make(map[string]map[string]*sd.Instance),
	}

	expectedEndpoints := []*endpoint.Endpoint{
		{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 60},
		{DNSName: "service2.private.com", Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 80},
		{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100},
	}

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

	ctx := context.Background()

	// apply creates
	provider.ApplyChanges(ctx, &plan.Changes{
		Create: expectedEndpoints,
	})

	// make sure services were created
	assert.Len(t, api.services["private"], 3)
	existingServices, _ := provider.ListServicesByNamespaceID(namespaces["private"].Id)
	assert.NotNil(t, existingServices["service1"])
	assert.NotNil(t, existingServices["service2"])
	assert.NotNil(t, existingServices["service3"])

	// make sure instances were registered
	endpoints, _ := provider.Records(ctx)
	assert.True(t, testutils.SameEndpoints(expectedEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", expectedEndpoints, endpoints)

	ctx = context.Background()
	// apply deletes
	provider.ApplyChanges(ctx, &plan.Changes{
		Delete: expectedEndpoints,
	})

	// make sure all instances are gone
	endpoints, _ = provider.Records(ctx)
	assert.Empty(t, endpoints)
}

func TestAWSSDProvider_ListNamespaces(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
		"public": {
			Id:   aws.String("public"),
			Name: aws.String("public.com"),
			Type: aws.String(sd.NamespaceTypeDnsPublic),
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
	}

	for _, tc := range []struct {
		msg                 string
		domainFilter        endpoint.DomainFilter
		namespaceTypeFilter string
		expectedNamespaces  []*sd.NamespaceSummary
	}{
		{"public filter", endpoint.NewDomainFilter([]string{}), "public", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
		{"private filter", endpoint.NewDomainFilter([]string{}), "private", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["private"])}},
		{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
		{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sd.NamespaceSummary{}},
	} {
		provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter, "")

		result, err := provider.ListNamespaces()
		require.NoError(t, err)

		expectedMap := make(map[string]*sd.NamespaceSummary)
		resultMap := make(map[string]*sd.NamespaceSummary)
		for _, ns := range tc.expectedNamespaces {
			expectedMap[*ns.Id] = ns
		}
		for _, ns := range result {
			resultMap[*ns.Id] = ns
		}

		if !reflect.DeepEqual(resultMap, expectedMap) {
			t.Errorf("AWSSDProvider.ListNamespaces() error = %v, wantErr %v", result, tc.expectedNamespaces)
		}
	}
}

func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
		"public": {
			Id:   aws.String("public"),
			Name: aws.String("public.com"),
			Type: aws.String(sd.NamespaceTypeDnsPublic),
		},
	}

	services := map[string]map[string]*sd.Service{
		"private": {
			"srv1": {
				Id:          aws.String("srv1"),
				Name:        aws.String("service1"),
				NamespaceId: aws.String("private"),
			},
			"srv2": {
				Id:          aws.String("srv2"),
				Name:        aws.String("service2"),
				NamespaceId: aws.String("private"),
			},
		},
		"public": {
			"srv3": {
				Id:          aws.String("srv3"),
				Name:        aws.String("service3"),
				NamespaceId: aws.String("public"),
			},
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   services,
	}

	for _, tc := range []struct {
		expectedServices map[string]*sd.Service
	}{
		{map[string]*sd.Service{"service1": services["private"]["srv1"], "service2": services["private"]["srv2"]}},
	} {
		provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

		result, err := provider.ListServicesByNamespaceID(namespaces["private"].Id)
		require.NoError(t, err)
		assert.Equal(t, tc.expectedServices, result)
	}
}

func TestAWSSDProvider_CreateService(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   make(map[string]map[string]*sd.Service),
	}

	expectedServices := make(map[string]*sd.Service)

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

	// A type
	provider.CreateService(aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeA,
		RecordTTL:  60,
		Targets:    endpoint.Targets{"1.2.3.4"},
	})
	expectedServices["A-srv"] = &sd.Service{
		Name: aws.String("A-srv"),
		DnsConfig: &sd.DnsConfig{
			RoutingPolicy: aws.String(sd.RoutingPolicyMultivalue),
			DnsRecords: []*sd.DnsRecord{{
				Type: aws.String(sd.RecordTypeA),
				TTL:  aws.Int64(60),
			}},
		},
		NamespaceId: aws.String("private"),
	}

	// CNAME type
	provider.CreateService(aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeCNAME,
		RecordTTL:  80,
		Targets:    endpoint.Targets{"cname.target.com"},
	})
	expectedServices["CNAME-srv"] = &sd.Service{
		Name: aws.String("CNAME-srv"),
		DnsConfig: &sd.DnsConfig{
			RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
			DnsRecords: []*sd.DnsRecord{{
				Type: aws.String(sd.RecordTypeCname),
				TTL:  aws.Int64(80),
			}},
		},
		NamespaceId: aws.String("private"),
	}

	// ALIAS type
	provider.CreateService(aws.String("private"), aws.String("ALIAS-srv"), &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeCNAME,
		RecordTTL:  100,
		Targets:    endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"},
	})
	expectedServices["ALIAS-srv"] = &sd.Service{
		Name: aws.String("ALIAS-srv"),
		DnsConfig: &sd.DnsConfig{
			RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
			DnsRecords: []*sd.DnsRecord{{
				Type: aws.String(sd.RecordTypeA),
				TTL:  aws.Int64(100),
			}},
		},
		NamespaceId: aws.String("private"),
	}

	validateAWSSDServicesMapsEqual(t, expectedServices, api.services["private"])
}

func validateAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sd.Service, services map[string]*sd.Service) {
	require.Len(t, services, len(expected))

	for _, srv := range services {
		validateAWSSDServicesEqual(t, expected[*srv.Name], srv)
	}
}

func validateAWSSDServicesEqual(t *testing.T, expected *sd.Service, srv *sd.Service) {
	assert.Equal(t, aws.StringValue(expected.Description), aws.StringValue(srv.Description))
	assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(srv.Name))
	assert.True(t, reflect.DeepEqual(*expected.DnsConfig, *srv.DnsConfig))
}

func TestAWSSDProvider_UpdateService(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	services := map[string]map[string]*sd.Service{
		"private": {
			"srv1": {
				Id:   aws.String("srv1"),
				Name: aws.String("service1"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyMultivalue),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeA),
						TTL:  aws.Int64(60),
					}},
				},
			},
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   services,
	}

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

	// update service with different TTL
	provider.UpdateService(services["private"]["srv1"], &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeA,
		RecordTTL:  100,
	})

	assert.Equal(t, int64(100), *api.services["private"]["srv1"].DnsConfig.DnsRecords[0].TTL)
}

func TestAWSSDProvider_DeleteService(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	services := map[string]map[string]*sd.Service{
		"private": {
			"srv1": {
				Id:          aws.String("srv1"),
				Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
				Name:        aws.String("service1"),
				NamespaceId: aws.String("private"),
			},
			"srv2": {
				Id:          aws.String("srv2"),
				Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
				Name:        aws.String("service2"),
				NamespaceId: aws.String("private"),
			},
			"srv3": {
				Id:          aws.String("srv3"),
				Description: aws.String("heritage=external-dns,external-dns/owner=owner-id,external-dns/resource=virtualservice/grpc-server/validate-grpc-server"),
				Name:        aws.String("service3"),
				NamespaceId: aws.String("private"),
			},
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   services,
	}

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id")

	// delete first service
	err := provider.DeleteService(services["private"]["srv1"])
	assert.NoError(t, err)
	assert.Len(t, api.services["private"], 2)

	// delete third service
	err1 := provider.DeleteService(services["private"]["srv3"])
	assert.NoError(t, err1)
	assert.Len(t, api.services["private"], 1)

	expectedServices := map[string]*sd.Service{
		"srv2": {
			Id:          aws.String("srv2"),
			Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
			Name:        aws.String("service2"),
			NamespaceId: aws.String("private"),
		},
	}

	assert.Equal(t, expectedServices, api.services["private"])
}

func TestAWSSDProvider_RegisterInstance(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	services := map[string]map[string]*sd.Service{
		"private": {
			"a-srv": {
				Id:   aws.String("a-srv"),
				Name: aws.String("service1"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeA),
						TTL:  aws.Int64(60),
					}},
				},
			},
			"cname-srv": {
				Id:   aws.String("cname-srv"),
				Name: aws.String("service2"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeCname),
						TTL:  aws.Int64(60),
					}},
				},
			},
			"alias-srv": {
				Id:   aws.String("alias-srv"),
				Name: aws.String("service3"),
				DnsConfig: &sd.DnsConfig{
					NamespaceId:   aws.String("private"),
					RoutingPolicy: aws.String(sd.RoutingPolicyWeighted),
					DnsRecords: []*sd.DnsRecord{{
						Type: aws.String(sd.RecordTypeA),
						TTL:  aws.Int64(60),
					}},
				},
			},
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   services,
		instances:  make(map[string]map[string]*sd.Instance),
	}

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

	expectedInstances := make(map[string]*sd.Instance)

	// IP-based instance
	provider.RegisterInstance(services["private"]["a-srv"], &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeA,
		DNSName:    "service1.private.com.",
		RecordTTL:  300,
		Targets:    endpoint.Targets{"1.2.3.4", "1.2.3.5"},
	})
	expectedInstances["1.2.3.4"] = &sd.Instance{
		Id: aws.String("1.2.3.4"),
		Attributes: map[string]*string{
			sdInstanceAttrIPV4: aws.String("1.2.3.4"),
		},
	}
	expectedInstances["1.2.3.5"] = &sd.Instance{
		Id: aws.String("1.2.3.5"),
		Attributes: map[string]*string{
			sdInstanceAttrIPV4: aws.String("1.2.3.5"),
		},
	}

	// AWS ELB instance (ALIAS)
	provider.RegisterInstance(services["private"]["alias-srv"], &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeCNAME,
		DNSName:    "service1.private.com.",
		RecordTTL:  300,
		Targets:    endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com", "load-balancer.us-west-2.elb.amazonaws.com"},
	})
	expectedInstances["load-balancer.us-east-1.elb.amazonaws.com"] = &sd.Instance{
		Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
		Attributes: map[string]*string{
			sdInstanceAttrAlias: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
		},
	}
	expectedInstances["load-balancer.us-west-2.elb.amazonaws.com"] = &sd.Instance{
		Id: aws.String("load-balancer.us-west-2.elb.amazonaws.com"),
		Attributes: map[string]*string{
			sdInstanceAttrAlias: aws.String("load-balancer.us-west-2.elb.amazonaws.com"),
		},
	}

	// AWS NLB instance (ALIAS)
	provider.RegisterInstance(services["private"]["alias-srv"], &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeCNAME,
		DNSName:    "service1.private.com.",
		RecordTTL:  300,
		Targets:    endpoint.Targets{"load-balancer.elb.us-west-2.amazonaws.com"},
	})
	expectedInstances["load-balancer.elb.us-west-2.amazonaws.com"] = &sd.Instance{
		Id: aws.String("load-balancer.elb.us-west-2.amazonaws.com"),
		Attributes: map[string]*string{
			sdInstanceAttrAlias: aws.String("load-balancer.elb.us-west-2.amazonaws.com"),
		},
	}

	// CNAME instance
	provider.RegisterInstance(services["private"]["cname-srv"], &endpoint.Endpoint{
		RecordType: endpoint.RecordTypeCNAME,
		DNSName:    "service2.private.com.",
		RecordTTL:  300,
		Targets:    endpoint.Targets{"cname.target.com"},
	})
	expectedInstances["cname.target.com"] = &sd.Instance{
		Id: aws.String("cname.target.com"),
		Attributes: map[string]*string{
			sdInstanceAttrCname: aws.String("cname.target.com"),
		},
	}

	// validate instances
	for _, srvInst := range api.instances {
		for id, inst := range srvInst {
			if !reflect.DeepEqual(*expectedInstances[id], *inst) {
				t.Errorf("Instances don't match, expected = %v, actual %v", *expectedInstances[id], *inst)
			}
		}
	}
}

func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
	namespaces := map[string]*sd.Namespace{
		"private": {
			Id:   aws.String("private"),
			Name: aws.String("private.com"),
			Type: aws.String(sd.NamespaceTypeDnsPrivate),
		},
	}

	services := map[string]map[string]*sd.Service{
		"private": {
			"srv1": {
				Id:   aws.String("srv1"),
				Name: aws.String("service1"),
			},
		},
	}

	instances := map[string]map[string]*sd.Instance{
		"srv1": {
			"1.2.3.4": {
				Id: aws.String("1.2.3.4"),
				Attributes: map[string]*string{
					sdInstanceAttrIPV4: aws.String("1.2.3.4"),
				},
			},
		},
	}

	api := &AWSSDClientStub{
		namespaces: namespaces,
		services:   services,
		instances:  instances,
	}

	provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")

	provider.DeregisterInstance(services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))

	assert.Len(t, instances["srv1"], 0)
}
