package authorizer_test

import (
	"context"
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/authorizer"
	influxdbcontext "github.com/influxdata/influxdb/v2/context"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/mock"
	influxdbtesting "github.com/influxdata/influxdb/v2/testing"
	"github.com/stretchr/testify/require"
)

var (
	annOrgID1 = influxdbtesting.IDPtr(1)
	annOrgID2 = influxdbtesting.IDPtr(10)
	rID       = influxdbtesting.IDPtr(2)
)

func Test_CreateAnnotations(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		wantRet []influxdb.AnnotationEvent
		wantErr error
	}{
		{
			"authorized to create annotation(s) with the specified org",
			[]influxdb.AnnotationEvent{{ID: *rID}},
			nil,
		},
		{
			"not authorized to create annotation(s) with the specified org",
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			var perm influxdb.Permission
			if tt.wantErr == nil {
				perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1)
				svc.EXPECT().
					CreateAnnotations(gomock.Any(), *annOrgID1, []influxdb.AnnotationCreate{{}}).
					Return(tt.wantRet, nil)
			} else {
				perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1)
			}

			ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.CreateAnnotations(ctx, *annOrgID1, []influxdb.AnnotationCreate{{}})
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_ListAnnotations(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		wantRet []influxdb.StoredAnnotation
		wantErr error
	}{
		{
			"authorized to list annotations for the specified org",
			[]influxdb.StoredAnnotation{},
			nil,
		},
		{
			"not authorized to list annotations for the specified org",
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("read:orgs/%s/annotations is unauthorized", annOrgID1),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			var perm influxdb.Permission
			if tt.wantErr == nil {
				perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1)
				svc.EXPECT().
					ListAnnotations(gomock.Any(), *annOrgID1, influxdb.AnnotationListFilter{}).
					Return(tt.wantRet, nil)
			}

			ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.ListAnnotations(ctx, *annOrgID1, influxdb.AnnotationListFilter{})
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_GetAnnotation(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		permissionOrg *platform.ID
		wantRet       *influxdb.StoredAnnotation
		wantErr       error
	}{
		{
			"authorized to access annotation by id",
			annOrgID1,
			&influxdb.StoredAnnotation{
				ID:    *rID,
				OrgID: *annOrgID1,
			},
			nil,
		},
		{
			"not authorized to access annotation by id",
			annOrgID2,
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("read:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			svc.EXPECT().
				GetAnnotation(gomock.Any(), *rID).
				Return(&influxdb.StoredAnnotation{
					ID:    *rID,
					OrgID: *annOrgID1,
				}, nil)

			perm := newTestAnnotationsPermission(influxdb.ReadAction, tt.permissionOrg)

			ctx := context.Background()
			ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.GetAnnotation(ctx, *rID)
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_DeleteAnnotations(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		wantErr error
	}{
		{
			"authorized to delete annotations with the specified org",
			nil,
		},
		{
			"not authorized to delete annotations with the specified org",
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			var perm influxdb.Permission
			if tt.wantErr == nil {
				perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1)
				svc.EXPECT().
					DeleteAnnotations(gomock.Any(), *annOrgID1, influxdb.AnnotationDeleteFilter{}).
					Return(nil)
			} else {
				perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1)
			}

			ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			err := s.DeleteAnnotations(ctx, *annOrgID1, influxdb.AnnotationDeleteFilter{})
			require.Equal(t, tt.wantErr, err)
		})
	}
}

func Test_DeleteAnnotation(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		permissionOrg *platform.ID
		wantErr       error
	}{
		{
			"authorized to delete annotation by id",
			annOrgID1,
			nil,
		},
		{
			"not authorized to delete annotation by id",
			annOrgID2,
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			svc.EXPECT().
				GetAnnotation(gomock.Any(), *rID).
				Return(&influxdb.StoredAnnotation{
					ID:    *rID,
					OrgID: *annOrgID1,
				}, nil)

			perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg)

			if tt.wantErr == nil {
				svc.EXPECT().
					DeleteAnnotation(gomock.Any(), *rID).
					Return(nil)
			}

			ctx := context.Background()
			ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			err := s.DeleteAnnotation(ctx, *rID)
			require.Equal(t, tt.wantErr, err)
		})
	}
}

func Test_UpdateAnnotation(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		permissionOrg *platform.ID
		wantRet       *influxdb.AnnotationEvent
		wantErr       error
	}{
		{
			"authorized to update annotation by id",
			annOrgID1,
			&influxdb.AnnotationEvent{},
			nil,
		},
		{
			"not authorized to update annotation by id",
			annOrgID2,
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			svc.EXPECT().
				GetAnnotation(gomock.Any(), *rID).
				Return(&influxdb.StoredAnnotation{
					ID:    *rID,
					OrgID: *annOrgID1,
				}, nil)

			perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg)

			if tt.wantErr == nil {
				svc.EXPECT().
					UpdateAnnotation(gomock.Any(), *rID, influxdb.AnnotationCreate{}).
					Return(tt.wantRet, nil)
			}

			ctx := context.Background()
			ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.UpdateAnnotation(ctx, *rID, influxdb.AnnotationCreate{})
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_ListStreams(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		wantRet []influxdb.StoredStream
		wantErr error
	}{
		{
			"authorized to list streams for the specified org",
			[]influxdb.StoredStream{},
			nil,
		},
		{
			"not authorized to list streams for the specified org",
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("read:orgs/%s/annotations is unauthorized", annOrgID1),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			var perm influxdb.Permission
			if tt.wantErr == nil {
				perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1)
				svc.EXPECT().
					ListStreams(gomock.Any(), *annOrgID1, influxdb.StreamListFilter{}).
					Return(tt.wantRet, nil)
			}

			ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.ListStreams(ctx, *annOrgID1, influxdb.StreamListFilter{})
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_GetStream(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		permissionOrg *platform.ID
		wantRet       *influxdb.StoredStream
		wantErr       error
	}{
		{
			"authorized to access stream by id",
			annOrgID1,
			&influxdb.StoredStream{
				ID:    *rID,
				OrgID: *annOrgID1,
			},
			nil,
		},
		{
			"not authorized to access stream by id",
			annOrgID2,
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("read:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			svc.EXPECT().
				GetStream(gomock.Any(), *rID).
				Return(&influxdb.StoredStream{
					ID:    *rID,
					OrgID: *annOrgID1,
				}, nil)

			perm := newTestAnnotationsPermission(influxdb.ReadAction, tt.permissionOrg)

			ctx := context.Background()
			ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.GetStream(ctx, *rID)
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_CreateOrUpdateStream(t *testing.T) {
	t.Parallel()

	var (
		testStreamName = "test stream"
		testStream     = influxdb.Stream{
			Name: testStreamName,
		}
	)

	t.Run("updating a stream", func(t *testing.T) {
		tests := []struct {
			name            string
			permissionOrg   *platform.ID
			existingStreams []influxdb.StoredStream
			getStreamRet    *influxdb.StoredStream
			wantRet         *influxdb.ReadStream
			wantErr         error
		}{
			{
				"authorized to update an existing stream",
				annOrgID1,
				[]influxdb.StoredStream{{ID: *rID, OrgID: *annOrgID1}},
				&influxdb.StoredStream{ID: *rID, OrgID: *annOrgID1},
				&influxdb.ReadStream{ID: *rID},
				nil,
			},
			{
				"not authorized to update an existing stream",
				annOrgID2,
				[]influxdb.StoredStream{{ID: *rID, OrgID: *annOrgID1}},
				&influxdb.StoredStream{ID: *rID, OrgID: *annOrgID1},
				nil,
				&errors.Error{
					Msg:  fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
					Code: errors.EUnauthorized,
				},
			},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				ctrlr := gomock.NewController(t)
				svc := mock.NewMockAnnotationService(ctrlr)
				s := authorizer.NewAnnotationService(svc)

				svc.EXPECT().
					ListStreams(gomock.Any(), *tt.permissionOrg, influxdb.StreamListFilter{
						StreamIncludes: []string{testStreamName},
					}).
					Return(tt.existingStreams, nil)

				svc.EXPECT().
					GetStream(gomock.Any(), tt.existingStreams[0].ID).
					Return(tt.getStreamRet, nil)

				if tt.wantErr == nil {
					svc.EXPECT().
						UpdateStream(gomock.Any(), tt.existingStreams[0].ID, testStream).
						Return(tt.wantRet, tt.wantErr)
				}

				perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg)
				ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
				got, err := s.CreateOrUpdateStream(ctx, *tt.permissionOrg, testStream)
				require.Equal(t, tt.wantErr, err)
				require.Equal(t, tt.wantRet, got)
			})
		}
	})

	t.Run("creating a stream", func(t *testing.T) {
		tests := []struct {
			name            string
			existingStreams []influxdb.StoredStream
			wantRet         *influxdb.ReadStream
			wantErr         error
		}{
			{
				"authorized to create a stream with the specified org",
				[]influxdb.StoredStream{},
				&influxdb.ReadStream{},
				nil,
			},
			{
				"not authorized to create a stream with the specified org",
				[]influxdb.StoredStream{},
				nil,
				&errors.Error{
					Msg:  fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1),
					Code: errors.EUnauthorized,
				},
			},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				ctrlr := gomock.NewController(t)
				svc := mock.NewMockAnnotationService(ctrlr)
				s := authorizer.NewAnnotationService(svc)

				svc.EXPECT().
					ListStreams(gomock.Any(), *annOrgID1, influxdb.StreamListFilter{
						StreamIncludes: []string{testStreamName},
					}).
					Return(tt.existingStreams, nil)

				var perm influxdb.Permission
				if tt.wantErr == nil {
					perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1)
					svc.EXPECT().
						CreateOrUpdateStream(gomock.Any(), *annOrgID1, testStream).
						Return(tt.wantRet, nil)
				} else {
					perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1)
				}

				ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
				got, err := s.CreateOrUpdateStream(ctx, *annOrgID1, testStream)
				require.Equal(t, tt.wantErr, err)
				require.Equal(t, tt.wantRet, got)
			})
		}
	})

	t.Run("stream list longer than 1 returns a server error", func(t *testing.T) {
		ctrlr := gomock.NewController(t)
		svc := mock.NewMockAnnotationService(ctrlr)
		s := authorizer.NewAnnotationService(svc)

		svc.EXPECT().
			ListStreams(gomock.Any(), *annOrgID1, influxdb.StreamListFilter{
				StreamIncludes: []string{testStreamName},
			}).
			Return([]influxdb.StoredStream{{Name: testStreamName}, {Name: testStreamName}}, nil)

		wantErr := &errors.Error{
			Code: errors.EInternal,
			Msg:  fmt.Sprintf("more than one stream named %q for org %q", testStreamName, annOrgID1),
		}

		got, err := s.CreateOrUpdateStream(context.Background(), *annOrgID1, testStream)
		require.Nil(t, got)
		require.Equal(t, err, wantErr)
	})
}

func Test_UpdateStream(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		permissionOrg *platform.ID
		wantRet       *influxdb.ReadStream
		wantErr       error
	}{
		{
			"authorized to update stream by id",
			annOrgID1,
			&influxdb.ReadStream{},
			nil,
		},
		{
			"not authorized to update stream by id",
			annOrgID2,
			nil,
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			svc.EXPECT().
				GetStream(gomock.Any(), *rID).
				Return(&influxdb.StoredStream{
					ID:    *rID,
					OrgID: *annOrgID1,
				}, nil)

			perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg)

			if tt.wantErr == nil {
				svc.EXPECT().
					UpdateStream(gomock.Any(), *rID, influxdb.Stream{}).
					Return(tt.wantRet, nil)
			}

			ctx := context.Background()
			ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			got, err := s.UpdateStream(ctx, *rID, influxdb.Stream{})
			require.Equal(t, tt.wantErr, err)
			require.Equal(t, tt.wantRet, got)
		})
	}
}

func Test_DeleteStreams(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		wantErr error
	}{
		{
			"authorized to delete streams with the specified org",
			nil,
		},
		{
			"not authorized to delete streams with the specified org",
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			var perm influxdb.Permission
			if tt.wantErr == nil {
				perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1)
				svc.EXPECT().
					DeleteStreams(gomock.Any(), *annOrgID1, influxdb.BasicStream{}).
					Return(nil)
			} else {
				perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1)
			}

			ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			err := s.DeleteStreams(ctx, *annOrgID1, influxdb.BasicStream{})
			require.Equal(t, tt.wantErr, err)
		})
	}
}

func Test_DeleteStreamByID(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		permissionOrg *platform.ID
		wantErr       error
	}{
		{
			"authorized to delete stream by id",
			annOrgID1,
			nil,
		},
		{
			"not authorized to delete stream by id",
			annOrgID2,
			&errors.Error{
				Msg:  fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID),
				Code: errors.EUnauthorized,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrlr := gomock.NewController(t)
			svc := mock.NewMockAnnotationService(ctrlr)
			s := authorizer.NewAnnotationService(svc)

			svc.EXPECT().
				GetStream(gomock.Any(), *rID).
				Return(&influxdb.StoredStream{
					ID:    *rID,
					OrgID: *annOrgID1,
				}, nil)

			perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg)

			if tt.wantErr == nil {
				svc.EXPECT().
					DeleteStreamByID(gomock.Any(), *rID).
					Return(nil)
			}

			ctx := context.Background()
			ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm}))
			err := s.DeleteStreamByID(ctx, *rID)
			require.Equal(t, tt.wantErr, err)
		})
	}
}

func newTestAnnotationsPermission(action influxdb.Action, orgID *platform.ID) influxdb.Permission {
	return influxdb.Permission{
		Action: action,
		Resource: influxdb.Resource{
			Type:  influxdb.AnnotationsResourceType,
			OrgID: orgID,
		},
	}
}
