package ratecontroller

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/golang/mock/gomock"
	. "github.com/smartystreets/goconvey/convey"
	"github.com/tidbcloud/account/pkg/ratecontroller/mock_ratecontroller"
	"github.com/tidbcloud/account/pkg/ratecontroller/types"
)

const (
	accessor = "this is accessor"
	target   = "this is target"
)

func Test_rateControllerWithScene_Access(t *testing.T) {
	Convey("Test_rateControllerWithScene_Access", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		scene := &types.Scene{
			SceneKey: "test",
			Capacity: 10,
			TTL:      10 * time.Millisecond,
		}
		ctx := context.Background()
		target := "this is target"
		now := time.Now()
		mock_store := mock_ratecontroller.NewMockStore(ctrl)
		rcm := NewRateController(mock_store)
		rc := rcm.WithScene(scene)

		Convey("should be success when all is well", func() {
			mock_store.EXPECT().AddEntry(ctx, scene, accessor, target, now)
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL))
			mock_store.EXPECT().PurgeOverflowed(ctx, scene, accessor)

			rc.Access(ctx, accessor, target, now)
		})
		Convey("should be success when store.AddEntry failed", func() {
			mock_store.EXPECT().AddEntry(ctx, scene, accessor, target, gomock.Any()).Return(fmt.Errorf("some error"))
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL))
			mock_store.EXPECT().PurgeOverflowed(ctx, scene, accessor)

			rc.Access(ctx, accessor, target, now)
		})
		Convey("should be success when store.PurgeExpired failed", func() {
			mock_store.EXPECT().AddEntry(ctx, scene, accessor, target, gomock.Any())
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL)).Return(fmt.Errorf("some error"))

			rc.Access(ctx, accessor, target, now)
		})
		Convey("should be success when store.PurgeOverflowed failed", func() {
			mock_store.EXPECT().AddEntry(ctx, scene, accessor, target, gomock.Any())
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL))
			mock_store.EXPECT().PurgeOverflowed(ctx, scene, accessor).Return(fmt.Errorf("some error"))

			rc.Access(ctx, accessor, target, now)
		})
	})
}

func Test_rateControllerWithScene_purge(t *testing.T) {
	Convey("Test_rateControllerWithScene_purge", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		scene := &types.Scene{
			SceneKey: "test",
			Capacity: 10,
			TTL:      10 * time.Millisecond,
		}
		ctx := context.Background()
		now := time.Now()
		mock_store := mock_ratecontroller.NewMockStore(ctrl)
		rcm := NewRateController(mock_store)
		rc := rcm.WithScene(scene)

		Convey("should be success when all is well", func() {
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL))
			mock_store.EXPECT().PurgeOverflowed(ctx, scene, accessor)

			err := rc.purge(ctx, accessor, now)
			So(err, ShouldBeNil)
		})
		Convey("should skip store.PurgeExpired when scene.TTL is zero", func() {
			scene.TTL = 0
			mock_store.EXPECT().PurgeOverflowed(ctx, scene, accessor)

			err := rc.purge(ctx, accessor, now)
			So(err, ShouldBeNil)
		})
		Convey("should skip store.PurgeOverflowed when scene.Capacity is zero", func() {
			scene.Capacity = 0
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL))

			err := rc.purge(ctx, accessor, now)
			So(err, ShouldBeNil)
		})
		Convey("should be failure when purgeExpired failed", func() {
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL)).Return(fmt.Errorf("some error"))

			err := rc.purge(ctx, accessor, now)
			So(err, ShouldNotBeNil)
		})
		Convey("should be failure when purgeOverflowed failed", func() {
			mock_store.EXPECT().PurgeExpired(ctx, scene, accessor, now.Add(-scene.TTL))
			mock_store.EXPECT().PurgeOverflowed(ctx, scene, accessor).Return(fmt.Errorf("some error"))

			err := rc.purge(ctx, accessor, now)
			So(err, ShouldNotBeNil)
		})
	})
}

func Test_rateControllerWithScene_AllowAll(t *testing.T) {
	Convey("Test_rateControllerWithScene_AllowAll", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		scene := &types.Scene{
			SceneKey: "test",
			Capacity: 10,
			TTL:      10 * time.Minute,
		}
		rate1 := &types.Rate{
			Period: 1 * time.Minute,
			Limit:  5,
		}
		rate2 := &types.Rate{
			Period: 10 * time.Second,
			Limit:  2,
		}
		rate3 := &types.Rate{
			Period: 5 * time.Second,
			Limit:  1,
		}
		ctx := context.Background()
		now := time.Now()
		mock_store := mock_ratecontroller.NewMockStore(ctrl)
		rcm := NewRateController(mock_store)
		rc := rcm.WithScene(scene)

		Convey("should be success when all is well", func() {
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate1.Period), now).Return(int64(2), nil)
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate2.Period), now).Return(int64(1), nil)

			r1 := rc.AllowAll(ctx, accessor, target, now, rate1, rate2)
			So(r1, ShouldBeTrue)
		})
		Convey("should return false when have 2 access log in last 10 seconds ", func() {
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate1.Period), now).Return(int64(2), nil)
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate2.Period), now).Return(int64(2), nil)

			r1 := rc.AllowAll(ctx, accessor, target, now, rate1, rate2)
			So(r1, ShouldBeFalse)
		})
		Convey("should return false when have 6 access log in last 1 minute ", func() {
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate1.Period), now).Return(int64(6), nil)

			r1 := rc.AllowAll(ctx, accessor, target, now, rate1, rate2)
			So(r1, ShouldBeFalse)
		})
		Convey("should return true when some Count occur err but others success", func() {
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate1.Period), now).Return(int64(0), fmt.Errorf("err"))
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate2.Period), now).Return(int64(1), nil)
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate3.Period), now).Return(int64(0), nil)

			r1 := rc.AllowAll(ctx, accessor, target, now, rate1, rate2, rate3)
			So(r1, ShouldBeTrue)
		})
		Convey("should return true when ALL Count occur error", func() {
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate1.Period), now).Return(int64(0), fmt.Errorf("err"))
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate2.Period), now).Return(int64(0), fmt.Errorf("err"))

			r1 := rc.AllowAll(ctx, accessor, target, now, rate1, rate2)
			So(r1, ShouldBeTrue)
		})
		Convey("should return false when some Count failed and any other occur err", func() {
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate1.Period), now).Return(int64(0), fmt.Errorf("err"))
			mock_store.EXPECT().Count(ctx, scene, accessor, target, now.Add(-rate2.Period), now).Return(int64(4), nil) // failed

			r1 := rc.AllowAll(ctx, accessor, target, now, rate1, rate2, rate3)
			So(r1, ShouldBeFalse)
		})
	})
}

func Test_rateControllerWithScene_validateRate(t *testing.T) {
	Convey("Test_rateControllerWithScene_AllowAll", t, func() {
		for _, each := range []struct {
			rate  *types.Rate
			scene *types.Scene
			panic bool
		}{
			{rate: &types.Rate{Limit: 1, Period: time.Second}, scene: &types.Scene{Capacity: 10, TTL: time.Minute}, panic: false},
			// rate.Limit bigger than scene.Capacity
			{rate: &types.Rate{Limit: 10, Period: time.Second}, scene: &types.Scene{Capacity: 8, TTL: time.Minute}, panic: true},
			// rate.Limit bigger than scene.Capacity, but scene.Capacity is 0
			{rate: &types.Rate{Limit: 10, Period: time.Second}, scene: &types.Scene{Capacity: 0, TTL: time.Minute}, panic: false},
			// rate.Period bigger than scene.TTL
			{rate: &types.Rate{Limit: 10, Period: time.Second}, scene: &types.Scene{Capacity: 8, TTL: time.Millisecond}, panic: true},
			// rate.Period bigger than scene.TTL, but scene.TTL is 0
			{rate: &types.Rate{Limit: 10, Period: time.Second}, scene: &types.Scene{Capacity: 100, TTL: 0}, panic: false},
		} {
			rc := &rateControllerWithScene{scene: each.scene}
			if each.panic {
				So(func() {
					rc.validateRate(each.rate)
				}, ShouldPanic)
			} else {
				So(func() {
					rc.validateRate(each.rate)
				}, ShouldNotPanic)
			}
		}
	})
}
