// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package quota

import (
	"fmt"
	"os"
	"sync"
	"testing"

	"github.com/goharbor/harbor/src/common/dao"
	"github.com/goharbor/harbor/src/common/models"
	"github.com/goharbor/harbor/src/common/quota/driver"
	"github.com/goharbor/harbor/src/common/quota/driver/mocks"
	"github.com/goharbor/harbor/src/pkg/types"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
)

var (
	hardLimits = types.ResourceList{types.ResourceCount: -1, types.ResourceStorage: 1000}
	reference  = "mock"
)

func init() {
	mockDriver := &mocks.Driver{}

	mockHardLimitsFn := func() types.ResourceList {
		return types.ResourceList{
			types.ResourceCount:   -1,
			types.ResourceStorage: -1,
		}
	}

	mockLoadFn := func(key string) driver.RefObject {
		return driver.RefObject{"id": key}
	}

	mockDriver.On("HardLimits").Return(mockHardLimitsFn)
	mockDriver.On("Load", mock.AnythingOfType("string")).Return(mockLoadFn, nil)
	mockDriver.On("Validate", mock.AnythingOfType("types.ResourceList")).Return(nil)

	driver.Register(reference, mockDriver)
}

func mustResourceList(s string) types.ResourceList {
	resources, _ := types.NewResourceList(s)
	return resources
}

type ManagerSuite struct {
	suite.Suite
}

func (suite *ManagerSuite) SetupTest() {
	_, ok := driver.Get(reference)
	if !ok {
		suite.Fail("driver not found for %s", reference)
	}
}

func (suite *ManagerSuite) quotaManager(referenceIDs ...string) *Manager {
	referenceID := "1"
	if len(referenceIDs) > 0 {
		referenceID = referenceIDs[0]
	}

	mgr, _ := NewManager(reference, referenceID)
	return mgr
}

func (suite *ManagerSuite) TearDownTest() {
	dao.ClearTable("quota")
	dao.ClearTable("quota_usage")
}

func (suite *ManagerSuite) TestNewQuota() {
	mgr := suite.quotaManager()

	if id, err := mgr.NewQuota(hardLimits); suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Equal(hardLimits, mustResourceList(quota.Hard))
	}

	mgr = suite.quotaManager("2")
	used := types.ResourceList{types.ResourceStorage: 100}
	if id, err := mgr.NewQuota(hardLimits, used); suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Equal(hardLimits, mustResourceList(quota.Hard))

		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(used, mustResourceList(usage.Used))
	}
}

func (suite *ManagerSuite) TestDeleteQuota() {
	mgr := suite.quotaManager()

	id, err := mgr.NewQuota(hardLimits)
	if suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Equal(hardLimits, mustResourceList(quota.Hard))
	}

	if err := mgr.DeleteQuota(); suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Nil(quota)
	}
}

func (suite *ManagerSuite) TestUpdateQuota() {
	mgr := suite.quotaManager()

	id, _ := mgr.NewQuota(hardLimits)
	largeHardLimits := types.ResourceList{types.ResourceCount: -1, types.ResourceStorage: 1000000}

	if err := mgr.UpdateQuota(largeHardLimits); suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Equal(largeHardLimits, mustResourceList(quota.Hard))
	}
}

func (suite *ManagerSuite) TestSetResourceUsage() {
	mgr := suite.quotaManager()
	id, _ := mgr.NewQuota(hardLimits)

	if err := mgr.SetResourceUsage(types.ResourceCount, 999999999999999999); suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Equal(hardLimits, mustResourceList(quota.Hard))

		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(types.ResourceList{types.ResourceCount: 999999999999999999, types.ResourceStorage: 0}, mustResourceList(usage.Used))
	}

	if err := mgr.SetResourceUsage(types.ResourceStorage, 234); suite.Nil(err) {
		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(types.ResourceList{types.ResourceCount: 999999999999999999, types.ResourceStorage: 234}, mustResourceList(usage.Used))
	}
}

func (suite *ManagerSuite) TestEnsureQuota() {
	// non-existent
	nonExistRefID := "3"
	mgr := suite.quotaManager(nonExistRefID)
	infinite := types.ResourceList{types.ResourceCount: -1, types.ResourceStorage: -1}
	usage := types.ResourceList{types.ResourceCount: 10, types.ResourceStorage: 10}
	err := mgr.EnsureQuota(usage)
	suite.Nil(err)
	query := &models.QuotaQuery{
		Reference:   reference,
		ReferenceID: nonExistRefID,
	}
	quotas, err := dao.ListQuotas(query)
	suite.Nil(err)
	suite.Equal(usage, mustResourceList(quotas[0].Used))
	suite.Equal(infinite, mustResourceList(quotas[0].Hard))

	// existent
	existRefID := "4"
	mgr = suite.quotaManager(existRefID)
	used := types.ResourceList{types.ResourceCount: 11, types.ResourceStorage: 11}
	if id, err := mgr.NewQuota(hardLimits, used); suite.Nil(err) {
		quota, _ := dao.GetQuota(id)
		suite.Equal(hardLimits, mustResourceList(quota.Hard))

		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(used, mustResourceList(usage.Used))
	}

	usage2 := types.ResourceList{types.ResourceCount: 12, types.ResourceStorage: 12}
	err = mgr.EnsureQuota(usage2)
	suite.Nil(err)
	query2 := &models.QuotaQuery{
		Reference:   reference,
		ReferenceID: existRefID,
	}
	quotas2, err := dao.ListQuotas(query2)
	suite.Equal(usage2, mustResourceList(quotas2[0].Used))
	suite.Equal(hardLimits, mustResourceList(quotas2[0].Hard))

}

func (suite *ManagerSuite) TestQuotaAutoCreation() {
	for i := 0; i < 10; i++ {
		mgr := suite.quotaManager(fmt.Sprintf("%d", i))
		resource := types.ResourceList{types.ResourceCount: 0, types.ResourceStorage: 100}

		suite.Nil(mgr.AddResources(resource))
	}
}

func (suite *ManagerSuite) TestAddResources() {
	mgr := suite.quotaManager()
	id, _ := mgr.NewQuota(hardLimits)

	resource := types.ResourceList{types.ResourceCount: 0, types.ResourceStorage: 100}

	if suite.Nil(mgr.AddResources(resource)) {
		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(resource, mustResourceList(usage.Used))
	}

	if suite.Nil(mgr.AddResources(resource)) {
		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(types.ResourceList{types.ResourceCount: 0, types.ResourceStorage: 200}, mustResourceList(usage.Used))
	}

	if err := mgr.AddResources(types.ResourceList{types.ResourceStorage: 10000}); suite.Error(err) {
		if errs, ok := err.(Errors); suite.True(ok) {
			for _, err := range errs {
				suite.IsType(&ResourceOverflow{}, err)
			}
		}
	}
}

func (suite *ManagerSuite) TestSubtractResources() {
	mgr := suite.quotaManager()
	id, _ := mgr.NewQuota(hardLimits)

	resource := types.ResourceList{types.ResourceCount: 0, types.ResourceStorage: 100}

	if suite.Nil(mgr.AddResources(resource)) {
		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(resource, mustResourceList(usage.Used))
	}

	if suite.Nil(mgr.SubtractResources(resource)) {
		usage, _ := dao.GetQuotaUsage(id)
		suite.Equal(types.ResourceList{types.ResourceCount: 0, types.ResourceStorage: 0}, mustResourceList(usage.Used))
	}
}

func (suite *ManagerSuite) TestRaceAddResources() {
	mgr := suite.quotaManager()
	mgr.NewQuota(hardLimits)

	resources := types.ResourceList{
		types.ResourceStorage: 100,
	}

	var wg sync.WaitGroup

	results := make([]bool, 100)
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()

			results[i] = mgr.AddResources(resources) == nil
		}(i)
	}
	wg.Wait()

	var success int
	for _, result := range results {
		if result {
			success++
		}
	}

	suite.Equal(10, success)
}

func (suite *ManagerSuite) TestRaceSubtractResources() {
	mgr := suite.quotaManager()
	mgr.NewQuota(hardLimits, types.ResourceList{types.ResourceStorage: 1000})

	resources := types.ResourceList{
		types.ResourceStorage: 100,
	}

	var wg sync.WaitGroup

	results := make([]bool, 100)
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()

			results[i] = mgr.SubtractResources(resources) == nil
		}(i)
	}
	wg.Wait()

	var success int
	for _, result := range results {
		if result {
			success++
		}
	}

	suite.Equal(10, success)
}

func TestMain(m *testing.M) {
	dao.PrepareTestForPostgresSQL()

	if result := m.Run(); result != 0 {
		os.Exit(result)
	}
}

func TestRunManagerSuite(t *testing.T) {
	suite.Run(t, new(ManagerSuite))
}

func BenchmarkAddResources(b *testing.B) {
	defer func() {
		dao.ClearTable("quota")
		dao.ClearTable("quota_usage")
	}()

	mgr, _ := NewManager(reference, "1")
	mgr.NewQuota(types.ResourceList{types.ResourceStorage: int64(b.N)})

	resource := types.ResourceList{
		types.ResourceStorage: 1,
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		mgr.AddResources(resource)
	}
	b.StopTimer()
}

func BenchmarkAddResourcesParallel(b *testing.B) {
	defer func() {
		dao.ClearTable("quota")
		dao.ClearTable("quota_usage")
	}()

	mgr, _ := NewManager(reference, "1")
	mgr.NewQuota(types.ResourceList{})

	resource := types.ResourceList{
		types.ResourceStorage: 1,
	}

	b.ResetTimer()
	b.RunParallel(func(b *testing.PB) {
		for b.Next() {
			mgr.AddResources(resource)
		}
	})
	b.StopTimer()
}
