package oauth2_test

import (
	"context"
	"log"
	"net/url"

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

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/auth-server/commons/config"
	"github.com/tidbcloud/auth-server/commons/database"
	"github.com/tidbcloud/auth-server/commons/metrics"
	"github.com/tidbcloud/auth-server/commons/testhelpers"
	oauth2_dto "github.com/tidbcloud/auth-server/internal/dto/oauth2"
	"github.com/tidbcloud/auth-server/internal/entity"
	oauth2_svc "github.com/tidbcloud/auth-server/internal/service/oauth2"
	"github.com/tidbcloud/auth-server/migrate"
)

type Oauth2Suite struct {
	suite.Suite

	ctrl        *gomock.Controller
	dbContainer *testhelpers.MysqlContainer
	ctx         context.Context
	db          *gorm.DB

	testUser                        *account.User
	testOrgId                       uint64
	oauth2Service                   oauth2_svc.Oauth2Service
	accountMockProvider             *account.MockAccountAPIServiceClient
	deviceFlowApp                   *entity.Oauth2Application
	deviceFlowAppSecret             string
	authorizationCodeApp            *entity.Oauth2Application
	authorizationCodeAppSecret      string
	authorizationCodeRawRedirectURI string
	authorizationCodeRedirectURL    *url.URL
	authorizationCodeReq            *oauth2_svc.AuthorizeRequest
}

func (s *Oauth2Suite) SetupSuite() {
	s.ctx = context.Background()
	mysqlContainer, err := testhelpers.CreateMysqlContainer(s.ctx)
	if err != nil {
		s.T().Fatal(err)
	}
	s.dbContainer = mysqlContainer
	s.db = testhelpers.MustGetTestDB(mysqlContainer.DSN)
	database.DBConn.DB = s.db
	err = migrate.RunAuthServerDBMigrate(s.db)
	if err != nil {
		s.T().Fatal(err)
	}

	err = config.NormalizeConfig()
	if err != nil {
		s.T().Fatal(err)
	}

	s.testUser = &account.User{
		Id:    1,
		Email: "test@example.com",
	}
	s.testOrgId = 1000

	metrics.RegisterCentralMetrics()

	s.ctrl = gomock.NewController(nil)
	s.accountMockProvider = account.NewMockAccountAPIServiceClient(s.ctrl)
	s.oauth2Service = oauth2_svc.NewOauth2Service(s.db, s.accountMockProvider)

	appObj, clientSecret, err := s.oauth2Service.CreateApplication(s.ctx,
		"device", "http://127.0.0.1", entity.Oauth2DeviceCode, "", "")
	if err != nil {
		s.T().Fatal(err)
	}
	s.deviceFlowApp = appObj
	s.deviceFlowAppSecret = clientSecret

	s.authorizationCodeRawRedirectURI = "https://example.com/callback"
	s.authorizationCodeRedirectURL, _ = url.Parse(s.authorizationCodeRawRedirectURI)

	appObj, clientSecret, err = s.oauth2Service.CreateApplication(s.ctx,
		"authorization code", s.authorizationCodeRawRedirectURI, entity.Oauth2AuthorizationCode, "org:member", "project:owner")
	if err != nil {
		s.T().Fatal(err)
	}
	s.authorizationCodeApp = appObj
	s.authorizationCodeAppSecret = clientSecret
	s.authorizationCodeReq = &oauth2_svc.AuthorizeRequest{
		Application: s.authorizationCodeApp,
		ClientID:    s.authorizationCodeApp.ClientID,
		RedirectURI: s.authorizationCodeRedirectURL,
		OrgRole:     "org:member",
		ProjectRole: "project:owner",
		State:       "state",
	}
}

func (s *Oauth2Suite) TearDownSuite() {
	if err := s.dbContainer.Terminate(s.ctx); err != nil {
		log.Fatalf("error terminating db container: %s", err)
	}
	s.ctrl.Finish()
	s.T().Log("TearDownSuite")
}

func (s *Oauth2Suite) createCodeGrant() *entity.Oauth2CodeGrant {
	s.accountMockProvider.EXPECT().VerifyUserOrgAndProjects(gomock.Any(), &account.VerifyUserOrgAndProjectsReq{
		UserId:      s.testUser.Id,
		OrgId:       1000,
		ProjectIds:  []uint64{1, 2},
		OrgRole:     s.authorizationCodeReq.OrgRole,
		ProjectRole: s.authorizationCodeReq.ProjectRole,
	}).Return(
		&account.VerifyUserOrgAndProjectsResp{Result: true}, nil,
	)
	grant, redirectURL, err := s.oauth2Service.InnerAuthorizationCodeApprove(s.ctx, s.testUser, s.authorizationCodeReq, 1000, []uint64{1, 2})
	require.Nil(s.T(), err)
	require.NotNil(s.T(), grant)
	require.NotNil(s.T(), redirectURL)
	return grant
}

func (s *Oauth2Suite) createDeviceGrant() (*oauth2_dto.DeviceAuthorizationResponse, *entity.Oauth2DeviceGrant) {
	resp, err := s.oauth2Service.HandleDeviceAuthorizationRequest(s.ctx, s.deviceFlowApp.ClientID, "127.0.0.1", "ticloud")
	require.Nil(s.T(), err)
	require.NotNil(s.T(), resp)

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

func (s *Oauth2Suite) createAccessTokenByDeviceGrant() *oauth2_dto.TokenResponse {
	resp, _ := s.createDeviceGrant()

	s.accountMockProvider.EXPECT().ListOrgsByUser(gomock.Any(), gomock.Any()).Return(
		&account.ListOrgsByUserResponse{Orgs: []*account.Org{{Id: s.testOrgId}}}, nil)
	err1 := s.oauth2Service.HandleInnerDeviceCodeConfirmationRequest(s.ctx, s.testUser, resp.UserCode, true, s.testOrgId)
	require.Nil(s.T(), err1)

	tokenResp, err2 := s.oauth2Service.HandleDeviceTokenRequest(s.ctx, s.deviceFlowApp.ClientID, resp.DeviceCode)
	require.Nil(s.T(), err2)
	return tokenResp
}
