package authorization

import (
	"context"
	"strings"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/stretchr/testify/assert"
)

func TestCachingPasswordsService(t *testing.T) {
	const (
		user1 = platform.ID(1)
		user2 = platform.ID(2)
	)

	makeUser := func(salt, pass string) authUser {
		if len(salt) != SaltBytes {
			panic("invalid salt")
		}

		var ps CachingPasswordsService
		return authUser{salt: []byte(salt), hash: ps.hashWithSalt([]byte(salt), pass)}
	}

	var (
		userE1 = makeUser(strings.Repeat("salt---1", 4), "foo")
		userE2 = makeUser(strings.Repeat("salt---2", 4), "bar")
	)

	t.Run("SetPassword deletes cached user", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		inner.EXPECT().
			SetPassword(gomock.Any(), user1, "foo").
			Return(nil)

		s := NewCachingPasswordsService(inner)
		s.authCache[user1] = userE1
		s.authCache[user2] = userE2

		ctx := context.Background()

		_, ok := s.authCache[user1]
		assert.True(t, ok)
		assert.NoError(t, s.SetPassword(ctx, user1, "foo"))
		_, ok = s.authCache[user1]
		assert.False(t, ok)
		_, ok = s.authCache[user2]
		assert.True(t, ok)
	})

	t.Run("ComparePassword adds cached user", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		inner.EXPECT().
			ComparePassword(gomock.Any(), user1, "foo").
			Return(nil)

		s := NewCachingPasswordsService(inner)
		s.authCache[user2] = userE2

		ctx := context.Background()

		assert.NoError(t, s.ComparePassword(ctx, user1, "foo"))
		_, ok := s.authCache[user1]
		assert.True(t, ok)
		_, ok = s.authCache[user2]
		assert.True(t, ok)
	})

	t.Run("ComparePassword does not add cached user when inner errors", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		inner.EXPECT().
			ComparePassword(gomock.Any(), user1, "foo").
			Return(errors.EPasswordLength)

		s := NewCachingPasswordsService(inner)
		s.authCache[user2] = userE2

		ctx := context.Background()

		assert.Error(t, s.ComparePassword(ctx, user1, "foo"))
		_, ok := s.authCache[user1]
		assert.False(t, ok)
		_, ok = s.authCache[user2]
		assert.True(t, ok)
	})

	t.Run("ComparePassword uses cached password when context option set", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		s := NewCachingPasswordsService(inner)
		s.authCache[user1] = userE1
		s.authCache[user2] = userE2

		ctx := context.Background()
		assert.NoError(t, s.ComparePassword(ctx, user1, "foo"))
	})

	t.Run("CompareAndSetPassword deletes cached user", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		inner.EXPECT().
			CompareAndSetPassword(gomock.Any(), user1, "foo", "foo2").
			Return(nil)

		s := NewCachingPasswordsService(inner)
		s.authCache[user1] = userE1
		s.authCache[user2] = userE2

		ctx := context.Background()

		assert.NoError(t, s.CompareAndSetPassword(ctx, user1, "foo", "foo2"))
		_, ok := s.authCache[user1]
		assert.False(t, ok)
		_, ok = s.authCache[user2]
		assert.True(t, ok)
	})

	// The following tests ensure the service does not change state for invalid
	// requests, which may permit a certain class of attacks.

	t.Run("SetPassword does not delete cached user when inner errors", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		inner.EXPECT().
			SetPassword(gomock.Any(), user1, "foo").
			Return(errors.EPasswordLength)

		s := NewCachingPasswordsService(inner)
		s.authCache[user1] = userE1
		s.authCache[user2] = userE2

		ctx := context.Background()

		_, ok := s.authCache[user1]
		assert.True(t, ok)
		assert.EqualError(t, s.SetPassword(ctx, user1, "foo"), errors.EPasswordLength.Error())
		_, ok = s.authCache[user1]
		assert.True(t, ok)
	})

	t.Run("CompareAndSetPassword does not delete cached user when inner errors", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		inner := mock.NewMockPasswordsService(ctrl)
		inner.EXPECT().
			CompareAndSetPassword(gomock.Any(), user1, "foo", "foo2").
			Return(errors.EPasswordLength)

		s := NewCachingPasswordsService(inner)
		s.authCache[user1] = userE1
		s.authCache[user2] = userE2

		ctx := context.Background()

		assert.Error(t, s.CompareAndSetPassword(ctx, user1, "foo", "foo2"))
		_, ok := s.authCache[user1]
		assert.True(t, ok)
		_, ok = s.authCache[user2]
		assert.True(t, ok)
	})

}
