package oauth2_test

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"github.com/tidbcloud/auth-server/commons/crypto"
	"github.com/tidbcloud/auth-server/commons/errs"
	"github.com/tidbcloud/auth-server/internal/entity"
)

type RefreshTokenGrantSuite struct {
	Oauth2Suite
}

func TestRefreshTokenGrantSuite(t *testing.T) {
	suite.Run(t, new(RefreshTokenGrantSuite))
}

func (s *RefreshTokenGrantSuite) TestHandleRefreshTokenRequest() {
	tokenResp := s.createAccessTokenByDeviceGrant()

	tokenEncryptor := crypto.NewPbdkf2Sha512Encryptor()

	rawRefreshToken := tokenResp.RefreshToken
	encrptedRefreshToken := tokenEncryptor.Encrypt(rawRefreshToken, "", 20000)

	var akObj entity.Oauth2AccessToken
	dbErr := entity.NewOauth2AccessTokenQuerySet(s.db).RefreshTokenEq(encrptedRefreshToken).One(&akObj)
	require.Nil(s.T(), dbErr)
	assert.Nil(s.T(), akObj.RevokedAt)

	s.T().Run("invalid client id", func(t *testing.T) {
		resp, err := s.oauth2Service.HandleRefreshTokenRequest(s.ctx, "invalid_client_id", s.deviceFlowAppSecret, tokenResp.RefreshToken)
		assert.Equal(t, errs.ErrOauth2InvalidClient, err)
		assert.Nil(t, resp)
	})

	s.T().Run("invalid client secret", func(t *testing.T) {
		resp, err := s.oauth2Service.HandleRefreshTokenRequest(s.ctx, s.deviceFlowApp.ClientID, "invalid_client_secret", tokenResp.RefreshToken)
		assert.Equal(t, errs.ErrOauth2InvalidClient, err)
		assert.Nil(t, resp)
	})

	s.T().Run("invalid refresh token", func(t *testing.T) {
		resp, err := s.oauth2Service.HandleRefreshTokenRequest(s.ctx, s.deviceFlowApp.ClientID, s.deviceFlowAppSecret, "invalid_refresh_token")
		assert.Equal(t, errs.ErrOauth2InvalidGrant, err)
		assert.Nil(t, resp)
	})

	s.T().Run("success", func(t *testing.T) {
		resp, err := s.oauth2Service.HandleRefreshTokenRequest(s.ctx, s.deviceFlowApp.ClientID, s.deviceFlowAppSecret, tokenResp.RefreshToken)
		assert.Nil(t, err)
		assert.NotNil(t, resp)
		assert.NotEmpty(t, resp.AccessToken)
		assert.NotEmpty(t, resp.RefreshToken)
		assert.Equal(t, resp.TokenType, "Bearer")
		assert.Equal(t, resp.ExpiresIn, s.deviceFlowApp.AccessTokenExpiresIn)

		var akObj entity.Oauth2AccessToken
		dbErr := entity.NewOauth2AccessTokenQuerySet(s.db).RefreshTokenEq(encrptedRefreshToken).One(&akObj)
		require.Nil(t, dbErr)
		assert.NotNil(t, akObj.RevokedAt)

		var akObjNew entity.Oauth2AccessToken
		newEncrypedRefreshToken := tokenEncryptor.Encrypt(resp.RefreshToken, "", 20000)
		newEncrypedAccessToken := tokenEncryptor.Encrypt(resp.AccessToken, "", 20000)
		dbErr = entity.NewOauth2AccessTokenQuerySet(s.db).RefreshTokenEq(newEncrypedRefreshToken).One(&akObjNew)
		require.Nil(t, dbErr)
		assert.Equal(t, newEncrypedAccessToken, akObjNew.AccessToken)
		assert.Equal(t, newEncrypedRefreshToken, akObjNew.RefreshToken)
		assert.NotEqual(t, akObj.AccessToken, akObjNew.AccessToken)
		assert.NotEqual(t, akObj.RefreshToken, akObjNew.RefreshToken)
		assert.Equal(t, uint64(s.deviceFlowApp.ID), akObjNew.ApplicationID)
		assert.Equal(t, akObj.UserID, akObjNew.UserID)
		assert.Equal(t, akObj.UserEmail, akObjNew.UserEmail)
		assert.Equal(t, akObj.GrantType, akObjNew.GrantType)
		assert.Equal(t, akObj.Scopes, akObjNew.Scopes)
		assert.Nil(t, akObjNew.RevokedAt)
	})

}

func (s *RefreshTokenGrantSuite) TestHandleRefreshTokenRequestRevoked() {
	tokenResp := s.createAccessTokenByDeviceGrant()

	tokenEncryptor := crypto.NewPbdkf2Sha512Encryptor()

	rawRefreshToken := tokenResp.RefreshToken
	encrptedRefreshToken := tokenEncryptor.Encrypt(rawRefreshToken, "", 20000)

	var akObj entity.Oauth2AccessToken
	dbErr := entity.NewOauth2AccessTokenQuerySet(s.db).RefreshTokenEq(encrptedRefreshToken).One(&akObj)
	require.Nil(s.T(), dbErr)
	assert.Nil(s.T(), akObj.RevokedAt)

	now := time.Now()
	akObj.RevokedAt = &now
	err := akObj.Update(s.db, []entity.Oauth2AccessTokenDBSchemaField{
		entity.Oauth2AccessTokenDBSchema.RevokedAt,
	}...)
	require.Nil(s.T(), err)

	resp, err := s.oauth2Service.HandleRefreshTokenRequest(s.ctx, s.deviceFlowApp.ClientID, s.deviceFlowAppSecret, tokenResp.RefreshToken)
	assert.Equal(s.T(), errs.ErrOauth2InvalidGrant, err)
	assert.Nil(s.T(), resp)
}
