package oauth2_test

import (
	"testing"
	"time"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/auth-server/internal/entity"
	oauth2_svc "github.com/tidbcloud/auth-server/internal/service/oauth2"
)

type HandleDeviceInnerCodeCheckSuite struct {
	Oauth2Suite
}

func TestHandleDeviceInnerCodeCheckSuite(t *testing.T) {
	suite.Run(t, new(HandleDeviceInnerCodeCheckSuite))
}

func (s *HandleDeviceInnerCodeCheckSuite) TestHandleDeviceInnerCodeCheckSuccess() {
	resp, grantObj := s.createDeviceGrant()

	resp2, err := s.oauth2Service.HandleInnerDeviceCodeCheckRequest(s.ctx, resp.UserCode)
	require.Nil(s.T(), err)
	require.NotNil(s.T(), resp2)
	assert.Equal(s.T(), resp2.InnerBasicResponse.Code, 200)
	assert.Equal(s.T(), resp2.Data.IP, "127.0.0.1")
	assert.Equal(s.T(), resp2.Data.UserAgent, "ticloud")
	assert.Equal(s.T(), resp2.Data.CreatedAt, grantObj.CreatedAt.Format(time.RFC3339))
}

func (s *HandleDeviceInnerCodeCheckSuite) TestHandleDeviceInnerCodeCheckInvalid() {
	resp, err := s.oauth2Service.HandleInnerDeviceCodeCheckRequest(s.ctx, "invalid")
	assert.NotNil(s.T(), err)
	assert.Nil(s.T(), resp)
	assert.Equal(s.T(), err.Error(), oauth2_svc.UserCodeInvalidMsg)
}

func (s *HandleDeviceInnerCodeCheckSuite) TestHandleDeviceInnerCodeCheckRevoked() {
	resp, grantObj := s.createDeviceGrant()

	now := time.Now()
	grantObj.RevokedAt = &now
	dbErr := grantObj.Update(s.db, []entity.Oauth2DeviceGrantDBSchemaField{
		entity.Oauth2DeviceGrantDBSchema.RevokedAt,
	}...)
	require.Nil(s.T(), dbErr)

	resp2, err := s.oauth2Service.HandleInnerDeviceCodeCheckRequest(s.ctx, resp.UserCode)
	assert.NotNil(s.T(), err)
	assert.Nil(s.T(), resp2)
	assert.Equal(s.T(), err.Error(), oauth2_svc.UserCodeInvalidMsg)
}

func (s *HandleDeviceInnerCodeCheckSuite) TestHandleDeviceInnerCodeCheckDenied() {
	resp, grantObj := s.createDeviceGrant()

	grantObj.Status = entity.Oauth2GrantStatusDeny
	dbErr := grantObj.Update(s.db, []entity.Oauth2DeviceGrantDBSchemaField{
		entity.Oauth2DeviceGrantDBSchema.Status,
	}...)
	require.Nil(s.T(), dbErr)

	resp2, err := s.oauth2Service.HandleInnerDeviceCodeCheckRequest(s.ctx, resp.UserCode)
	assert.NotNil(s.T(), err)
	assert.Nil(s.T(), resp2)
	assert.Equal(s.T(), err.Error(), oauth2_svc.UserCodeInvalidMsg)
}

func (s *HandleDeviceInnerCodeCheckSuite) TestHandleDeviceInnerCodeCheckExpired() {
	resp, grantObj := s.createDeviceGrant()

	// TODO: update createdAt
	grantObj.ExpiresIn = 0
	dbErr := grantObj.Update(s.db, []entity.Oauth2DeviceGrantDBSchemaField{
		entity.Oauth2DeviceGrantDBSchema.ExpiresIn,
	}...)
	require.Nil(s.T(), dbErr)

	time.Sleep(200 * time.Millisecond)

	resp2, err := s.oauth2Service.HandleInnerDeviceCodeCheckRequest(s.ctx, resp.UserCode)
	assert.NotNil(s.T(), err)
	assert.Nil(s.T(), resp2)
	assert.Equal(s.T(), err.Error(), oauth2_svc.UserCodeInvalidMsg)
}

type HandleDeviceInnerCodeConfirmationSuite struct {
	Oauth2Suite
}

func TestHandleDeviceInnerCodeConfirmationSuite(t *testing.T) {
	suite.Run(t, new(HandleDeviceInnerCodeConfirmationSuite))
}

func (s *HandleDeviceInnerCodeConfirmationSuite) TestHandleDeviceInnerCodeApproveSuccess() {
	resp, _ := s.createDeviceGrant()

	accountUser := &account.User{Id: 1, Email: "test@example.com"}
	s.accountMockProvider.EXPECT().ListOrgsByUser(gomock.Any(), gomock.Any()).Return(
		&account.ListOrgsByUserResponse{Orgs: []*account.Org{{Id: uint64(1000)}}}, nil)
	err := s.oauth2Service.HandleInnerDeviceCodeConfirmationRequest(s.ctx, accountUser, resp.UserCode, true, 1000)
	assert.Nil(s.T(), err)

	var grantObj2 entity.Oauth2DeviceGrant
	dbErr := entity.NewOauth2DeviceGrantQuerySet(s.db).UserCodeEq(resp.UserCode).One(&grantObj2)
	require.Nil(s.T(), dbErr)

	assert.Equal(s.T(), grantObj2.Status, entity.Oauth2GrantStatusApprove)
	assert.Equal(s.T(), grantObj2.UserID, uint64(1))
	assert.Equal(s.T(), grantObj2.UserEmail, "test@example.com")
	assert.Equal(s.T(), grantObj2.OrgID, uint64(1000))
	assert.Equal(s.T(), grantObj2.GrantAllProjects, true)
	assert.Equal(s.T(), entity.ProjectIDs{IDs: []uint64{}}, grantObj2.ProjectIDs)
	assert.Equal(s.T(), grantObj2.Scopes, "[]")
	assert.Nil(s.T(), grantObj2.RevokedAt)
}

func (s *HandleDeviceInnerCodeConfirmationSuite) TestHandleDeviceInnerCodeApproveNotInOrg() {
	resp, _ := s.createDeviceGrant()

	accountUser := &account.User{Id: 1, Email: "test@example.com"}
	s.accountMockProvider.EXPECT().ListOrgsByUser(gomock.Any(), gomock.Any()).Return(
		&account.ListOrgsByUserResponse{Orgs: []*account.Org{{Id: uint64(2000)}}}, nil)
	err := s.oauth2Service.HandleInnerDeviceCodeConfirmationRequest(s.ctx, accountUser, resp.UserCode, true, 1000)
	assert.NotNil(s.T(), err)
	assert.Equal(s.T(), err.Error(), "user is not in the org")

	var grantObj2 entity.Oauth2DeviceGrant
	dbErr := entity.NewOauth2DeviceGrantQuerySet(s.db).UserCodeEq(resp.UserCode).One(&grantObj2)
	require.Nil(s.T(), dbErr)

	assert.Equal(s.T(), grantObj2.Status, entity.Oauth2GrantStatusPending)
	assert.Equal(s.T(), grantObj2.UserID, uint64(0))
	assert.Equal(s.T(), grantObj2.UserEmail, "")
	assert.Equal(s.T(), grantObj2.OrgID, uint64(0))
	assert.Equal(s.T(), grantObj2.GrantAllProjects, false)
	assert.Equal(s.T(), entity.ProjectIDs{IDs: []uint64{}}, grantObj2.ProjectIDs)
	assert.Equal(s.T(), grantObj2.Scopes, "[]")
	assert.Nil(s.T(), grantObj2.RevokedAt)
}

func (s *HandleDeviceInnerCodeConfirmationSuite) TestHandleDeviceInnerCodeDeny() {
	resp, _ := s.createDeviceGrant()

	accountUser := &account.User{Id: 1, Email: "test@example.com"}
	err := s.oauth2Service.HandleInnerDeviceCodeConfirmationRequest(s.ctx, accountUser, resp.UserCode, false, 1000)
	assert.Nil(s.T(), err)

	var grantObj2 entity.Oauth2DeviceGrant
	dbErr := entity.NewOauth2DeviceGrantQuerySet(s.db).UserCodeEq(resp.UserCode).One(&grantObj2)
	require.Nil(s.T(), dbErr)

	assert.Equal(s.T(), grantObj2.Status, entity.Oauth2GrantStatusDeny)
	assert.Equal(s.T(), grantObj2.UserID, uint64(1))
	assert.Equal(s.T(), grantObj2.UserEmail, "test@example.com")
	assert.Equal(s.T(), grantObj2.OrgID, uint64(0))
	assert.Equal(s.T(), grantObj2.GrantAllProjects, false)
	assert.Equal(s.T(), entity.ProjectIDs{IDs: []uint64{}}, grantObj2.ProjectIDs)
	assert.Equal(s.T(), grantObj2.Scopes, "[]")
	assert.NotNil(s.T(), grantObj2.RevokedAt)
}
