package services

import (
	"context"
	"errors"
	"strconv"
	"strings"
	"testing"

	"code.justin.tv/eventbus/controlplane/internal/db/postgres"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/kms"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/twitchtv/twirp"

	clientMocks "code.justin.tv/eventbus/controlplane/internal/clients/mocks"
	"code.justin.tv/eventbus/controlplane/internal/clients/servicecatalog"
	"code.justin.tv/eventbus/controlplane/internal/db"
	dbMocks "code.justin.tv/eventbus/controlplane/internal/db/mocks"
	"code.justin.tv/eventbus/controlplane/internal/ldap"
	twirpErrMocks "code.justin.tv/eventbus/controlplane/internal/twirperr/mocks"
	"code.justin.tv/eventbus/controlplane/rpc"
	servicesMocks "code.justin.tv/eventbus/controlplane/services/mocks"
)

type dummyCtxKey string

var (
	dummyServiceID         = 1
	dummyServiceIDAsString = "1"
	dummyName              = "Greatest Service Ever"
	dummyServiceCatalogID  = "111"
	dummyServiceCatalogURL = "https://catalog.xarth.tv/services/111/details"
	dummyDescription       = "This is a cool service that does cool stuff"
	dummyLDAPGroups        = []string{"team-1", "team-2", "team-3", "infra"}
)

func dummyRPCService() *rpc.Service {
	return &rpc.Service{
		// dummy fields
		Name:              dummyName,
		ServiceCatalogUrl: dummyServiceCatalogURL,
		Description:       dummyDescription,
		LdapGroup:         dummyLDAPGroups[0],
	}
}

func adminContext() (context.Context, context.CancelFunc) {
	ctx := context.Background()
	ctx = ldap.WithGroups(ctx, []string{"team-eventbus"})
	ctx = ldap.WithUser(ctx, "admin")
	return context.WithCancel(ctx)
}

func dummyContext(ids ...string) (context.Context, context.CancelFunc) {
	ctx := context.Background()
	ctx = ldap.WithGroups(ctx, dummyLDAPGroups)
	ctx = ldap.WithUser(ctx, "cooluser")
	for _, id := range ids {
		ctx = context.WithValue(ctx, dummyCtxKey(id), id)
	}
	return context.WithCancel(ctx)
}

func serviceMatcher(expected *db.Service) func(*db.Service) bool {
	return func(actual *db.Service) bool {
		if len(actual.Accounts) != len(expected.Accounts) {
			return false
		}
		for i, expectedAcct := range expected.Accounts {
			actualAcct := actual.Accounts[i]
			if actualAcct.AWSAccountID != expectedAcct.AWSAccountID ||
				actualAcct.Label != expectedAcct.Label {
				return false
			}
		}
		if len(actual.IAMRoles) != len(expected.IAMRoles) {
			return false
		}
		for i, expectedRole := range expected.IAMRoles {
			actualRole := actual.IAMRoles[i]
			if !strings.HasPrefix(actualRole.ARN, expectedRole.ARN) ||
				actualRole.Label != expectedRole.Label {
				return false
			}
		}
		return true
	}
}

func duplicateServiceCatalogIDError() error {
	return twirpErrMocks.NewDBErrorFor("DuplicateServiceCatalogID")
}

func notFoundError() error {
	return twirpErrMocks.NewDBErrorFor("ServiceNotFound")
}

type Mocks struct {
	DB                      *dbMocks.DB
	SCClient                *clientMocks.Client
	EncryptionAtRestManager *servicesMocks.EncryptionAtRestManager
}

func setup() (*ServicesService, Mocks) {
	mockDB := &dbMocks.DB{}
	mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)

	mockSCClient := &clientMocks.Client{}
	mockEncryptionAtRestManager := &servicesMocks.EncryptionAtRestManager{}
	// Create a twirp service
	s := &ServicesService{
		DB:                      mockDB,
		ServiceCatalog:          mockSCClient,
		EncryptionAtRestManager: mockEncryptionAtRestManager,
	}

	mockSCClient.On("Get", mock.Anything, mock.Anything).Return(&servicecatalog.Response{
		Name:        "Test Service",
		Description: "Description of Test Service",
	}, nil)

	return s, Mocks{mockDB, mockSCClient, mockEncryptionAtRestManager}
}

func TestServiceTwirp(t *testing.T) {
	ctx, cancel := dummyContext()
	defer cancel()

	dummyDBService := &db.Service{
		ID:               dummyServiceID,
		Name:             dummyName,
		ServiceCatalogID: dummyServiceCatalogID,
		Description:      dummyDescription,
		LDAPGroup:        dummyLDAPGroups[0],
	}

	t.Run("Create", func(t *testing.T) {
		t.Run("HappyPath", func(t *testing.T) {
			t.Run("without accounts or IAM roles", func(t *testing.T) {
				s, allMocks := setup()
				mockDB := allMocks.DB
				req := &rpc.CreateServiceReq{
					Service: dummyRPCService(),
				}
				m := serviceMatcher(&db.Service{})
				mockDB.On("ServiceCreate", mock.Anything, mock.MatchedBy(m)).Return(1, nil)

				resp, err := s.Create(ctx, req)
				require.NoError(t, err)
				assert.Equal(t, req.Service.Name, resp.Service.Name)
				assert.Equal(t, req.Service.Description, resp.Service.Description)
				assert.Equal(t, req.Service.ServiceCatalogUrl, resp.Service.ServiceCatalogUrl)

				mockDB.AssertExpectations(t)
			})
		})
		t.Run("NoLDAPGroup", func(t *testing.T) {
			s, _ := setup()
			req := &rpc.CreateServiceReq{
				Service: dummyRPCService(),
			}
			req.Service.LdapGroup = ""
			_, err := s.Create(ctx, req)
			assert.Error(t, err)
			assert.Equal(t, twirp.InvalidArgument, err.(twirp.Error).Code())
		})
		t.Run("NotInLDAPGroup", func(t *testing.T) {
			s, _ := setup()
			req := &rpc.CreateServiceReq{
				Service: dummyRPCService(),
			}
			req.Service.LdapGroup = "team-user-does-not-belong-to"
			_, err := s.Create(ctx, req)
			assert.Error(t, err)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
	})

	t.Run("Get", func(t *testing.T) {
		t.Run("HappyPath", func(t *testing.T) {
			t.Run("without accounts or IAM roles", func(t *testing.T) {
				s, mocks := setup()
				mockDB := mocks.DB
				req := &rpc.GetServiceReq{

					Id: dummyServiceIDAsString,
				}
				mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)
				mockDB.On("AccountsByServiceID", mock.Anything, dummyServiceID).Return([]*db.Account{}, nil)
				mockDB.On("IAMRolesByServiceID", mock.Anything, dummyServiceID).Return([]*db.IAMRole{}, nil)

				resp, err := s.Get(ctx, req)
				require.NoError(t, err)
				assert.NotNil(t, resp)
				assert.Equal(t, dummyServiceIDAsString, resp.Id)
				assert.Equal(t, dummyServiceCatalogURL, resp.ServiceCatalogUrl)
				assert.Len(t, resp.Accounts, 0)
				assert.Len(t, resp.IamRoles, 0)
			})
			t.Run("with accounts", func(t *testing.T) {

				req := &rpc.GetServiceReq{
					Id: dummyServiceIDAsString,
				}
				accounts := []*db.Account{
					{
						AWSAccountID: "123456789012",
						Label:        "label",
					},
				}
				s, mocks := setup()
				mocks.DB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)
				mocks.DB.On("AccountsByServiceID", mock.Anything, dummyServiceID).Return(accounts, nil)
				mocks.DB.On("IAMRolesByServiceID", mock.Anything, dummyServiceID).Return([]*db.IAMRole{}, nil)

				resp, err := s.Get(ctx, req)
				require.NoError(t, err)
				assert.NotNil(t, resp)
				assert.Equal(t, dummyServiceIDAsString, resp.Id)
				assert.Equal(t, dummyServiceCatalogURL, resp.ServiceCatalogUrl)
				assert.Equal(t, []*rpc.Account{{Id: "123456789012", Label: "label"}}, resp.Accounts)
				assert.Len(t, resp.IamRoles, 0)
			})
			t.Run("with IAM roles", func(t *testing.T) {
				s, mocks := setup()
				mockDB := mocks.DB
				iamRoles := []*db.IAMRole{
					{
						ID:    1,
						ARN:   "arn:aws:iam::123456789012:role/my-service-role",
						Label: "label",
					},
				}
				req := &rpc.GetServiceReq{
					Id: dummyServiceIDAsString,
				}

				mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)
				mockDB.On("AccountsByServiceID", mock.Anything, dummyServiceID).Return([]*db.Account{}, nil)
				mockDB.On("IAMRolesByServiceID", mock.Anything, dummyServiceID).Return(iamRoles, nil)

				resp, err := s.Get(ctx, req)
				require.NoError(t, err)
				assert.NotNil(t, resp)
				assert.Equal(t, dummyServiceIDAsString, resp.Id)
				assert.Equal(t, dummyServiceCatalogURL, resp.ServiceCatalogUrl)
				assert.Equal(t, []*rpc.IAMRole{{Id: "1", Arn: "arn:aws:iam::123456789012:role/my-service-role", Label: "label"}}, resp.IamRoles)
				assert.Len(t, resp.Accounts, 0)
			})

		})
		t.Run("NotInLDAPGroup", func(t *testing.T) {
			s, mocks := setup()

			ctx, _ := dummyContext()
			ctx = ldap.WithGroups(ctx, []string{"team-evil"})
			req := &rpc.GetServiceReq{
				Id: dummyServiceIDAsString,
			}
			mocks.DB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)

			resp, err := s.Get(ctx, req)
			assert.Nil(t, resp)
			assert.Error(t, err)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
		t.Run("NotFound", func(t *testing.T) {
			s, mocks := setup()

			ctx, _ := dummyContext()
			ctx = ldap.WithGroups(ctx, []string{"team-evil"})
			req := &rpc.GetServiceReq{
				Id: dummyServiceIDAsString,
			}
			mocks.DB.On("ServiceByID", mock.Anything, dummyServiceID).Return(nil, notFoundError())

			resp, err := s.Get(ctx, req)
			assert.Nil(t, resp)
			assert.Error(t, err)
			assert.Equal(t, string(twirp.NotFound), string(err.(twirp.Error).Code()))
		})
	})

	t.Run("List", func(t *testing.T) {
		services := []*db.Service{
			{
				Name:      "Service 1",
				LDAPGroup: dummyLDAPGroups[0],
			},
			{
				Name:      "Service 2",
				LDAPGroup: dummyLDAPGroups[0],
			},
			{
				Name:      "Service 3",
				LDAPGroup: "team-other",
			},
		}
		// Listing services should return 2 of 3 (not in last group)
		t.Run("GroupSet1", func(t *testing.T) {
			s, mocks := setup()
			mockDB := mocks.DB
			mockDB.On("ServicesWithIAMRoles", mock.Anything).Return(services, nil)
			mockDB.On("Accounts", mock.Anything).Return([]*db.Account{}, nil)

			resp, err := s.List(ctx, &rpc.ListServicesReq{})
			assert.NoError(t, err)
			assert.NotNil(t, resp.Services)
			assert.Equal(t, 2, len(resp.Services))
		})

		t.Run("GroupSet2", func(t *testing.T) {
			newCtx := ldap.WithGroups(ctx, []string{"team-other"})
			s, mocks := setup()
			mockDB := mocks.DB
			mockDB.On("ServicesWithIAMRoles", mock.Anything).Return(services, nil)
			mockDB.On("Accounts", mock.Anything).Return([]*db.Account{}, nil)

			resp, err := s.List(newCtx, &rpc.ListServicesReq{})
			assert.NoError(t, err)
			assert.NotNil(t, resp.Services)
			assert.Equal(t, 1, len(resp.Services))
		})

		t.Run("ServiceCatalogFails", func(t *testing.T) {
			s, mocks := setup()
			mockDB := mocks.DB
			mockDB.On("ServicesWithIAMRoles", mock.Anything).Return(services, nil)
			mockDB.On("Accounts", mock.Anything).Return([]*db.Account{}, nil)

			newMock := &clientMocks.Client{}
			newMock.On("Get", mock.Anything, mock.Anything).Return(nil, errors.New("failed"))
			s.ServiceCatalog = newMock

			_, err := s.List(ctx, &rpc.ListServicesReq{})
			assert.Error(t, err)
		})
	})

	t.Run("Update", func(t *testing.T) {
		s, mocks := setup()
		mockDB := mocks.DB

		newMock := &clientMocks.Client{}
		newMock.On("Get", mock.Anything, mock.Anything).Return(&servicecatalog.Response{
			Name:        "Test",
			Description: "Description",
		}, nil)
		s.ServiceCatalog = newMock

		t.Run("HappyPath", func(t *testing.T) {
			newName := "Cool new name"
			newURL := "https://catalog.xarth.tv/services/123/details"
			newDesc := "Cool new description"

			req := &rpc.UpdateServiceReq{
				Service: &rpc.Service{
					Id:                dummyServiceIDAsString, // update the service we have been working with
					Name:              newName,
					ServiceCatalogUrl: newURL,
					Description:       newDesc,
				},
			}

			mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)
			mockDB.On("ServiceUpdate", mock.Anything, dummyServiceID, mock.Anything).Return(dummyServiceID, nil)
			mockDB.On("AccountsByServiceID", mock.Anything, dummyServiceID).Return([]*db.Account{}, nil)
			mockDB.On("IAMRolesByServiceID", mock.Anything, dummyServiceID).Return([]*db.IAMRole{}, nil)
			resp, err := s.Update(ctx, req)
			mockDB.AssertCalled(t, "ServiceUpdate", mock.Anything, dummyServiceID, mock.Anything)

			require.NoError(t, err)
			assert.NotNil(t, resp.Service)
			assert.Equal(t, dummyServiceIDAsString, resp.Service.Id)
			assert.Equal(t, newURL, resp.Service.ServiceCatalogUrl)
		})
		t.Run("DeleteIAMRoleNoError", func(t *testing.T) {
			adminCtx, _ := adminContext()
			mockDB = &dbMocks.DB{}
			s := ServicesService{
				DB: mockDB,
			}
			mockDB.On("IAMRoleByARN", mock.Anything, "my-cool-iam-role-arn").Return(&db.IAMRole{ID: 1, ARN: "my-cool-iam-role-arn", Label: "dev", ServiceID: 1}, nil)
			mockDB.On("ServiceByID", mock.Anything, 1).Return(&db.Service{ID: 1, LDAPGroup: "team-service-owner"}, nil)
			mockDB.On("IAMRoleAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
			mockDB.On("IAMRoleReleaseLease", mock.Anything).Return(nil)
			mockDB.On("IAMRoleDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
			mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
			mockDB.On("PublicationsByIAMRoleID", mock.Anything, 1).Return([]*db.Publication{}, nil)
			mockDB.On("AuthorizedFieldPublisherGrantsByIAMRoleID", mock.Anything, 1).Return([]*db.AuthorizedFieldPublisherGrant{}, nil)
			mockDB.On("AuthorizedFieldSubscriberGrantsByIAMRoleID", mock.Anything, 1).Return([]*db.AuthorizedFieldSubscriberGrant{}, nil)
			resp, err := s.DeleteIAMRole(adminCtx, &rpc.DeleteIAMRoleReq{
				Arn: "my-cool-iam-role-arn",
			})
			assert.NoError(t, err)
			assert.NotNil(t, resp)
		})
		t.Run("DeleteIAMRolePermissionDenied", func(t *testing.T) {
			dummyCtx, _ := dummyContext()
			mockDB = &dbMocks.DB{}
			s := ServicesService{
				DB: mockDB,
			}
			mockDB.On("IAMRoleByARN", mock.Anything, "my-cool-iam-role-arn").Return(&db.IAMRole{ID: 1, ARN: "my-cool-iam-role-arn", Label: "dev", ServiceID: 1}, nil)
			mockDB.On("ServiceByID", mock.Anything, 1).Return(&db.Service{ID: 1, LDAPGroup: "team-not-allowed"}, nil)
			mockDB.On("IAMRoleAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
			mockDB.On("IAMRoleReleaseLease", mock.Anything).Return(nil)
			mockDB.On("IAMRoleDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
			mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
			resp, err := s.DeleteIAMRole(dummyCtx, &rpc.DeleteIAMRoleReq{
				Arn: "my-cool-iam-role-arn",
			})
			assert.Error(t, err)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
			assert.Nil(t, resp)
		})
		t.Run("DeleteIAMRoleFailedPreconditions", func(t *testing.T) {
			adminCtx, _ := adminContext()
			mockDB = &dbMocks.DB{}
			s := ServicesService{
				DB: mockDB,
			}
			mockDB.On("IAMRoleByARN", mock.Anything, "my-cool-iam-role-arn").Return(&db.IAMRole{ID: 1, ARN: "my-cool-iam-role-arn", Label: "dev", ServiceID: 1}, nil)
			mockDB.On("ServiceByID", mock.Anything, 1).Return(&db.Service{ID: 1, LDAPGroup: "team-not-allowed"}, nil)
			mockDB.On("IAMRoleAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
			mockDB.On("IAMRoleReleaseLease", mock.Anything).Return(nil)
			mockDB.On("IAMRoleDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
			mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)

			t.Run("PublicationExists", func(t *testing.T) {
				mockDB.On("PublicationsByIAMRoleID", mock.Anything, 1).Return([]*db.Publication{{ID: 1}}, nil)
				resp, err := s.DeleteIAMRole(adminCtx, &rpc.DeleteIAMRoleReq{
					Arn: "my-cool-iam-role-arn",
				})
				assert.Error(t, err)
				assert.Equal(t, twirp.FailedPrecondition, err.(twirp.Error).Code())
				assert.Nil(t, resp)
			})
			t.Run("AuthorizedFieldPublisherGrantExists", func(t *testing.T) {
				mockDB.On("PublicationsByIAMRoleID", mock.Anything, 1).Return([]*db.Publication{}, nil)
				mockDB.On("AuthorizedFieldPublisherGrantsByIAMRoleID", mock.Anything, 1).Return([]*db.AuthorizedFieldPublisherGrant{{ID: 1}}, nil)
				resp, err := s.DeleteIAMRole(adminCtx, &rpc.DeleteIAMRoleReq{
					Arn: "my-cool-iam-role-arn",
				})
				assert.Error(t, err)
				assert.Equal(t, twirp.FailedPrecondition, err.(twirp.Error).Code())
				assert.Nil(t, resp)
			})
			t.Run("AuthorizedFieldSubscriberGrantExists", func(t *testing.T) {
				mockDB.On("PublicationsByIAMRoleID", mock.Anything, 1).Return([]*db.Publication{}, nil)
				mockDB.On("AuthorizedFieldPublisherGrantsByIAMRoleID", mock.Anything, 1).Return([]*db.AuthorizedFieldPublisherGrant{}, nil)
				mockDB.On("AuthorizedFieldSubscriberGrantsByIAMRoleID", mock.Anything, 1).Return([]*db.AuthorizedFieldSubscriberGrant{{ID: 1}}, nil)
				resp, err := s.DeleteIAMRole(adminCtx, &rpc.DeleteIAMRoleReq{
					Arn: "my-cool-iam-role-arn",
				})
				assert.Error(t, err)
				assert.Equal(t, twirp.FailedPrecondition, err.(twirp.Error).Code())
				assert.Nil(t, resp)
			})

		})
		t.Run("DeleteIAMRolesError", func(t *testing.T) {
			adminCtx, _ := adminContext()
			mockDB = &dbMocks.DB{}
			s := ServicesService{
				DB: mockDB,
			}
			mockDB.On("IAMRoleByARN", mock.Anything, mock.Anything).Return(nil, errors.New("OH NOES"))
			mockDB.On("IAMRoleAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
			mockDB.On("IAMRoleReleaseLease", mock.Anything).Return(nil)
			mockDB.On("IAMRoleDelete", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("error"))
			resp, err := s.DeleteIAMRole(adminCtx, &rpc.DeleteIAMRoleReq{})
			assert.Error(t, err)
			assert.Nil(t, resp)
		})
		t.Run("NotInCurrentLDAPGroup", func(t *testing.T) {
			ctx := ldap.WithGroups(context.Background(), []string{"team-evil"})
			newName := "Nefarious Name"
			newURL := "https://catalog.xarth.tv/services/123123123/details"
			newDesc := "Nefarious Description"
			req := &rpc.UpdateServiceReq{
				Service: &rpc.Service{
					Id:                dummyServiceIDAsString, // update the service we have been working with
					Name:              newName,
					ServiceCatalogUrl: newURL,
					Description:       newDesc,
				},
			}
			mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)
			_, err := s.Update(ctx, req)
			assert.Error(t, err)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
		t.Run("NotInDestinationLDAPGroup", func(t *testing.T) {
			newName := "Nefarious Name"
			newURL := "https://catalog.xarth.tv/services/123123213/details"
			newDesc := "Nefarious Description"
			req := &rpc.UpdateServiceReq{
				Service: &rpc.Service{
					Id:                dummyServiceIDAsString, // update the service we have been working with
					Name:              newName,
					ServiceCatalogUrl: newURL,
					Description:       newDesc,
					LdapGroup:         "team-user-does-not-belong-to",
				},
			}
			mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyDBService, nil)
			_, err := s.Update(ctx, req)
			assert.Error(t, err)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
	})

	t.Run("RequestorLDAPGroups", func(t *testing.T) {
		s, _ := setup()
		resp, err := s.RequestorLDAPGroups(ctx, &rpc.RequestorLDAPGroupsReq{})
		assert.NoError(t, err)
		assert.NotEmpty(t, resp.LdapGroups)

		for _, group := range resp.LdapGroups {
			assert.True(t, isAllowedLDAPGroup(group))
		}
	})
}

func TestIsAllowedLDAPGroup(t *testing.T) {
	tests := []struct {
		ldapGroup string
		expected  bool
	}{
		{"team-awesome", true},
		{"team-awesome-2", true},

		{"", false},
		{"ldap", false},
		{"awesome", false},
	}

	for _, test := range tests {
		isAllowed := isAllowedLDAPGroup(test.ldapGroup)
		if test.expected {
			assert.True(t, isAllowed, "expected allowed ldap group %q", test.ldapGroup)
		} else {
			assert.False(t, isAllowed, "expected unallowed ldap group %q", test.ldapGroup)
		}
	}
}

func TestIsValidServiceCatalogURL(t *testing.T) {
	tests := []struct {
		url      string
		expected bool
	}{
		{"https://catalog.xarth.tv/services/1262354253/details", true},
		{"https://catalog.xarth.tv/services/12/details", true},

		{"", false},
		{"http://catalog.xarth.tv/services/63a7/details", false},
		{"http://catalog.xarth.tv/services/-126/details", false},
		{"https://catalog.xarth.tv/services/12/abc/details", false},
		{"https://catalog.xarth.tv/services/12/details/etc", false},
		{"https://catalog.xarth.tv/services/12/%34/details", false},
		{"https://catalog.xarth.tv/services/abc/details", false},
		{"https://www.catalog.xarth.tv/services/abc/details", false},
		{"www.catalog.xarth.tv/services/12/details", false},
		{"https2://catalog.xarth.tv/services/12/details", false},
		{"https:///catalog.xarth.tv/services/1/details", false},
		{"https://1catalog.xarth.tv/services/1/details", false},
		{"https://catalog.2xarth.tv/services/1/details", false},
		{"https://catalog.xarth.tv3/services/1/details", false},
		{"https://catalog.xarth.tv/services/1/d4etails", false},
		{"https:///catalog.xarth.tv/services/foo/7/details", false},
	}

	for _, test := range tests {
		isValid, err := isValidServiceCatalogURL(test.url)
		if err != nil {
			t.Errorf("could not call isValidServiceCatalogURL: %v", err)
		}

		if test.expected {
			assert.True(t, isValid, "expected valid service catalog url %q", test.url)
		} else {
			assert.False(t, isValid, "expected invalid service catalog url %q", test.url)
		}
	}
}

func TestIsValidServiceName(t *testing.T) {
	tests := []struct {
		name     string
		expected bool
	}{
		{"Websub", true},
		{"test_service_1", true},
		{"test_service 2 ", true},
		{strings.Repeat("a", ServiceNameMaxLength-1), true},
		{string([]byte{65, 66, 67}), true},
		{string([]byte{126}), true},

		{"", false},
		{strings.Repeat("a", ServiceNameMaxLength), false},
		{strings.Repeat("b", ServiceNameMaxLength+1), false},
		{strings.Repeat("c", ServiceNameMaxLength*2), false},
		{string([]byte{0, 66, 67}), false},
		{string([]byte{19}), false},
		{string([]byte{127}), false},
	}

	for _, test := range tests {
		if test.expected {
			assert.True(t, isValidServiceName(test.name), "expected valid service name %q", test.name)
		} else {
			assert.False(t, isValidServiceName(test.name), "expected invalid service name %q", test.name)
		}
	}
}

func TestIsValidServiceDescription(t *testing.T) {
	tests := []struct {
		desc     string
		expected bool
	}{
		{"multi\nline service", true},
		{strings.Repeat("a", ServiceDescriptionMaxLength-1), true},
		{string([]byte{65, 66, 67}), true},
		{string([]byte{126, 125, 100, 45, 32, 65, 66, 67, 32, 41}), true},

		{"", false},
		{strings.Repeat("a", ServiceDescriptionMaxLength), false},
		{strings.Repeat("b", ServiceDescriptionMaxLength+1), false},
		{strings.Repeat("c", ServiceDescriptionMaxLength*2), false},
		{string([]byte{19}), false},
		{string([]byte{127}), false},
	}

	for _, test := range tests {
		if test.expected {
			assert.True(t, isValidServiceDescription(test.desc), "expected valid service description %q", test.desc)
		} else {
			assert.False(t, isValidServiceDescription(test.desc), "expected invalid service description %q", test.desc)
		}
	}
}

func TestServiceCatalogURLUniqueness(t *testing.T) {
	s, mocks := setup()
	mockSC := mocks.SCClient
	mockSC.On("Get", mock.Anything, mock.Anything).Return(&servicecatalog.Response{
		Name:        "Test",
		Description: "Description",
	}, nil)

	scURL := "https://catalog.xarth.tv/services/333/details"

	existingService := &db.Service{
		ID:               1234,
		Name:             dummyName,
		ServiceCatalogID: "333",
		Description:      dummyDescription,
		LDAPGroup:        dummyLDAPGroups[0],
	}

	t.Run("Create", func(t *testing.T) {
		t.Run("HappyPath", func(t *testing.T) {
			ctx, _ := dummyContext()
			newService := dummyRPCService()
			newServiceReq := &rpc.CreateServiceReq{
				Service: newService,
			}
			newService.ServiceCatalogUrl = scURL
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceCreate", mock.Anything, mock.Anything).Return(dummyServiceID, nil)
			mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
			resp, err := s.Create(ctx, newServiceReq)

			require.NoError(t, err)
			assert.NotNil(t, resp)

		})
		t.Run("ClientError", func(t *testing.T) {
			ctx, _ := dummyContext()
			newService := dummyRPCService()
			newServiceReq := &rpc.CreateServiceReq{
				Service: newService,
			}
			newService.ServiceCatalogUrl = scURL
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceCreate", mock.Anything, mock.Anything).Return(-1, duplicateServiceCatalogIDError())
			resp, err := s.Create(ctx, newServiceReq)

			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, string(twirp.InvalidArgument), string(err.(twirp.Error).Code()))
		})

		t.Run("ServerError", func(t *testing.T) {
			ctx, _ := dummyContext()
			newService := dummyRPCService()
			newServiceReq := &rpc.CreateServiceReq{
				Service: newService,
			}
			newService.ServiceCatalogUrl = scURL
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceCreate", mock.Anything, mock.Anything).Return(-1, errors.New("unexpected"))
			resp, err := s.Create(ctx, newServiceReq)

			require.Error(t, err)
			assert.Nil(t, resp)
			require.NotPanics(t, func() { _ = err.(twirp.Error) })
			twirpErr := err.(twirp.Error)
			assert.Equal(t, string(twirp.Internal), string(twirpErr.Code()))
		})
	})

	t.Run("Update", func(t *testing.T) {
		t.Run("HappyPath", func(t *testing.T) {
			ctx, _ := dummyContext()
			service := dummyRPCService()
			updateServiceReq := &rpc.UpdateServiceReq{
				Service: service,
			}
			service.Id = strconv.Itoa(existingService.ID)
			service.ServiceCatalogUrl = scURL
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceByID", ctx, existingService.ID).Return(existingService, nil)
			mockDB.On("ServiceUpdate", mock.Anything, mock.Anything, mock.Anything).Return(dummyServiceID, nil)
			mockDB.On("AccountsByServiceID", mock.Anything, existingService.ID).Return([]*db.Account{}, nil)
			mockDB.On("IAMRolesByServiceID", mock.Anything, existingService.ID).Return([]*db.IAMRole{}, nil)
			resp, err := s.Update(ctx, updateServiceReq)

			require.NoError(t, err)
			assert.NotNil(t, resp)
		})
		t.Run("ClientError", func(t *testing.T) {
			ctx, _ := dummyContext()
			service := dummyRPCService()
			updateServiceReq := &rpc.UpdateServiceReq{
				Service: service,
			}
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceUpdate", mock.Anything, mock.Anything).Return(-1, duplicateServiceCatalogIDError())
			resp, err := s.Update(ctx, updateServiceReq)

			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.InvalidArgument, err.(twirp.Error).Code())
		})
		t.Run("ServerError", func(t *testing.T) {
			ctx, _ := dummyContext()
			service := dummyRPCService()
			updateServiceReq := &rpc.UpdateServiceReq{
				Service: service,
			}
			service.Id = strconv.Itoa(existingService.ID)
			service.ServiceCatalogUrl = scURL
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceUpdate", mock.Anything, mock.Anything).Return(-1, errors.New("unexpected"))
			mockDB.On("ServiceByID", ctx, existingService.ID).Return(nil, errors.New("uh oh spaghettios"))
			resp, err := s.Update(ctx, updateServiceReq)
			mockDB.AssertCalled(t, "ServiceByID", mock.Anything, existingService.ID)

			require.Error(t, err)
			assert.Nil(t, resp)
			require.NotPanics(t, func() { _ = err.(twirp.Error) })
			twirpErr := err.(twirp.Error)
			assert.Equal(t, string(twirp.Internal), string(twirpErr.Code()))
		})
	})
}

func TestCreateIAMRole(t *testing.T) {
	s, mocks := setup()
	mockSC := mocks.SCClient
	mockSC.On("Get", mock.Anything, mock.Anything).Return(&servicecatalog.Response{
		Name:        "Test",
		Description: "Description",
	}, nil)
	mocks.EncryptionAtRestManager.On("GrantEncryptionAtRest", mock.Anything, mock.Anything).Return("the-grant-id", nil)

	existingService := &db.Service{
		ID:               1234,
		Name:             dummyName,
		ServiceCatalogID: "333",
		Description:      dummyDescription,
		LDAPGroup:        dummyLDAPGroups[0],
	}

	t.Run("Create", func(t *testing.T) {
		t.Run("HappyPath", func(t *testing.T) {
			ctx, _ := dummyContext()
			req := &rpc.CreateIAMRoleReq{
				ServiceId: "1234",
				IamRole: &rpc.IAMRole{
					Arn:   "arn:aws:iam::111111111111:role/my-service-role",
					Label: "my-label",
				},
			}
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceByID", ctx, 1234).Return(existingService, nil)
			mockDB.On("IAMRoleCreate", mock.Anything, mock.Anything).Return(1, nil)
			mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
			resp, err := s.CreateIAMRole(ctx, req)
			mockDB.AssertCalled(t, "ServiceByID", ctx, 1234)
			mockDB.AssertCalled(t, "IAMRoleCreate", ctx, mock.Anything)

			assert.Equal(t, "arn:aws:iam::111111111111:role/my-service-role", resp.GetIamRole().GetArn())
			assert.Equal(t, "my-label", resp.GetIamRole().GetLabel())

			require.NoError(t, err)
			assert.NotNil(t, resp)
		})

		t.Run("InternalError", func(t *testing.T) {
			ctx, _ := dummyContext()
			req := &rpc.CreateIAMRoleReq{
				ServiceId: "1234",
				IamRole: &rpc.IAMRole{
					Arn:   "arn:aws:iam::111111111111:role/my-service-role",
					Label: "my-label",
				},
			}
			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceByID", ctx, 1234).Return(nil, errors.New("unexpected error"))
			resp, err := s.CreateIAMRole(ctx, req)
			mockDB.AssertCalled(t, "ServiceByID", ctx, 1234)

			assert.Nil(t, resp)
			assert.Error(t, err)
		})

		t.Run("InvalidArgument", func(t *testing.T) {
			ctx, _ := dummyContext()
			req := &rpc.CreateIAMRoleReq{
				ServiceId: "1234",
				IamRole: &rpc.IAMRole{
					Arn:   "arn:aws:iam::111111111111:role/my-service-role",
					Label: "my-label",
				},
			}
			mockErr := twirpErrMocks.NewDBErrorFor("DuplicateIAMRoleARN")

			mockDB := &dbMocks.DB{}
			s.DB = mockDB
			mockDB.On("ServiceByID", ctx, 1234).Return(existingService, nil)
			mockDB.On("IAMRoleCreate", mock.Anything, mock.Anything).Return(-1, mockErr)
			resp, err := s.CreateIAMRole(ctx, req)
			mockDB.AssertCalled(t, "ServiceByID", ctx, 1234)
			mockDB.AssertCalled(t, "IAMRoleCreate", ctx, mock.Anything)

			assert.Nil(t, resp)
			assert.Error(t, err)
			assert.Equal(t, twirp.InvalidArgument, err.(twirp.Error).Code())

		})
	})
}

func TestUpdateIAMRoleLabel(t *testing.T) {
	s, mocks := setup()
	mockSC := mocks.SCClient
	mockSC.On("Get", mock.Anything, mock.Anything).Return(&servicecatalog.Response{
		Name:        "Test",
		Description: "Description",
	}, nil)

	existingService := &db.Service{
		ID:               1234,
		Name:             dummyName,
		ServiceCatalogID: "333",
		Description:      dummyDescription,
		LDAPGroup:        dummyLDAPGroups[0],
	}

	existingIAMRole := &db.IAMRole{
		ID:        456,
		ServiceID: 1234,
		Label:     "old-label",
		ARN:       "arn:aws:iam::111111111111:role/my-service-role",
	}

	ctx, _ := dummyContext()
	req := &rpc.UpdateIAMRoleLabelReq{
		Arn:   "arn:aws:iam::111111111111:role/my-service-role",
		Label: "new-label",
	}
	mockDB := &dbMocks.DB{}
	s.DB = mockDB
	mockDB.On("IAMRoleByARN", ctx, mock.Anything).Return(existingIAMRole, nil)
	mockDB.On("ServiceByID", ctx, 1234).Return(existingService, nil)
	mockDB.On("IAMRoleUpdate", mock.Anything, mock.Anything, mock.Anything).Return(1, nil)
	resp, err := s.UpdateIAMRoleLabel(ctx, req)
	mockDB.AssertCalled(t, "ServiceByID", ctx, 1234)
	mockDB.AssertCalled(t, "IAMRoleUpdate", ctx, existingIAMRole.ID, mock.Anything)

	assert.Equal(t, "arn:aws:iam::111111111111:role/my-service-role", resp.GetArn())
	assert.Equal(t, "new-label", resp.GetLabel())

	require.NoError(t, err)
	assert.NotNil(t, resp)
}

func TestValidateIAMRole(t *testing.T) {
	s, mocks := setup()
	mockSC := mocks.SCClient
	mockSC.On("Get", mock.Anything, mock.Anything).Return(&servicecatalog.Response{
		Name:        "Test",
		Description: "Description",
	}, nil)

	existingService := &db.Service{
		ID:               1234,
		Name:             dummyName,
		ServiceCatalogID: "333",
		Description:      dummyDescription,
		LDAPGroup:        dummyLDAPGroups[0],
	}

	existingIAMRole := &db.IAMRole{
		ID:         456,
		ServiceID:  1234,
		Label:      "old-label",
		ARN:        "arn:aws:iam::111111111111:role/my-service-role",
		KMSGrantID: "abc",
	}

	mocks.DB.On("IAMRoleByARN", mock.Anything, existingIAMRole.ARN).Return(existingIAMRole, nil)
	mocks.DB.On("ServiceByID", mock.Anything, existingIAMRole.ServiceID).Return(existingService, nil)

	t.Run("HappyPath", func(t *testing.T) {
		ctx, _ := dummyContext("grant-correct")
		req := &rpc.ValidateIAMRoleReq{
			Arn: existingIAMRole.ARN,
		}

		t.Run("Passes validation", func(t *testing.T) {
			existingGrant := &kms.GrantListEntry{
				GrantId:          aws.String("abc"),
				GranteePrincipal: aws.String("arn:aws:iam::111111111111:root"),
			}
			mocks.EncryptionAtRestManager.On("FindByGrantID", ctx, existingIAMRole.KMSGrantID).Return(existingGrant, nil)

			resp, err := s.ValidateIAMRole(ctx, req)
			require.NoError(t, err)
			require.Equal(t, "", resp.GetMessage())
			require.True(t, resp.GetIsValid())
		})

		t.Run("Fails validation", func(t *testing.T) {
			t.Run("No grant found", func(t *testing.T) {
				ctx, _ := dummyContext("grant-not-found")
				mocks.EncryptionAtRestManager.On("FindByGrantID", ctx, existingIAMRole.KMSGrantID).Return(nil, nil)

				resp, err := s.ValidateIAMRole(ctx, req)
				require.NoError(t, err)
				require.NotEmpty(t, resp.GetMessage())
				require.False(t, resp.GetIsValid())
			})

			t.Run("Grant not correct", func(t *testing.T) {
				ctx, _ := dummyContext("grant-incorrect")
				existingGrant := &kms.GrantListEntry{
					GrantId:          aws.String("abc"),
					GranteePrincipal: aws.String("totally-not-what-you-were-expecting"),
				}
				mocks.EncryptionAtRestManager.On("FindByGrantID", ctx, existingIAMRole.KMSGrantID).Return(existingGrant, nil)

				resp, err := s.ValidateIAMRole(ctx, req)
				require.NoError(t, err)
				require.NotEmpty(t, resp.GetMessage())
				require.False(t, resp.GetIsValid())
			})
		})
	})
}
