//go:build integration
// +build integration

package authinfoservice

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/grafana/grafana/pkg/services/encryption/ossencryption"

	"github.com/grafana/grafana/pkg/services/sqlstore"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/models"
	"golang.org/x/oauth2"
)

//nolint:goconst
func TestUserAuth(t *testing.T) {
	sqlStore := sqlstore.InitTestDB(t)
	srv := ProvideAuthInfoService(bus.New(), sqlStore, &OSSUserProtectionImpl{}, ossencryption.ProvideService())

	t.Run("Given 5 users", func(t *testing.T) {
		for i := 0; i < 5; i++ {
			cmd := models.CreateUserCommand{
				Email: fmt.Sprint("user", i, "@test.com"),
				Name:  fmt.Sprint("user", i),
				Login: fmt.Sprint("loginuser", i),
			}
			_, err := srv.SQLStore.CreateUser(context.Background(), cmd)
			require.Nil(t, err)
		}

		t.Run("Can find existing user", func(t *testing.T) {
			// By Login
			login := "loginuser0"

			query := &models.GetUserByAuthInfoQuery{Login: login}
			user, err := srv.LookupAndUpdate(query)

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			// By ID
			id := user.Id

			_, user, err = srv.LookupByOneOf(id, "", "")

			require.Nil(t, err)
			require.Equal(t, user.Id, id)

			// By Email
			email := "user1@test.com"

			_, user, err = srv.LookupByOneOf(0, email, "")

			require.Nil(t, err)
			require.Equal(t, user.Email, email)

			// Don't find nonexistent user
			email = "nonexistent@test.com"

			_, user, err = srv.LookupByOneOf(0, email, "")

			require.Equal(t, models.ErrUserNotFound, err)
			require.Nil(t, user)
		})

		t.Run("Can set & locate by AuthModule and AuthId", func(t *testing.T) {
			// get nonexistent user_auth entry
			query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
			user, err := srv.LookupAndUpdate(query)

			require.Equal(t, models.ErrUserNotFound, err)
			require.Nil(t, user)

			// create user_auth entry
			login := "loginuser0"

			query.Login = login
			user, err = srv.LookupAndUpdate(query)

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			// get via user_auth
			query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
			user, err = srv.LookupAndUpdate(query)

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			// get with non-matching id
			id := user.Id

			query.UserId = id + 1
			user, err = srv.LookupAndUpdate(query)

			require.Nil(t, err)
			require.Equal(t, user.Login, "loginuser1")

			// get via user_auth
			query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
			user, err = srv.LookupAndUpdate(query)

			require.Nil(t, err)
			require.Equal(t, user.Login, "loginuser1")

			// remove user
			srv.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
				sess.Exec("DELETE FROM "+srv.SQLStore.Dialect.Quote("user")+" WHERE id=?", user.Id)
				require.NoError(t, err)

				return nil
			})

			// get via user_auth for deleted user
			query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
			user, err = srv.LookupAndUpdate(query)

			require.Equal(t, err, models.ErrUserNotFound)
			require.Nil(t, user)
		})

		t.Run("Can set & retrieve oauth token information", func(t *testing.T) {
			token := &oauth2.Token{
				AccessToken:  "testaccess",
				RefreshToken: "testrefresh",
				Expiry:       time.Now(),
				TokenType:    "Bearer",
			}

			// Find a user to set tokens on
			login := "loginuser0"

			// Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table
			query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test", AuthId: "test"}
			user, err := srv.LookupAndUpdate(query)

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			cmd := &models.UpdateAuthInfoCommand{
				UserId:     user.Id,
				AuthId:     query.AuthId,
				AuthModule: query.AuthModule,
				OAuthToken: token,
			}
			err = srv.UpdateAuthInfo(cmd)

			require.Nil(t, err)

			getAuthQuery := &models.GetAuthInfoQuery{
				UserId: user.Id,
			}

			err = srv.GetAuthInfo(getAuthQuery)

			require.Nil(t, err)
			require.Equal(t, getAuthQuery.Result.OAuthAccessToken, token.AccessToken)
			require.Equal(t, getAuthQuery.Result.OAuthRefreshToken, token.RefreshToken)
			require.Equal(t, getAuthQuery.Result.OAuthTokenType, token.TokenType)
		})

		t.Run("Always return the most recently used auth_module", func(t *testing.T) {
			// Restore after destructive operation
			sqlStore = sqlstore.InitTestDB(t)

			for i := 0; i < 5; i++ {
				cmd := models.CreateUserCommand{
					Email: fmt.Sprint("user", i, "@test.com"),
					Name:  fmt.Sprint("user", i),
					Login: fmt.Sprint("loginuser", i),
				}
				_, err := sqlStore.CreateUser(context.Background(), cmd)
				require.Nil(t, err)
			}

			// Find a user to set tokens on
			login := "loginuser0"

			// Calling srv.LookupAndUpdateQuery on an existing user will populate an entry in the user_auth table
			// Make the first log-in during the past
			getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
			query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test1", AuthId: "test1"}
			user, err := srv.LookupAndUpdate(query)
			getTime = time.Now

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			// Add a second auth module for this user
			// Have this module's last log-in be more recent
			getTime = func() time.Time { return time.Now().AddDate(0, 0, -1) }
			query = &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test2", AuthId: "test2"}
			user, err = srv.LookupAndUpdate(query)
			getTime = time.Now

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			// Get the latest entry by not supply an authmodule or authid
			getAuthQuery := &models.GetAuthInfoQuery{
				UserId: user.Id,
			}

			err = srv.GetAuthInfo(getAuthQuery)

			require.Nil(t, err)
			require.Equal(t, getAuthQuery.Result.AuthModule, "test2")

			// "log in" again with the first auth module
			updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: user.Id, AuthModule: "test1", AuthId: "test1"}
			err = srv.UpdateAuthInfo(updateAuthCmd)

			require.Nil(t, err)

			// Get the latest entry by not supply an authmodule or authid
			getAuthQuery = &models.GetAuthInfoQuery{
				UserId: user.Id,
			}

			err = srv.GetAuthInfo(getAuthQuery)

			require.Nil(t, err)
			require.Equal(t, getAuthQuery.Result.AuthModule, "test1")
		})

		t.Run("Can set & locate by generic oauth auth module and user id", func(t *testing.T) {
			// Find a user to set tokens on
			login := "loginuser0"

			// Expect to pass since there's a matching login user
			getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
			query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: genericOAuthModule, AuthId: ""}
			user, err := srv.LookupAndUpdate(query)
			getTime = time.Now

			require.Nil(t, err)
			require.Equal(t, user.Login, login)

			// Should throw a "user not found" error since there's no matching login user
			getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
			query = &models.GetUserByAuthInfoQuery{Login: "aloginuser", AuthModule: genericOAuthModule, AuthId: ""}
			user, err = srv.LookupAndUpdate(query)
			getTime = time.Now

			require.NotNil(t, err)
			require.Nil(t, user)
		})
	})
}
