package lattice

import (
	"context"
	"errors"
	"testing"

	pkg_aws "github.com/aws/aws-application-networking-k8s/pkg/aws"
	mocks "github.com/aws/aws-application-networking-k8s/pkg/aws/services"
	"github.com/aws/aws-application-networking-k8s/pkg/model/core"
	model "github.com/aws/aws-application-networking-k8s/pkg/model/lattice"
	lattice_runtime "github.com/aws/aws-application-networking-k8s/pkg/runtime"
	"github.com/aws/aws-application-networking-k8s/pkg/utils/gwlog"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/vpclattice"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
)

func TestServiceManagerInteg(t *testing.T) {
	c := gomock.NewController(t)
	defer c.Finish()

	mockLattice := mocks.NewMockLattice(c)
	mockTagging := mocks.NewMockTagging(c)
	cfg := pkg_aws.CloudConfig{VpcId: "vpc-id", AccountId: "account-id"}
	cl := pkg_aws.NewDefaultCloudWithTagging(mockLattice, mockTagging, cfg)
	ctx := context.Background()
	m := NewServiceManager(gwlog.FallbackLogger, cl)

	// Case for single service and single sn-svc association
	// Make sure that we send requests to Lattice for create Service and create Sn-Svc
	t.Run("create new service and association", func(t *testing.T) {

		svc := &Service{
			Spec: model.ServiceSpec{
				ServiceTagFields: model.ServiceTagFields{
					RouteName:      "svc",
					RouteNamespace: "ns",
					RouteType:      core.HttpRouteType,
				},
				ServiceNetworkNames: []string{"sn"},
				CustomerDomainName:  "dns",
				CustomerCertARN:     "cert-arn",
			},
		}

		// service does not exist in lattice
		mockLattice.EXPECT().
			FindService(gomock.Any(), gomock.Any()).
			Return(nil, mocks.NewNotFoundError("", "")).
			Times(1)

		// assert that we call create service
		mockLattice.EXPECT().
			CreateServiceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(_ context.Context, req *CreateSvcReq, _ ...interface{}) (*CreateSvcResp, error) {
					assert.Equal(t, svc.LatticeServiceName(), *req.Name)
					return &CreateSvcResp{
						Arn:      aws.String("arn"),
						DnsEntry: &vpclattice.DnsEntry{DomainName: aws.String("dns")},
						Id:       aws.String("svc-id"),
					}, nil
				}).
			Times(1)

		// assert that we call create association
		mockLattice.EXPECT().
			CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(_ context.Context, req *CreateSnSvcAssocReq, _ ...interface{}) (*CreateSnSvcAssocResp, error) {
					assert.Equal(t, "sn-id", *req.ServiceNetworkIdentifier)
					assert.Contains(t, req.Tags, pkg_aws.TagManagedBy)
					return &CreateSnSvcAssocResp{
						Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
					}, nil
				}).
			Times(1)

		// expect a call to find the service network
		mockLattice.EXPECT().
			FindServiceNetwork(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(ctx context.Context, name string) (*mocks.ServiceNetworkInfo, error) {
					return &mocks.ServiceNetworkInfo{
						SvcNetwork: vpclattice.ServiceNetworkSummary{
							Arn:  aws.String("sn-arn"),
							Id:   aws.String("sn-id"),
							Name: aws.String(name),
						},
						Tags: nil,
					}, nil
				}).
			Times(1)

		status, err := m.Upsert(ctx, svc)
		assert.Nil(t, err)
		assert.Equal(t, "arn", status.Arn)
	})

	// Update is more complex than create, we need to apply diff for Sn-Svc associations
	// This test covers creation/deletion for multiple SN's
	// sn-keep - no changes
	// sn-delete - managed by gateway and want to delete
	// sn-create - managed by gateway and want to create
	// sn-foreign - not managed by gateway and exists in lattice (should keep)
	// sn-foreign-ram - managed in a different account. management check will be skipped (should keep)
	t.Run("update service's associations", func(t *testing.T) {
		snKeep := "sn-keep"
		snDelete := "sn-delete"
		snAdd := "sn-add"
		snForeign := "sn-foreign"
		snForeignRAM := "sn-foreign-ram"

		svc := &Service{
			Spec: model.ServiceSpec{
				ServiceTagFields: model.ServiceTagFields{
					RouteName:      "svc",
					RouteNamespace: "ns",
					RouteType:      core.HttpRouteType,
				},
				ServiceNetworkNames: []string{snKeep, snAdd},
			},
		}

		// service exists in lattice
		mockLattice.EXPECT().
			FindService(gomock.Any(), gomock.Any()).
			Return(&vpclattice.ServiceSummary{
				Arn:  aws.String("svc-arn"),
				Id:   aws.String("svc-id"),
				Name: aws.String(svc.LatticeServiceName()),
			}, nil).
			Times(1)

		mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
				return &vpclattice.ListTagsForResourceOutput{
					Tags: cl.DefaultTagsMergedWith(svc.Spec.ToTags()),
				}, nil
			}).
			Times(1) // for service only

		mockTagging.EXPECT().UpdateTags(ctx, "svc-arn", gomock.Any()).Return(nil)

		// 3 associations exist in lattice: keep, delete, and foreign
		mockLattice.EXPECT().
			ListServiceNetworkServiceAssociationsAsList(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *ListSnSvcAssocsReq) ([]*SnSvcAssocSummary, error) {
				assocs := []*SnSvcAssocSummary{}
				for _, sn := range []string{snKeep, snDelete, snForeign, snForeignRAM, snForeignRAM} {
					assocs = append(assocs, &SnSvcAssocSummary{
						Arn:                aws.String(sn + "-arn"),
						Id:                 aws.String(sn + "-id"),
						ServiceNetworkName: aws.String(sn),
						Status:             aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
					})
				}
				return assocs, nil
			}).
			Times(1)

		mockTagging.EXPECT().UpdateTags(ctx, "sn-keep-arn", gomock.Any()).Return(nil)

		// return managed by gateway controller tags for all associations except for foreign and foreign ram
		mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
				if *req.ResourceArn == snForeign+"-arn" {
					return &vpclattice.ListTagsForResourceOutput{}, nil
				} else if *req.ResourceArn == snForeignRAM+"-arn" {
					return nil, errors.New("some_error")
				} else {
					return &vpclattice.ListTagsForResourceOutput{
						Tags: cl.DefaultTags(),
					}, nil
				}
			}).
			Times(3) // delete and foreign and foreign ram

		// assert we call create and delete associations on managed resources
		mockLattice.EXPECT().
			CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(_ context.Context, req *CreateSnSvcAssocReq, _ ...interface{}) (*CreateSnSvcAssocResp, error) {
					assert.Equal(t, snAdd+"-id", *req.ServiceNetworkIdentifier)
					return &CreateSnSvcAssocResp{Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive)}, nil
				}).
			Times(1)

		// make sure we call delete only on managed resource, only snDelete should be deleted
		mockLattice.EXPECT().
			DeleteServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(_ context.Context, req *DelSnSvcAssocReq, _ ...interface{}) (*DelSnSvcAssocResp, error) {
					assert.Equal(t, snDelete+"-arn", *req.ServiceNetworkServiceAssociationIdentifier)
					return &DelSnSvcAssocResp{}, nil
				}).
			Times(1)

		// expect calls to find the service network
		mockLattice.EXPECT().
			FindServiceNetwork(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(ctx context.Context, name string) (*mocks.ServiceNetworkInfo, error) {
					return &mocks.ServiceNetworkInfo{
						SvcNetwork: vpclattice.ServiceNetworkSummary{
							Arn:  aws.String(name + "-arn"),
							Id:   aws.String(name + "-id"),
							Name: aws.String(name),
						},
						Tags: nil,
					}, nil
				}).
			AnyTimes()

		status, err := m.Upsert(ctx, svc)
		assert.Nil(t, err)
		assert.Equal(t, "svc-arn", status.Arn)
	})

	t.Run("backfilling service tags", func(t *testing.T) {
		svc := &Service{
			Spec: model.ServiceSpec{
				ServiceTagFields: model.ServiceTagFields{
					RouteName:      "svc",
					RouteNamespace: "ns",
					RouteType:      core.HttpRouteType,
				},
				ServiceNetworkNames: []string{"sn"},
			},
		}

		// service exists in lattice
		mockLattice.EXPECT().
			FindService(gomock.Any(), gomock.Any()).
			Return(&vpclattice.ServiceSummary{
				Arn:  aws.String("svc-arn"),
				Id:   aws.String("svc-id"),
				Name: aws.String(svc.LatticeServiceName()),
			}, nil).
			Times(1)

		mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
				return &vpclattice.ListTagsForResourceOutput{
					Tags: map[string]*string{},
				}, nil
			}).
			Times(1)

		mockLattice.EXPECT().TagResourceWithContext(gomock.Any(), gomock.Eq(&vpclattice.TagResourceInput{
			ResourceArn: aws.String("svc-arn"),
			Tags:        cl.DefaultTags(),
		})).Times(1)

		mockLattice.EXPECT().TagResourceWithContext(gomock.Any(), gomock.Eq(&vpclattice.TagResourceInput{
			ResourceArn: aws.String("svc-arn"),
			Tags:        svc.Spec.ToTags(),
		})).Times(1)

		mockTagging.EXPECT().UpdateTags(ctx, "svc-arn", gomock.Any()).Return(nil)

		mockLattice.EXPECT().ListServiceNetworkServiceAssociationsAsList(gomock.Any(), gomock.Any()).Times(1)
		mockLattice.EXPECT().
			CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(_ context.Context, req *CreateSnSvcAssocReq, _ ...interface{}) (*CreateSnSvcAssocResp, error) {
					return &CreateSnSvcAssocResp{Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive)}, nil
				}).
			Times(1)

		status, err := m.Upsert(ctx, svc)
		assert.Nil(t, err)
		assert.Equal(t, "svc-arn", status.Arn)
	})

	// Test for standalone service creation (no service network associations)
	t.Run("create standalone service without associations", func(t *testing.T) {
		svc := &Service{
			Spec: model.ServiceSpec{
				ServiceTagFields: model.ServiceTagFields{
					RouteName:      "standalone-svc",
					RouteNamespace: "ns",
					RouteType:      core.HttpRouteType,
				},
				ServiceNetworkNames: []string{}, // Empty for standalone mode
				CustomerDomainName:  "standalone-dns",
				CustomerCertARN:     "standalone-cert-arn",
			},
		}

		// service does not exist in lattice
		mockLattice.EXPECT().
			FindService(gomock.Any(), gomock.Any()).
			Return(nil, mocks.NewNotFoundError("", "")).
			Times(1)

		// assert that we call create service
		mockLattice.EXPECT().
			CreateServiceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(
				func(_ context.Context, req *CreateSvcReq, _ ...interface{}) (*CreateSvcResp, error) {
					assert.Equal(t, svc.LatticeServiceName(), *req.Name)
					return &CreateSvcResp{
						Arn:      aws.String("standalone-arn"),
						DnsEntry: &vpclattice.DnsEntry{DomainName: aws.String("standalone-dns")},
						Id:       aws.String("standalone-svc-id"),
					}, nil
				}).
			Times(1)

		// assert that we do NOT call create association for standalone services
		mockLattice.EXPECT().
			CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
			Times(0)

		// assert that we do NOT call find service network for standalone services
		mockLattice.EXPECT().
			FindServiceNetwork(gomock.Any(), gomock.Any()).
			Times(0)

		status, err := m.Upsert(ctx, svc)
		assert.Nil(t, err)
		assert.Equal(t, "standalone-arn", status.Arn)
		assert.Equal(t, "standalone-svc-id", status.Id)
		assert.Equal(t, "standalone-dns", status.Dns)
	})

	// Test for updating standalone service (should not create/delete associations)
	t.Run("update standalone service without associations", func(t *testing.T) {
		svc := &Service{
			Spec: model.ServiceSpec{
				ServiceTagFields: model.ServiceTagFields{
					RouteName:      "standalone-svc",
					RouteNamespace: "ns",
					RouteType:      core.HttpRouteType,
				},
				ServiceNetworkNames: []string{}, // Empty for standalone mode
			},
		}

		// service exists in lattice
		mockLattice.EXPECT().
			FindService(gomock.Any(), gomock.Any()).
			Return(&vpclattice.ServiceSummary{
				Arn:  aws.String("standalone-svc-arn"),
				Id:   aws.String("standalone-svc-id"),
				Name: aws.String(svc.LatticeServiceName()),
			}, nil).
			Times(1)

		mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
				return &vpclattice.ListTagsForResourceOutput{
					Tags: cl.DefaultTagsMergedWith(svc.Spec.ToTags()),
				}, nil
			}).
			Times(1) // for service only

		mockTagging.EXPECT().UpdateTags(ctx, "standalone-svc-arn", gomock.Any()).Return(nil)

		// no associations exist for standalone service
		mockLattice.EXPECT().
			ListServiceNetworkServiceAssociationsAsList(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *ListSnSvcAssocsReq) ([]*SnSvcAssocSummary, error) {
				return []*SnSvcAssocSummary{}, nil
			}).
			Times(1)

		// assert that we do NOT call create or delete association for standalone services
		mockLattice.EXPECT().
			CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
			Times(0)

		mockLattice.EXPECT().
			DeleteServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
			Times(0)

		// assert that we do NOT call find service network for standalone services
		mockLattice.EXPECT().
			FindServiceNetwork(gomock.Any(), gomock.Any()).
			Times(0)

		status, err := m.Upsert(ctx, svc)
		assert.Nil(t, err)
		assert.Equal(t, "standalone-svc-arn", status.Arn)
	})

	t.Run("delete service and association", func(t *testing.T) {
		svc := &Service{
			Spec: model.ServiceSpec{
				ServiceTagFields: model.ServiceTagFields{
					RouteName:      "svc",
					RouteNamespace: "ns",
				},
				ServiceNetworkNames: []string{"sn"},
			},
		}

		// service exists
		mockLattice.EXPECT().
			FindService(gomock.Any(), gomock.Any()).
			Return(&vpclattice.ServiceSummary{
				Arn:  aws.String("svc-arn"),
				Id:   aws.String("svc-id"),
				Name: aws.String(svc.LatticeServiceName()),
			}, nil)

		mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
			DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
				return &vpclattice.ListTagsForResourceOutput{
					Tags: cl.DefaultTagsMergedWith(svc.Spec.ToTags()),
				}, nil
			}).
			Times(1) // for service only

		mockLattice.EXPECT().
			ListServiceNetworkServiceAssociationsAsList(gomock.Any(), gomock.Any()).
			Return([]*SnSvcAssocSummary{
				{
					Arn:                aws.String("assoc-arn"),
					Id:                 aws.String("assoc-id"),
					ServiceNetworkName: aws.String("sn"),
					Status:             aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
				},
			}, nil)

		// assert we delete association and service
		mockLattice.EXPECT().
			DeleteServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(nil, nil).
			Times(1)

		mockLattice.EXPECT().
			DeleteServiceWithContext(gomock.Any(), gomock.Any()).Return(nil, nil).
			Times(1)

		// assert we delete listeners
		mockLattice.EXPECT().
			ListListenersAsList(gomock.Any(), gomock.Any()).
			Return([]*vpclattice.ListenerSummary{
				{
					Id: aws.String("L1"),
				},
				{
					Id: aws.String("L2"),
				},
			}, nil)

		mockLattice.EXPECT().
			DeleteListenerWithContext(gomock.Any(), gomock.Any()).
			Return(nil, nil).
			Times(2)

		err := m.Delete(ctx, svc)
		assert.Nil(t, err)
	})

}

func TestCreateSvcReq(t *testing.T) {
	cfg := pkg_aws.CloudConfig{VpcId: "vpc-id", AccountId: "account-id"}
	cl := pkg_aws.NewDefaultCloud(nil, cfg)
	m := NewServiceManager(gwlog.FallbackLogger, cl)

	spec := model.ServiceSpec{
		ServiceTagFields: model.ServiceTagFields{
			RouteName:      "svc",
			RouteNamespace: "ns",
			RouteType:      core.HttpRouteType,
		},
		CustomerDomainName: "dns",
		CustomerCertARN:    "cert-arn",
	}

	svcModel := &model.Service{
		Spec: spec,
	}

	req := m.newCreateSvcReq(svcModel)

	assert.Equal(t, req.Tags, cl.DefaultTagsMergedWith(spec.ToTags()))
	assert.Equal(t, *req.Name, svcModel.LatticeServiceName())
	assert.Equal(t, *req.CustomDomainName, spec.CustomerDomainName)
	assert.Equal(t, *req.CertificateArn, spec.CustomerCertARN)

}

func TestSvcStatusFromCreateSvcResp(t *testing.T) {
	resp := &CreateSvcResp{
		Arn: aws.String("arn"),
		DnsEntry: &vpclattice.DnsEntry{
			DomainName: aws.String("dns"),
		},
		Id: aws.String("id"),
	}

	status := svcStatusFromCreateSvcResp(resp)

	assert.Equal(t, *resp.Arn, status.Arn)
	assert.Equal(t, *resp.Id, status.Id)
	assert.Equal(t, *resp.DnsEntry.DomainName, status.Dns)
}

func TestHandleSnSvcAssocResp(t *testing.T) {

	t.Run("assoc status active", func(t *testing.T) {
		resp := &CreateSnSvcAssocResp{
			Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
		}
		err := handleCreateAssociationResp(resp)
		assert.Nil(t, err)
	})

	t.Run("assoc status non active", func(t *testing.T) {
		resp := &CreateSnSvcAssocResp{
			Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusCreateInProgress),
		}
		err := handleCreateAssociationResp(resp)
		var requeueNeededAfter *lattice_runtime.RequeueNeededAfter
		assert.True(t, errors.As(err, &requeueNeededAfter))
	})

}

func TestSnSvcAssocsDiff(t *testing.T) {

	t.Run("no diff", func(t *testing.T) {
		svc := &Service{Spec: model.ServiceSpec{
			ServiceNetworkNames: []string{"sn"},
		}}
		assocs := []*SnSvcAssocSummary{{ServiceNetworkName: aws.String("sn")}}
		c, d, _, err := associationsDiff(svc, assocs)
		assert.Nil(t, err)
		assert.Equal(t, 0, len(c))
		assert.Equal(t, 0, len(d))
	})

	t.Run("only create", func(t *testing.T) {
		svc := &Service{Spec: model.ServiceSpec{
			ServiceNetworkNames: []string{"sn1", "sn2"},
		}}
		assocs := []*SnSvcAssocSummary{}
		c, d, _, _ := associationsDiff(svc, assocs)
		assert.Equal(t, 2, len(c))
		assert.Equal(t, 0, len(d))
	})

	t.Run("only delete", func(t *testing.T) {
		svc := &Service{}
		assocs := []*SnSvcAssocSummary{
			{ServiceNetworkName: aws.String("sn1")},
			{ServiceNetworkName: aws.String("sn2")},
		}
		c, d, _, _ := associationsDiff(svc, assocs)
		assert.Equal(t, 0, len(c))
		assert.Equal(t, 2, len(d))
	})

	t.Run("create and delete", func(t *testing.T) {
		svc := &Service{Spec: model.ServiceSpec{
			ServiceNetworkNames: []string{"sn1", "sn2", "sn3"},
		}}
		assocs := []*SnSvcAssocSummary{
			{ServiceNetworkName: aws.String("sn1")},
			{ServiceNetworkName: aws.String("sn4")},
		}
		c, d, _, _ := associationsDiff(svc, assocs)
		assert.Equal(t, 2, len(c))
		assert.Equal(t, 1, len(d))
	})

	t.Run("retry error on create assoc that in deletion state", func(t *testing.T) {
		svc := &Service{Spec: model.ServiceSpec{
			ServiceNetworkNames: []string{"sn"},
		}}
		assocs := []*SnSvcAssocSummary{{
			ServiceNetworkName: aws.String("sn"),
			Status:             aws.String(vpclattice.ServiceNetworkServiceAssociationStatusDeleteInProgress),
		}}
		_, _, _, err := associationsDiff(svc, assocs)
		var requeueNeededAfter *lattice_runtime.RequeueNeededAfter
		assert.True(t, errors.As(err, &requeueNeededAfter))
	})

}

func Test_ServiceManager_WithAdditionalTags_CreateService(t *testing.T) {
	c := gomock.NewController(t)
	defer c.Finish()

	mockLattice := mocks.NewMockLattice(c)
	mockTagging := mocks.NewMockTagging(c)
	cfg := pkg_aws.CloudConfig{VpcId: "vpc-id", AccountId: "account-id"}
	cl := pkg_aws.NewDefaultCloudWithTagging(mockLattice, mockTagging, cfg)
	ctx := context.Background()
	m := NewServiceManager(gwlog.FallbackLogger, cl)

	svc := &Service{
		Spec: model.ServiceSpec{
			ServiceTagFields: model.ServiceTagFields{
				RouteName:      "svc-with-tags",
				RouteNamespace: "ns",
				RouteType:      core.HttpRouteType,
			},
			ServiceNetworkNames: []string{"sn"},
			AdditionalTags: mocks.Tags{
				"Environment": &[]string{"Test"}[0],
				"Project":     &[]string{"ServiceManager"}[0],
			},
		},
	}

	mockLattice.EXPECT().
		FindService(gomock.Any(), gomock.Any()).
		Return(nil, mocks.NewNotFoundError("", "")).
		Times(1)

	expectedServiceTags := cl.MergeTags(cl.DefaultTagsMergedWith(svc.Spec.ToTags()), svc.Spec.AdditionalTags)

	mockLattice.EXPECT().
		CreateServiceWithContext(gomock.Any(), gomock.Any()).
		DoAndReturn(
			func(_ context.Context, req *CreateSvcReq, _ ...interface{}) (*CreateSvcResp, error) {
				assert.Equal(t, svc.LatticeServiceName(), *req.Name)
				assert.Equal(t, expectedServiceTags, req.Tags, "Service tags should include additional tags")
				return &CreateSvcResp{
					Arn:      aws.String("arn"),
					DnsEntry: &vpclattice.DnsEntry{DomainName: aws.String("dns")},
					Id:       aws.String("svc-id"),
				}, nil
			}).
		Times(1)

	expectedAssocTags := cl.MergeTags(cl.DefaultTags(), svc.Spec.AdditionalTags)

	mockLattice.EXPECT().
		CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
		DoAndReturn(
			func(_ context.Context, req *CreateSnSvcAssocReq, _ ...interface{}) (*CreateSnSvcAssocResp, error) {
				assert.Equal(t, "sn-id", *req.ServiceNetworkIdentifier)
				assert.Equal(t, expectedAssocTags, req.Tags, "Association tags should include additional tags")
				return &CreateSnSvcAssocResp{
					Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
				}, nil
			}).
		Times(1)

	mockLattice.EXPECT().
		FindServiceNetwork(gomock.Any(), gomock.Any()).
		DoAndReturn(
			func(ctx context.Context, name string) (*mocks.ServiceNetworkInfo, error) {
				return &mocks.ServiceNetworkInfo{
					SvcNetwork: vpclattice.ServiceNetworkSummary{
						Arn:  aws.String("sn-arn"),
						Id:   aws.String("sn-id"),
						Name: aws.String(name),
					},
					Tags: nil,
				}, nil
			}).
		Times(1)

	status, err := m.Upsert(ctx, svc)
	assert.Nil(t, err)
	assert.Equal(t, "arn", status.Arn)
}

func Test_ServiceManager_WithAdditionalTags_UpdateService(t *testing.T) {
	c := gomock.NewController(t)
	defer c.Finish()

	mockLattice := mocks.NewMockLattice(c)
	mockTagging := mocks.NewMockTagging(c)
	cfg := pkg_aws.CloudConfig{VpcId: "vpc-id", AccountId: "account-id"}
	cl := pkg_aws.NewDefaultCloudWithTagging(mockLattice, mockTagging, cfg)
	ctx := context.Background()
	m := NewServiceManager(gwlog.FallbackLogger, cl)

	svc := &Service{
		Spec: model.ServiceSpec{
			ServiceTagFields: model.ServiceTagFields{
				RouteName:      "svc-update",
				RouteNamespace: "ns",
				RouteType:      core.HttpRouteType,
			},
			ServiceNetworkNames: []string{"sn-keep"},
			AdditionalTags: mocks.Tags{
				"Environment": &[]string{"Prod"}[0],
				"Project":     &[]string{"UpdateTest"}[0],
			},
		},
	}

	mockLattice.EXPECT().
		FindService(gomock.Any(), gomock.Any()).
		Return(&vpclattice.ServiceSummary{
			Arn:  aws.String("svc-arn"),
			Id:   aws.String("svc-id"),
			Name: aws.String(svc.LatticeServiceName()),
		}, nil).
		Times(1)

	mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
			return &vpclattice.ListTagsForResourceOutput{
				Tags: cl.DefaultTagsMergedWith(svc.Spec.ToTags()),
			}, nil
		}).
		Times(1)

	mockTagging.EXPECT().UpdateTags(ctx, "svc-arn", svc.Spec.AdditionalTags).Return(nil)

	mockLattice.EXPECT().
		ListServiceNetworkServiceAssociationsAsList(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *ListSnSvcAssocsReq) ([]*SnSvcAssocSummary, error) {
			return []*SnSvcAssocSummary{
				{
					Arn:                aws.String("assoc-arn"),
					Id:                 aws.String("assoc-id"),
					ServiceNetworkName: aws.String("sn-keep"),
					Status:             aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
				},
			}, nil
		}).
		Times(1)

	mockTagging.EXPECT().UpdateTags(ctx, "assoc-arn", svc.Spec.AdditionalTags).Return(nil)

	status, err := m.Upsert(ctx, svc)
	assert.Nil(t, err)
	assert.Equal(t, "svc-arn", status.Arn)
}

func Test_ServiceManager_WithAdditionalTags_UpdateAssociations(t *testing.T) {
	c := gomock.NewController(t)
	defer c.Finish()

	mockLattice := mocks.NewMockLattice(c)
	mockTagging := mocks.NewMockTagging(c)
	cfg := pkg_aws.CloudConfig{VpcId: "vpc-id", AccountId: "account-id"}
	cl := pkg_aws.NewDefaultCloudWithTagging(mockLattice, mockTagging, cfg)
	ctx := context.Background()
	m := NewServiceManager(gwlog.FallbackLogger, cl)

	svc := &Service{
		Spec: model.ServiceSpec{
			ServiceTagFields: model.ServiceTagFields{
				RouteName:      "complex-svc",
				RouteNamespace: "ns",
				RouteType:      core.HttpRouteType,
			},
			ServiceNetworkNames: []string{"sn-keep", "sn-create"},
			AdditionalTags: mocks.Tags{
				"Environment": &[]string{"Complex"}[0],
				"Project":     &[]string{"UpdateAssocTest"}[0],
			},
		},
	}

	mockLattice.EXPECT().
		FindService(gomock.Any(), gomock.Any()).
		Return(&vpclattice.ServiceSummary{
			Arn:  aws.String("svc-arn"),
			Id:   aws.String("svc-id"),
			Name: aws.String(svc.LatticeServiceName()),
		}, nil).
		Times(1)

	mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
			return &vpclattice.ListTagsForResourceOutput{
				Tags: cl.DefaultTagsMergedWith(svc.Spec.ToTags()),
			}, nil
		}).
		Times(1)

	mockTagging.EXPECT().UpdateTags(ctx, "svc-arn", svc.Spec.AdditionalTags).Return(nil)

	mockLattice.EXPECT().
		ListServiceNetworkServiceAssociationsAsList(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *ListSnSvcAssocsReq) ([]*SnSvcAssocSummary, error) {
			return []*SnSvcAssocSummary{
				{
					Arn:                aws.String("sn-keep-arn"),
					Id:                 aws.String("sn-keep-id"),
					ServiceNetworkName: aws.String("sn-keep"),
					Status:             aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
				},
				{
					Arn:                aws.String("sn-delete-arn"),
					Id:                 aws.String("sn-delete-id"),
					ServiceNetworkName: aws.String("sn-delete"),
					Status:             aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
				},
			}, nil
		}).
		Times(1)

	mockTagging.EXPECT().UpdateTags(ctx, "sn-keep-arn", svc.Spec.AdditionalTags).Return(nil)

	mockLattice.EXPECT().ListTagsForResourceWithContext(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *vpclattice.ListTagsForResourceInput, _ ...interface{}) (*vpclattice.ListTagsForResourceOutput, error) {
			return &vpclattice.ListTagsForResourceOutput{
				Tags: cl.DefaultTags(),
			}, nil
		}).
		Times(1)

	expectedAssocTags := cl.MergeTags(cl.DefaultTags(), svc.Spec.AdditionalTags)
	mockLattice.EXPECT().
		CreateServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any()).
		DoAndReturn(
			func(_ context.Context, req *CreateSnSvcAssocReq, _ ...interface{}) (*CreateSnSvcAssocResp, error) {
				assert.Equal(t, "sn-create-id", *req.ServiceNetworkIdentifier)
				assert.Equal(t, expectedAssocTags, req.Tags, "New association should include additional tags")
				return &CreateSnSvcAssocResp{
					Status: aws.String(vpclattice.ServiceNetworkServiceAssociationStatusActive),
				}, nil
			}).
		Times(1)

	mockLattice.EXPECT().
		DeleteServiceNetworkServiceAssociationWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
		DoAndReturn(
			func(_ context.Context, req *DelSnSvcAssocReq, _ ...interface{}) (*DelSnSvcAssocResp, error) {
				assert.Equal(t, "sn-delete-arn", *req.ServiceNetworkServiceAssociationIdentifier)
				return &DelSnSvcAssocResp{}, nil
			}).
		Times(1)

	mockLattice.EXPECT().
		FindServiceNetwork(gomock.Any(), gomock.Any()).
		DoAndReturn(
			func(ctx context.Context, name string) (*mocks.ServiceNetworkInfo, error) {
				return &mocks.ServiceNetworkInfo{
					SvcNetwork: vpclattice.ServiceNetworkSummary{
						Arn:  aws.String(name + "-arn"),
						Id:   aws.String(name + "-id"),
						Name: aws.String(name),
					},
					Tags: nil,
				}, nil
			}).
		Times(1)

	status, err := m.Upsert(ctx, svc)
	assert.Nil(t, err)
	assert.Equal(t, "svc-arn", status.Arn)
}
