package targets

import (
	"context"
	"strings"
	"testing"

	"code.justin.tv/eventbus/controlplane/rpc"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/twitchtv/twirp"

	"code.justin.tv/eventbus/controlplane/internal/db"
	dbMocks "code.justin.tv/eventbus/controlplane/internal/db/mocks"
	"code.justin.tv/eventbus/controlplane/internal/db/postgres"
	"code.justin.tv/eventbus/controlplane/internal/ldap"
	twirperrMocks "code.justin.tv/eventbus/controlplane/internal/twirperr/mocks"
)

// This test can be thought of as a sort of integration test
// sitting on top of the Twirp API --> DB storage stack
//
// This test will go through the motions of using the Twirp client
// to perform CRUD operations on services that will persist in the DB

const (
	dummyServiceID         = 1
	dummyServiceIDAsString = "1"
)

func dummyContext() (context.Context, context.CancelFunc) {
	ctx := context.Background()
	ctx = ldap.WithUser(ctx, "bob")
	ctx = ldap.WithGroups(ctx, []string{"the", "builders"})
	return context.WithCancel(ctx)
}

func serviceNotFoundError() error {
	return twirperrMocks.NewDBErrorFor("ServiceNotFound")
}

func TestTargetTwirp(t *testing.T) {
	mockDB := &dbMocks.DB{}

	s := TargetsService{
		DB: mockDB,
	}

	ctx, cancel := dummyContext()
	defer cancel()

	dummyService := &db.Service{
		LDAPGroup: "builders",
	}

	t.Run("Create", func(t *testing.T) {
		t.Run("AccessDenied", func(t *testing.T) {
			badCtx := ldap.WithGroups(ldap.WithUser(context.Background(), "malice"), []string{"evil", "users"})
			req := &rpc.CreateTargetReq{
				Name:      "EVIL Target",
				ServiceId: dummyServiceIDAsString,
				Type:      rpc.TargetType_TARGET_TYPE_SQS,
				Details: &rpc.CreateTargetReq_Sqs{
					Sqs: &rpc.SQSDetails{
						QueueUrl: "https://sqs.test-region.amazonaws.com/111122223333/test",
					},
				},
			}
			mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyService, nil)
			mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
			resp, err := s.Create(badCtx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})

		t.Run("InvalidName", func(t *testing.T) {
			req := &rpc.CreateTargetReq{
				Name:      "Hello, 世界",
				ServiceId: dummyServiceIDAsString,
				Type:      rpc.TargetType_TARGET_TYPE_SQS,
				Details: &rpc.CreateTargetReq_Sqs{
					Sqs: &rpc.SQSDetails{
						QueueUrl: "https://sqs.test-region.amazonaws.com/111122223333/test",
					},
				},
			}

			resp, err := s.Create(ctx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.InvalidArgument, err.(twirp.Error).Code())
		})

		t.Run("MissingSQSQueueURL", func(t *testing.T) {
			req := &rpc.CreateTargetReq{
				Name:      "Target",
				ServiceId: dummyServiceIDAsString,
				Type:      rpc.TargetType_TARGET_TYPE_SQS,
				Details: &rpc.CreateTargetReq_Sqs{
					Sqs: &rpc.SQSDetails{},
				},
			}

			resp, err := s.Create(ctx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.InvalidArgument, err.(twirp.Error).Code())
		})

		t.Run("InvalidSQSQueueURL", func(t *testing.T) {
			req := &rpc.CreateTargetReq{
				Name:      "Target",
				ServiceId: dummyServiceIDAsString,
				Type:      rpc.TargetType_TARGET_TYPE_SQS,
				Details: &rpc.CreateTargetReq_Sqs{
					Sqs: &rpc.SQSDetails{
						QueueUrl: "!! https://sqs.test-region.amazonaws.com/111122223333/test",
					},
				},
			}

			resp, err := s.Create(ctx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.InvalidArgument, err.(twirp.Error).Code())
		})
	})

	// We previously created some targets for a service, now we should be able to get them
	t.Run("GetTargetsForService", func(t *testing.T) {
		t.Run("ServiceHasTargets", func(t *testing.T) {
			req := &rpc.GetTargetsForServiceReq{
				ServiceId: dummyServiceIDAsString,
			}
			mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyService, nil)
			mockDB.On("SubscriptionTargetsByServiceID", mock.Anything, mock.Anything).Return([]*db.SubscriptionTarget{
				{
					ID: 1,
					SQSDetails: db.SQSDetails{
						SQSQueueARN: "arn:aws:sqs:test-region:111122223333:test",
						SQSQueueURL: "https://sqs.test-region.amazonaws.com/111122223333/test",
					},
				},
				{
					ID: 2,
					SQSDetails: db.SQSDetails{
						SQSQueueARN: "arn:aws:sqs:test-region:111122223333:test",
						SQSQueueURL: "https://sqs.test-region.amazonaws.com/111122223333/test",
					},
				},
			}, nil)
			resp, err := s.GetTargetsForService(ctx, req)
			require.NoError(t, err)
			assert.NotNil(t, resp)
			assert.Equal(t, 2, len(resp.Targets))
		})

		t.Run("ServiceNotFound", func(t *testing.T) {
			req := &rpc.GetTargetsForServiceReq{
				ServiceId: "-1",
			}
			mockDB.On("ServiceByID", mock.Anything, -1).Return(nil, serviceNotFoundError())
			resp, err := s.GetTargetsForService(ctx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.NotFound, err.(twirp.Error).Code())
		})

		t.Run("AccessDenied", func(t *testing.T) {
			badCtx := ldap.WithGroups(ldap.WithUser(context.Background(), "malice"), []string{"evil", "users"})
			req := &rpc.GetTargetsForServiceReq{
				ServiceId: dummyServiceIDAsString,
			}
			mockDB.On("ServiceByID", mock.Anything, dummyServiceID).Return(dummyService, nil)
			resp, err := s.GetTargetsForService(badCtx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
	})
}

func TestTargetDeleteTwirp(t *testing.T) {
	t.Run("DeleteNoSubs", func(t *testing.T) {
		mockDB := &dbMocks.DB{}
		s := TargetsService{
			DB: mockDB,
		}
		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{}, nil)
		mockDB.On("SubscriptionTargetAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionTargetReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionTargetDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(&db.SubscriptionTarget{}, nil)
		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
		res, err := s.Delete(context.Background(), &rpc.DeleteTargetReq{
			SubscriptionTargetId: "1",
		})
		assert.NoError(t, err)
		assert.Equal(t, "1", res.GetSubscriptionTargetId())
	})

	t.Run("DeleteWithSubs", func(t *testing.T) {
		mockDB := &dbMocks.DB{}
		s := TargetsService{
			DB: mockDB,
		}
		mockDB.On("SubscriptionTargetAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionTargetReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{
			{
				ID:                 1,
				SNSSubscriptionARN: "arn-indicates-sub-still-exists",
			},
		}, nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(&db.SubscriptionTarget{}, nil)
		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
		res, err := s.Delete(context.Background(), &rpc.DeleteTargetReq{
			SubscriptionTargetId: "1",
		})
		assert.Error(t, err)
		assert.Nil(t, res)
	})

	t.Run("DeleteWithDisabledSubs", func(t *testing.T) {
		mockDB := &dbMocks.DB{}
		s := TargetsService{
			DB: mockDB,
		}
		mockDB.On("SubscriptionTargetAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionTargetReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("EventStreamByID", mock.Anything, mock.Anything).Return(EventStreamByIDReturnID, nil)
		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{
			{
				ID:                 1,
				SNSSubscriptionARN: "", //empty
			},
			{
				ID:                 2,
				SNSSubscriptionARN: "", // empty
			},
		}, nil)
		mockDB.On("SubscriptionTargetDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(&db.SubscriptionTarget{}, nil)
		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
		res, err := s.Delete(context.Background(), &rpc.DeleteTargetReq{
			SubscriptionTargetId: "1",
		})
		assert.NoError(t, err)
		assert.Equal(t, "1", res.GetSubscriptionTargetId())
	})

	t.Run("DeleteWithPendingDeletedSubsOK", func(t *testing.T) {
		mockDB := &dbMocks.DB{}
		s := TargetsService{
			DB: mockDB,
		}
		mockDB.On("SubscriptionTargetAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionTargetReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("EventStreamByID", mock.Anything, mock.Anything).Return(EventStreamByIDReturnID, nil)
		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{
			{
				ID:                 1,
				Status:             rpc.SubscriptionStatus_SUBSCRIPTION_STATUS_PENDING_DELETE.String(),
				SNSSubscriptionARN: "",
			},
			{
				ID:                 2,
				Status:             rpc.SubscriptionStatus_SUBSCRIPTION_STATUS_PENDING_DELETE.String(),
				SNSSubscriptionARN: "",
			},
		}, nil)
		mockDB.On("SubscriptionTargetDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(&db.SubscriptionTarget{}, nil)
		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
		res, err := s.Delete(context.Background(), &rpc.DeleteTargetReq{
			SubscriptionTargetId: "1",
		})
		assert.NoError(t, err)
		assert.Equal(t, "1", res.GetSubscriptionTargetId())
	})

	t.Run("DeleteWithPendingDeletedSubsNotOK", func(t *testing.T) {
		mockDB := &dbMocks.DB{}
		s := TargetsService{
			DB: mockDB,
		}
		mockDB.On("EventStreamByID", mock.Anything, mock.Anything).Return(EventStreamByIDReturnID, nil)
		mockDB.On("SubscriptionTargetAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionTargetReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{
			{
				ID:                 1,
				Status:             rpc.SubscriptionStatus_SUBSCRIPTION_STATUS_PENDING_DELETE.String(),
				SNSSubscriptionARN: "arn:something:something",
			},
			{
				ID:                 2,
				Status:             rpc.SubscriptionStatus_SUBSCRIPTION_STATUS_PENDING_DELETE.String(),
				SNSSubscriptionARN: "",
			},
		}, nil)
		mockDB.On("SubscriptionTargetDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(&db.SubscriptionTarget{}, nil)
		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
		res, err := s.Delete(context.Background(), &rpc.DeleteTargetReq{
			SubscriptionTargetId: "1",
		})
		assert.Nil(t, res)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "with active subscriptions")
	})

	t.Run("DeleteWithMixedSubs", func(t *testing.T) {
		mockDB := &dbMocks.DB{}
		s := TargetsService{
			DB: mockDB,
		}
		mockDB.On("SubscriptionTargetAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionTargetReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(&postgres.PostgresAWSLease{}, context.Background(), nil)
		mockDB.On("SubscriptionReleaseLease", mock.Anything).Return(nil)
		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{
			{
				ID:                 1,
				SNSSubscriptionARN: "", // empty
			},
			{
				ID:                 2,
				SNSSubscriptionARN: "still-exists",
			},
		}, nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(&db.SubscriptionTarget{}, nil)
		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
		res, err := s.Delete(context.Background(), &rpc.DeleteTargetReq{
			SubscriptionTargetId: "1",
		})
		assert.Error(t, err)
		assert.Nil(t, res)
	})
}

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

		{"", false},
		{strings.Repeat("a", TargetNameMaxLength), false},
		{strings.Repeat("b", TargetNameMaxLength+1), false},
		{strings.Repeat("c", TargetNameMaxLength*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, isValidTargetName(test.name), "expected valid subscription target name %q", test.name)
		} else {
			assert.False(t, isValidTargetName(test.name), "expected invalid subscription target name %q", test.name)
		}
	}
}

func TestIsTargetRoleARN(t *testing.T) {
	tests := []struct {
		arn      string
		expected bool
	}{
		{"arn:aws:iam::012345678912:eventbus/eventbus-arn", true},
		{"arn:aws:iam::1234234:eventbus/eventbus-arn-2", true},

		{"", false},
		{"arn:aws:iam::definitely-not-an-arn", false},
		{"arn1:aws:iam::123:eventbus/eventbus-arn", false},
		{"arn:aws1:iam::123:eventbus/eventbus-arn", false},
		{"arn:aws:iam1::123:eventbus/eventbus-arn", false},
		{"arn:aws:iam::1a23:eventbus/eventbus-arn", false},
		{"arn:aws:iam::123:eventbus", false},
	}

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

		if test.expected {
			assert.True(t, isValid, "expected valid iam role arn %q", test.arn)
		} else {
			assert.False(t, isValid, "expected invalid iam role arn %q", test.arn)
		}
	}
}

func EventStreamByIDReturnID(ctx context.Context, id int) *db.EventStream {
	return &db.EventStream{ID: id}
}
