package api

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/juju/errors"
	"github.com/pingcap/dbaas/admin/forms"
	"github.com/pingcap/dbaas/admin/models"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/tenant"
	"github.com/pingcap/dbaas/central/handler/billing2"
	infraapi "github.com/pingcap/dbaas/central/infra-api"
	"github.com/pingcap/dbaas/central/model/admin/grafana"
	"github.com/pingcap/dbaas/central/model/session"
	tenantmodel "github.com/pingcap/dbaas/central/model/tenant"
	"github.com/pingcap/dbaas/central/rbac"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/assert"
	mailv2 "github.com/tidbcloud/pkgv2/mail/v2"
	panicIf "github.com/tidbcloud/pkgv2/panicif"
	"github.com/tidbcloud/pkgv2/salesforce"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"github.com/tidbcloud/pkgv2/zendesk"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var (
	router *gin.Engine
)

func genURL(tpl string, args ...interface{}) string {
	return fmt.Sprintf("/api/v1/admin%s", fmt.Sprintf(tpl, args...))
}

func encodeJSON(data interface{}) io.Reader {
	b, _ := json.Marshal(data)

	return bytes.NewReader(b)
}

func decodeJSON(data []byte, dest interface{}) {
	if err := json.Unmarshal(data, dest); err != nil {
		log.Info("failed to decode json", zap.ByteString("data", data))
	}
}

func initFakeData() error {
	var err error

	// init fake role
	fakeRole = &entity.Role{
		Name:     "UnitTest Role",
		TenantID: entity.PingCAPTenantID,
		Privileges: entity.Privileges{
			Privileges: []entity.Privilege{
				{
					Resource:  rbac.AllResource,
					Operation: rbac.AllOperation,
				},
			},
		},
	}
	if err := db.Create(fakeRole); err != nil {
		return err
	}

	// init fake user
	dbUser := &entity.User{
		TenantID:  entity.PingCAPTenantID,
		FirstName: "FirstName",
		LastName:  "LastName",
		Email:     "Email",
		Status:    entity.UserStatusActive,
	}
	dbUser.ID = entity.ID(0)
	dbUser.Roles = append(dbUser.Roles, *fakeRole)
	if err = db.Create(dbUser); err != nil {
		return err
	}
	fakeUser = &tenant.User{
		User:           *dbUser,
		TenantID:       entity.PingCAPTenantID,
		IsImpersonated: false,
	}

	// init fake api key
	fakeAPIKey = &entity.APIKey{
		TenantID:    entity.PingCAPTenantID,
		CreatedByID: fakeUser.ID,
		Name:        "UnitTest Key",
		KeyHash:     "bla",
	}

	return db.Create(fakeAPIKey)
}

func revokeFakeData() {
	err := db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Delete(fakeUser.User).Error; err != nil {
			return err
		}
		if err := tx.Delete(fakeAPIKey).Error; err != nil {
			return err
		}

		return tx.Delete(fakeRole).Error
	})

	if err != nil {
		log.Info("failed to revoke fake data", zap.Error(err))
	}
}

func TestSuccessOrFail(t *testing.T) {
	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/ping", nil)
	router.ServeHTTP(w, req)

	assert.Equal(t, 200, w.Code)

	w = httptest.NewRecorder()
	req, _ = http.NewRequest("GET", "/ping?shouldFail=true", nil)
	router.ServeHTTP(w, req)

	assert.Equal(t, 401, w.Code)
}

func TestPagination(t *testing.T) {
	s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

	limitOffset := forms.LimitAndOffset{Limit: 10, Offset: 0}
	paged := Pagination(s, limitOffset)
	pagedSlice := paged.([]int)
	assert.Equal(t, 10, len(pagedSlice))
	assert.Equal(t, 0, pagedSlice[0])
	assert.Equal(t, 9, pagedSlice[len(pagedSlice)-1])

	limitOffset = forms.LimitAndOffset{Limit: 3, Offset: 10}
	paged = Pagination(s, limitOffset)
	pagedSlice = paged.([]int)
	assert.Equal(t, 1, len(pagedSlice))
	assert.Equal(t, 10, pagedSlice[0])

	limitOffset = forms.LimitAndOffset{Limit: 3, Offset: 20}
	paged = Pagination(s, limitOffset)
	pagedSlice = paged.([]int)
	assert.Equalf(t, 0, len(pagedSlice), fmt.Sprintf("%+v", pagedSlice))

	// test the bad case
	paged = Pagination(1, limitOffset)
	result := paged.(int)
	assert.Equal(t, 1, result)
}

type fakeGrafanaCollector struct{}

func (f fakeGrafanaCollector) Search(ctx context.Context, params ...grafana.SearchParam) ([]grafana.FoundBoard, error) {
	panic("implement me")
}

func newTokenSession() *session.Session {
	aesCrypt, err := encrypt.NewAESCrypt(make([]byte, 16))
	if err != nil {
		panicIf.Error(errors.Trace(err))
	}
	session := session.NewSession(aesCrypt)
	return session
}

type MockAuth0UserManagementHandler struct {
	tenantmodel.Auth0UserManagementHandler
}

type MockZendeskHandler struct{}

func (*MockZendeskHandler) CreateOrUpdateOrganization(input zendesk.Organization) (zendesk.Organization, error) {
	return input, nil
}

func (*MockZendeskHandler) CreateOrganization(input zendesk.Organization) (zendesk.Organization, error) {
	return input, nil
}

type MockSalesforceHandler struct {
}

func (m *MockSalesforceHandler) UpdateAccount(account salesforce.Account) error {
	return nil
}

func (m *MockSalesforceHandler) SyncLeads(lead salesforce.Lead) error {
	return nil
}

type FakeBillingHandler struct {
	billing2.BillingHandler
}

func (*FakeBillingHandler) GetPackageDiscount(uint64) (*decimal.Decimal, error) {
	return &decimal.Zero, nil
}

func (*FakeBillingHandler) GetBindedPackageDiscount(uint64, uint64) (*decimal.Decimal, error) {
	return nil, nil
}

func TestMain(m *testing.M) {
	log.Info("start to running test cases...")

	// set isUnitTest to true
	isUnitTest = true
	models.InitMySQL(isUnitTest)
	if err := initFakeData(); err != nil {
		log.Fatal("failed to init data", zap.Error(err))
	}
	defer revokeFakeData()

	mailFacade := mailv2.NewFakeMailFacade()
	grafanaClient := &fakeGrafanaCollector{}
	salesforceClient := &MockSalesforceHandler{}
	auth0Client := &MockAuth0UserManagementHandler{}
	zendeskClient := &MockZendeskHandler{}
	api := &infraapi.API{}

	billing2.SetHandler(&FakeBillingHandler{})

	// init fake runtime manager/billing/mail service...etc.
	if err := models.InitFakeAdminModel(log.L(), mailFacade, grafanaClient, salesforceClient, api, newTokenSession(), auth0Client, zendeskClient); err != nil {
		log.Fatal("failed to init fake clients for admin model", zap.Error(err))
	}

	router = InitRouter()

	os.Exit(m.Run())
}
