package repository

import (
	"context"
	"fmt"
	"git.sch9.ru/new_gate/ms-auth/internal/models"
	"git.sch9.ru/new_gate/ms-auth/pkg"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
	"github.com/valkey-io/valkey-go"
	"github.com/valkey-io/valkey-go/mock"
	"go.uber.org/mock/gomock"
	"strings"
	"testing"
)

var (
	matcherAny = mock.MatchFn(func(cmd []string) bool { return true })
)

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	client := mock.NewClient(ctrl)
	sessionRepo := NewValkeyRepository(client)

	var userId int32 = 1

	matcher := mock.MatchFn(func(cmd []string) bool {
		if cmd[0] != "SET" {
			return false
		}
		if !strings.HasPrefix(cmd[1], fmt.Sprintf("userid:%d:sessionid:", userId)) {
			return false
		}
		if cmd[3] != "EX" {
			return false
		}
		if cmd[4] != "2400" {
			return false
		}
		return true
	})

	t.Run("valid session creation", func(t *testing.T) {
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher)
		sessionId, err := sessionRepo.CreateSession(context.Background(), userId, models.RoleAdmin)
		require.NoError(t, err)
		require.NotEmpty(t, sessionId)
	})

	t.Run("invalid session creation 1", func(t *testing.T) {
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
		sessionId, err := sessionRepo.CreateSession(context.Background(), userId, models.RoleAdmin)
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.ErrorIs(t, err, valkey.Nil)
		require.Empty(t, sessionId)
	})

	t.Run("invalid session creation 2 (invalid userid)", func(t *testing.T) {
		sessionId, err := sessionRepo.CreateSession(context.Background(), 0, models.RoleAdmin)
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.Empty(t, sessionId)
	})

	t.Run("invalid session creation 3 (invalid role)", func(t *testing.T) {
		sessionId, err := sessionRepo.CreateSession(context.Background(), userId, 123)
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.Empty(t, sessionId)
	})
}

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	client := mock.NewClient(ctrl)
	sessionRepo := NewValkeyRepository(client)

	matcher := mock.MatchFn(func(cmd []string) bool {
		if cmd[0] != "EVALSHA" {
			return false
		}
		if cmd[2] != "0" {
			return false
		}
		if !strings.HasPrefix(cmd[3], "userid:*:sessionid:") {
			return false
		}
		return true
	})

	t.Run("valid session read", func(t *testing.T) {
		data, id, err := models.NewSession(1, models.RoleAdmin)
		require.NoError(t, err)
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyString(data)))
		res, err := sessionRepo.ReadSession(context.Background(), id)
		require.NoError(t, err)
		require.Equal(t, int32(1), res.UserId)
		require.Equal(t, id, res.Id)
		require.Equal(t, models.RoleAdmin, res.Role)
	})

	t.Run("invalid session read 1 (not found)", func(t *testing.T) {
		_, id, err := models.NewSession(1, models.RoleAdmin)
		require.NoError(t, err)
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
		res, err := sessionRepo.ReadSession(context.Background(), id)
		require.ErrorIs(t, err, pkg.ErrNotFound)
		require.ErrorIs(t, err, valkey.Nil)
		require.Empty(t, res)
	})

	t.Run("invalid session read 2 (corrupted session storage)", func(t *testing.T) {
		_, id, err := models.NewSession(1, models.RoleAdmin)
		require.NoError(t, err)
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyInt64(123)))
		res, err := sessionRepo.ReadSession(context.Background(), id)
		require.ErrorIs(t, err, pkg.ErrInternal)
		require.True(t, valkey.IsParseErr(err))
		require.Empty(t, res)
	})

	t.Run("invalid session read 3 (bad sessionid)", func(t *testing.T) {
		res, err := sessionRepo.ReadSession(context.Background(), "123")
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.Empty(t, res)
	})
}

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	client := mock.NewClient(ctrl)
	sessionRepo := NewValkeyRepository(client)

	matcher := mock.MatchFn(func(cmd []string) bool {
		if cmd[0] != "EVALSHA" {
			return false
		}
		if cmd[2] != "0" {
			return false
		}
		if !strings.HasPrefix(cmd[3], "userid:*:sessionid:") {
			return false
		}
		return true
	})

	t.Run("valid session update", func(t *testing.T) {
		id := uuid.NewString()
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher)
		err := sessionRepo.UpdateSession(context.Background(), id)
		require.NoError(t, err)
	})

	t.Run("invalid session update 1 (nil response)", func(t *testing.T) {
		id := uuid.NewString()
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcherAny).Return(mock.ErrorResult(valkey.Nil))
		err := sessionRepo.UpdateSession(context.Background(), id)
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.ErrorIs(t, err, valkey.Nil)
	})

	t.Run("invalid session update 2 (bad sessionid)", func(t *testing.T) {
		err := sessionRepo.UpdateSession(context.Background(), "123")
		require.ErrorIs(t, err, pkg.ErrBadInput)
	})
}

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	client := mock.NewClient(ctrl)
	sessionRepo := NewValkeyRepository(client)

	matcher := mock.MatchFn(func(cmd []string) bool {
		if cmd[0] != "EVALSHA" {
			return false
		}
		if cmd[2] != "0" {
			return false
		}
		if !strings.HasPrefix(cmd[3], "userid:*:sessionid:") {
			return false
		}
		return true
	})

	t.Run("valid session delete", func(t *testing.T) {
		id := uuid.NewString()
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyInt64(1)))
		err := sessionRepo.DeleteSession(context.Background(), id)
		require.NoError(t, err)
	})

	t.Run("invalid session delete 1", func(t *testing.T) {
		id := uuid.NewString()
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyNil()))
		err := sessionRepo.DeleteSession(context.Background(), id)
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.ErrorIs(t, err, valkey.Nil)
	})

	t.Run("invalid session delete 2 (bad sessionid)", func(t *testing.T) {
		err := sessionRepo.DeleteSession(context.Background(), "123")
		require.ErrorIs(t, err, pkg.ErrBadInput)
	})
}

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	client := mock.NewClient(ctrl)
	sessionRepo := NewValkeyRepository(client)

	matcher := mock.MatchFn(func(cmd []string) bool {
		if cmd[0] != "EVALSHA" {
			return false
		}
		if cmd[2] != "0" {
			return false
		}
		if !strings.HasPrefix(cmd[3], "userid:1:sessionid:*") {
			return false
		}
		return true
	})

	t.Run("valid all sessions deletion", func(t *testing.T) {
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyInt64(1)))
		err := sessionRepo.DeleteAllSessions(context.Background(), 1)
		require.NoError(t, err)
	})

	t.Run("invalid all sessions deletion 1 (nil response)", func(t *testing.T) {
		ctx := context.Background()
		client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyNil()))
		err := sessionRepo.DeleteAllSessions(context.Background(), 1)
		require.ErrorIs(t, err, pkg.ErrBadInput)
		require.ErrorIs(t, err, valkey.Nil)
	})
}