package subscriptions

import (
	"context"
	"strconv"
	"testing"

	"code.justin.tv/eventbus/controlplane/internal/db"
	dbMocks "code.justin.tv/eventbus/controlplane/internal/db/mocks"
	"code.justin.tv/eventbus/controlplane/internal/environment"
	"code.justin.tv/eventbus/controlplane/internal/ldap"
	"code.justin.tv/eventbus/controlplane/rpc"
	"code.justin.tv/eventbus/controlplane/subscriptions/mocks"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/twitchtv/twirp"
)

// 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 subscriptions that will persist in the DB

const (
	dummyServiceName = "MyService"
	dummyEventType1  = "CoolEvent1"
	dummyEventType2  = "CoolEvent2"
	dummyTargetID1   = "1"
	dummyTargetID2   = "2"
	dummyTargetID3   = "3"
)

func goodUserContext() (context.Context, context.CancelFunc) {
	ctx := context.Background()
	ctx = ldap.WithUser(ctx, "master-chief")
	ctx = ldap.WithGroups(ctx, []string{"cortana-admins", "spartans", "grunt-slayers"})
	return context.WithCancel(ctx)
}

func badUserContext() (context.Context, context.CancelFunc) {
	ctx := context.Background()
	ctx = ldap.WithUser(ctx, "elite")
	ctx = ldap.WithGroups(ctx, []string{"needler-enthusiasts", "team-covenant", "grunt-protectors"})
	return context.WithCancel(ctx)
}

func TestSubscriptionsAPITwirp(t *testing.T) {

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

	eventStream1 := &db.EventStream{
		ID: 1,
		EventType: db.EventType{
			Name: dummyEventType1,
		},
		Environment: environment.Development,
	}
	eventStream2 := &db.EventStream{
		ID: 1,
		EventType: db.EventType{
			Name: dummyEventType2,
		},
		Environment: environment.Development,
	}

	dummyService := &db.Service{
		ID:        1,
		Name:      dummyServiceName,
		LDAPGroup: "grunt-slayers",
	}

	dummyTarget1 := &db.SubscriptionTarget{
		ID:        1,
		Name:      "T1",
		ServiceID: 1,
		SQSDetails: db.SQSDetails{
			SQSQueueARN: "arn:aws:sqs::123456789012:dummy-queue-1",
		},
	}

	dummyTarget2 := &db.SubscriptionTarget{
		ID:        2,
		Name:      "T2",
		ServiceID: 1,
		SQSDetails: db.SQSDetails{
			SQSQueueARN: "arn:aws:sqs::123456789012:dummy-queue-2",
		},
	}

	dummyTarget3 := &db.SubscriptionTarget{
		ID:        3,
		Name:      "T3",
		ServiceID: 1,
		SQSDetails: db.SQSDetails{
			SQSQueueARN: "arn:aws:sqs::123456789012:dummy-queue-3",
		},
	}

	dummySub1 := &db.Subscription{
		ID:                   1,
		SubscriptionTargetID: dummyTarget1.ID,
		EventStreamID:        eventStream1.ID,
	}
	dummySub2 := &db.Subscription{
		ID:                   3,
		SubscriptionTargetID: dummyTarget3.ID,
		EventStreamID:        eventStream1.ID,
		SNSSubscriptionARN:   "arn:aws:snss::123456789012:dummy-susbcription-2",
	}

	dummyLease := &dbMocks.AWSLease{}

	addDBMocks := func(mockDB *dbMocks.DB) {
		mockDB.On("EventStreamByNameAndEnvironment", mock.Anything, dummyEventType1, environment.Development).Return(eventStream1, nil)
		mockDB.On("EventStreamByNameAndEnvironment", mock.Anything, dummyEventType2, environment.Development).Return(eventStream2, nil)
		mockDB.On("ServiceByID", mock.Anything, 1).Return(dummyService, nil)

		mockDB.On("SubscriptionTargetByID", mock.Anything, 1).Return(dummyTarget1, nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, 2).Return(dummyTarget2, nil)
		mockDB.On("SubscriptionTargetByID", mock.Anything, 3).Return(dummyTarget3, nil)

		mockDB.On("SubscriptionCreate", mock.Anything, mock.Anything).Return(dummySub1.ID, nil)
		mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything).Return(nil)

		mockDB.On("SubscriptionsBySubscriptionTargetID", mock.Anything, mock.Anything).Return([]*db.Subscription{dummySub1, dummySub2}, nil)

		mockDB.On("SubscriptionUpdateInfra", mock.Anything, dummyLease, mock.Anything, mock.Anything).Return(0, nil)

		mockDB.On("EventStreamByID", mock.Anything, 1).Return(eventStream1, nil)
		mockDB.On("EventStreamByID", mock.Anything, 2).Return(eventStream2, nil)

		mockDB.On("EventStreamAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(dummyLease, context.Background(), nil)
		mockDB.On("SubscriptionAcquireLease", mock.Anything, mock.Anything, mock.Anything).Return(dummyLease, context.Background(), nil)

		mockDB.On("EventStreamReleaseLease", dummyLease).Return(nil)
		mockDB.On("SubscriptionReleaseLease", dummyLease).Return(nil)

		mockDB.On("AuditLogCreate", mock.Anything, mock.Anything).Return(1, nil)
	}

	setup := func() (*SubscriptionsService, *dbMocks.DB, *mocks.SNSManager) {
		mockDB := &dbMocks.DB{}
		addDBMocks(mockDB)
		mockSNS := &mocks.SNSManager{}
		s := &SubscriptionsService{
			DB:         mockDB,
			SNSManager: mockSNS,
		}
		return s, mockDB, mockSNS
	}

	t.Run("Create", func(t *testing.T) {
		t.Run("HappyPath", func(t *testing.T) {
			t.Run("NewSubscription", func(t *testing.T) {
				s, mockDB, mockSNS := setup()
				mockDB.On("SubscriptionByEventStreamIDAndSubscriptionTargetID", mock.Anything, mock.Anything, mock.Anything).Return(nil, db.NewNotFoundError("SubscriptionTarget", 2))
				mockSNS.On("SubscriptionExists", mock.Anything, mock.Anything, mock.Anything).Return(false, nil)
				mockSNS.On("AllowAccountSubscribe", mock.Anything, mock.Anything, mock.Anything).Return(nil)
				mockSNS.On("Subscribe", mock.Anything, mock.Anything, mock.Anything).Return("sub-arn", nil)
				subReq := &rpc.CreateSubscriptionReq{
					TargetId:    dummyTargetID1,
					EventType:   dummyEventType1,
					Environment: environment.Development,
				}
				resp, err := s.Create(ctx, subReq)
				require.NoError(t, err)
				assert.Equal(t, dummyTargetID1, resp.Subscription.TargetId)
				assert.Equal(t, dummyEventType1, resp.Subscription.EventType)
				assert.Equal(t, environment.Development, resp.Subscription.Environment)
			})

			t.Run("SubscriptionRecordExists", func(t *testing.T) {
				s, mockDB, mockSNS := setup()
				existingSub := &db.Subscription{
					ID:                   4,
					SubscriptionTargetID: dummyTarget3.ID,
					EventStreamID:        eventStream1.ID,
					SNSSubscriptionARN:   "sub-arn",
				}
				mockDB.On("SubscriptionByEventStreamIDAndSubscriptionTargetID", mock.Anything, mock.Anything, mock.Anything).Return(existingSub, nil)
				mockSNS.On("SubscriptionExists", mock.Anything, mock.Anything, existingSub.SNSSubscriptionARN).Return(false, nil)
				mockSNS.On("AllowAccountSubscribe", mock.Anything, mock.Anything, mock.Anything).Return(nil)
				mockSNS.On("Subscribe", mock.Anything, mock.Anything, mock.Anything).Return("sub-arn", nil)
				subReq := &rpc.CreateSubscriptionReq{
					TargetId:    dummyTargetID3,
					EventType:   dummyEventType1,
					Environment: environment.Development,
				}
				resp, err := s.Create(ctx, subReq)
				require.NoError(t, err)
				assert.Equal(t, dummyTargetID3, resp.Subscription.TargetId)
				assert.Equal(t, dummyEventType1, resp.Subscription.EventType)
				assert.Equal(t, environment.Development, resp.Subscription.Environment)
				mockDB.AssertNotCalled(t, "SubscriptionCreate")
			})

			t.Run("SubscriptionInfraExists", func(t *testing.T) {
				s, mockDB, mockSNS := setup()
				existingSub := &db.Subscription{
					ID:                   4,
					SubscriptionTargetID: dummyTarget3.ID,
					EventStreamID:        eventStream1.ID,
					SNSSubscriptionARN:   "sub-arn",
				}
				mockDB.On("SubscriptionByEventStreamIDAndSubscriptionTargetID", mock.Anything, mock.Anything, mock.Anything).Return(existingSub, nil)
				mockSNS.On("SubscriptionExists", mock.Anything, mock.Anything, existingSub.SNSSubscriptionARN).Return(true, nil)
				subReq := &rpc.CreateSubscriptionReq{
					TargetId:    dummyTargetID3,
					EventType:   dummyEventType1,
					Environment: environment.Development,
				}
				resp, err := s.Create(ctx, subReq)
				require.NoError(t, err)
				assert.Equal(t, dummyTargetID3, resp.Subscription.TargetId)
				assert.Equal(t, dummyEventType1, resp.Subscription.EventType)
				assert.Equal(t, environment.Development, resp.Subscription.Environment)
				mockSNS.AssertNotCalled(t, "AllowAccountSubscribe")
				mockSNS.AssertNotCalled(t, "Subscribe")
			})
		})
		t.Run("AccessDenied", func(t *testing.T) {
			s, mockDB, _ := setup()
			mockDB.On("SubscriptionCreate", mock.Anything, mock.Anything).Return(dummySub2.ID, nil)
			badCtx, _ := badUserContext()
			subReq := &rpc.CreateSubscriptionReq{
				TargetId:    dummyTargetID3,
				EventType:   dummyEventType2,
				Environment: environment.Development,
			}
			resp, err := s.Create(badCtx, subReq)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
			mockDB.AssertNotCalled(t, "SubscriptionCreate")
		})

	})

	t.Run("GetSubscriptionsForTarget", func(t *testing.T) {
		t.Run("TargetHasSubscriptions", func(t *testing.T) {
			s, _, _ := setup()
			req := &rpc.GetSubscriptionsForTargetReq{
				TargetId: dummyTargetID1,
			}
			resp, err := s.GetSubscriptionsForTarget(ctx, req)
			require.NoError(t, err)
			assert.NotNil(t, resp)
			assert.Equal(t, 2, len(resp.Subscriptions))
		})

		t.Run("AccessDenied", func(t *testing.T) {
			s, _, _ := setup()
			badCtx, _ := badUserContext()
			req := &rpc.GetSubscriptionsForTargetReq{
				TargetId: strconv.Itoa(dummyTarget3.ID),
			}
			resp, err := s.GetSubscriptionsForTarget(badCtx, req)
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
	})

	t.Run("Delete", func(t *testing.T) {

		t.Run("HappyPath", func(t *testing.T) {
			t.Run("RecordAndInfraExists", func(t *testing.T) {
				s, mockDB, mockSNS := setup()
				existingSub := &db.Subscription{
					ID:                 5,
					SNSSubscriptionARN: "arn:aws:snss::123456789012:dummy-susbcription-2",
				}
				mockDB.On("SubscriptionByEventStreamIDAndSubscriptionTargetID", mock.Anything, mock.Anything, mock.Anything).Return(existingSub, nil)
				mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
				mockSNS.On("SubscriptionExists", mock.Anything, mock.Anything, existingSub.SNSSubscriptionARN).Return(true, nil)
				mockSNS.On("Unsubscribe", mock.Anything, mock.Anything, existingSub.SNSSubscriptionARN).Return(nil)
				mockSNS.On("DisallowAccountSubscribe", mock.Anything, mock.Anything, mock.Anything).Return(nil)

				resp, err := s.Delete(ctx, &rpc.DeleteSubscriptionReq{
					TargetId:    dummyTargetID3,
					EventType:   dummyEventType2,
					Environment: environment.Development,
				})
				assert.NotNil(t, resp)
				assert.NoError(t, err)
			})

			t.Run("SubscriptionARNEmpty", func(t *testing.T) {
				s, mockDB, mockSNS := setup()
				existingSub := &db.Subscription{
					ID:                 5,
					SNSSubscriptionARN: "",
				}
				mockDB.On("SubscriptionByEventStreamIDAndSubscriptionTargetID", mock.Anything, mock.Anything, mock.Anything).Return(existingSub, nil)
				mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)

				resp, err := s.Delete(ctx, &rpc.DeleteSubscriptionReq{
					TargetId:    dummyTargetID3,
					EventType:   dummyEventType2,
					Environment: environment.Development,
				})
				assert.NotNil(t, resp)
				assert.NoError(t, err)
				mockSNS.AssertNotCalled(t, "SubscriptionExists")
				mockSNS.AssertNotCalled(t, "Unsubscribe")
				mockSNS.AssertNotCalled(t, "DisallowAccountSubscribe")
			})

			t.Run("SubscriptionARNDoesntExist", func(t *testing.T) {
				s, mockDB, mockSNS := setup()
				existingSub := &db.Subscription{
					ID:                 5,
					SNSSubscriptionARN: "arn:aws:snss::123456789012:dummy-susbcription-2",
				}
				mockDB.On("SubscriptionByEventStreamIDAndSubscriptionTargetID", mock.Anything, mock.Anything, mock.Anything).Return(existingSub, nil)
				mockDB.On("SubscriptionDelete", mock.Anything, mock.Anything, mock.Anything).Return(nil)
				mockSNS.On("SubscriptionExists", mock.Anything, mock.Anything, existingSub.SNSSubscriptionARN).Return(false, nil)

				resp, err := s.Delete(ctx, &rpc.DeleteSubscriptionReq{
					TargetId:    dummyTargetID3,
					EventType:   dummyEventType2,
					Environment: environment.Development,
				})
				assert.NotNil(t, resp)
				assert.NoError(t, err)
				mockSNS.AssertNotCalled(t, "Unsubscribe")
				mockSNS.AssertNotCalled(t, "DisallowAccountSubscribe")
			})

		})
		t.Run("AccessDenied", func(t *testing.T) {
			s, _, _ := setup()
			badCtx, _ := badUserContext()
			resp, err := s.Delete(badCtx, &rpc.DeleteSubscriptionReq{
				TargetId:    dummyTargetID2,
				EventType:   dummyEventType1,
				Environment: environment.Development,
			})
			require.Error(t, err)
			assert.Nil(t, resp)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})

	})

	t.Run("Authorization", func(t *testing.T) {
		s, mockDB, _ := setup()
		mockDB.On("SubscriptionTargetByID", mock.Anything, mock.Anything).Return(dummyTarget1, nil)
		mockDB.On("ServiceByID", mock.Anything, mock.Anything).Return(dummyService)
		t.Run("HappyPath", func(t *testing.T) {
			err := s.authorize(ctx, dummyTarget1)
			require.NoError(t, err)
		})
		t.Run("AccessDenied", func(t *testing.T) {
			badCtx, _ := badUserContext()
			err := s.authorize(badCtx, dummyTarget1)
			require.Error(t, err)
			assert.Equal(t, twirp.PermissionDenied, err.(twirp.Error).Code())
		})
	})
}
