/*
 *
 * Copyright 2020 gRPC 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 adaptive

import (
	"sync"
	"testing"
	"time"
)

// stats returns a tuple with accepts, throttles for the current time.
func (th *Throttler) stats() (int64, int64) {
	now := timeNowFunc()

	th.mu.Lock()
	a, t := th.accepts.sum(now), th.throttles.sum(now)
	th.mu.Unlock()
	return a, t
}

// Enums for responses.
const (
	E = iota // No response
	A        // Accepted
	T        // Throttled
)

func TestRegisterBackendResponse(t *testing.T) {
	testcases := []struct {
		desc          string
		bins          int64
		ticks         []int64
		responses     []int64
		wantAccepts   []int64
		wantThrottled []int64
	}{
		{
			"Accumulate",
			3,
			[]int64{0, 1, 2}, // Ticks
			[]int64{A, T, E}, // Responses
			[]int64{1, 1, 1}, // Accepts
			[]int64{0, 1, 1}, // Throttled
		},
		{
			"LightTimeTravel",
			3,
			[]int64{1, 0, 2}, // Ticks
			[]int64{A, T, E}, // Response
			[]int64{1, 1, 1}, // Accepts
			[]int64{0, 1, 1}, // Throttled
		},
		{
			"HeavyTimeTravel",
			3,
			[]int64{8, 0, 9}, // Ticks
			[]int64{A, A, A}, // Response
			[]int64{1, 1, 2}, // Accepts
			[]int64{0, 0, 0}, // Throttled
		},
		{
			"Rollover",
			1,
			[]int64{0, 1, 2}, // Ticks
			[]int64{A, T, E}, // Responses
			[]int64{1, 0, 0}, // Accepts
			[]int64{0, 1, 0}, // Throttled
		},
	}

	m := mockClock{}
	oldTimeNowFunc := timeNowFunc
	timeNowFunc = m.Now
	defer func() { timeNowFunc = oldTimeNowFunc }()

	for _, test := range testcases {
		t.Run(test.desc, func(t *testing.T) {
			th := newWithArgs(time.Duration(test.bins), test.bins, 2.0, 8)
			for i, tick := range test.ticks {
				m.SetNanos(tick)

				if test.responses[i] != E {
					th.RegisterBackendResponse(test.responses[i] == T)
				}

				if gotAccepts, gotThrottled := th.stats(); gotAccepts != test.wantAccepts[i] || gotThrottled != test.wantThrottled[i] {
					t.Errorf("th.stats() = {%d, %d} for index %d, want {%d, %d}", i, gotAccepts, gotThrottled, test.wantAccepts[i], test.wantThrottled[i])
				}
			}
		})
	}
}

func TestShouldThrottleOptions(t *testing.T) {
	// ShouldThrottle should return true iff
	//    (requests - RatioForAccepts * accepts) / (requests + RequestsPadding) <= p
	// where p is a random number. For the purposes of this test it's fixed
	// to 0.5.
	responses := []int64{T, T, T, T, T, T, T, T, T, A, A, A, A, A, A, T, T, T, T}

	n := false
	y := true

	testcases := []struct {
		desc            string
		ratioForAccepts float64
		requestsPadding float64
		want            []bool
	}{
		{
			"Baseline",
			1.1,
			8,
			[]bool{n, n, n, n, n, n, n, n, y, y, y, y, y, n, n, n, y, y, y},
		},
		{
			"ChangePadding",
			1.1,
			7,
			[]bool{n, n, n, n, n, n, n, y, y, y, y, y, y, y, y, y, y, y, y},
		},
		{
			"ChangeRatioForAccepts",
			1.4,
			8,
			[]bool{n, n, n, n, n, n, n, n, y, y, n, n, n, n, n, n, n, n, n},
		},
	}

	m := mockClock{}
	oldTimeNowFunc := timeNowFunc
	timeNowFunc = m.Now
	oldRandFunc := randFunc
	randFunc = func() float64 { return 0.5 }
	defer func() {
		timeNowFunc = oldTimeNowFunc
		randFunc = oldRandFunc
	}()

	for _, test := range testcases {
		t.Run(test.desc, func(t *testing.T) {
			m.SetNanos(0)
			th := newWithArgs(time.Duration(time.Nanosecond), 1, test.ratioForAccepts, test.requestsPadding)
			for i, response := range responses {
				if response != E {
					th.RegisterBackendResponse(response == T)
				}
				if got := th.ShouldThrottle(); got != test.want[i] {
					t.Errorf("ShouldThrottle for index %d: got %v, want %v", i, got, test.want[i])
				}
			}
		})
	}
}

func TestParallel(t *testing.T) {
	// Uses all the defaults which comes with a 30 second duration.
	th := New()

	testDuration := 2 * time.Second
	numRoutines := 10
	accepts := make([]int64, numRoutines)
	throttles := make([]int64, numRoutines)
	var wg sync.WaitGroup
	for i := 0; i < numRoutines; i++ {
		wg.Add(1)
		go func(num int) {
			defer wg.Done()

			ticker := time.NewTicker(testDuration)
			var accept int64
			var throttle int64
			for i := 0; ; i++ {
				select {
				case <-ticker.C:
					ticker.Stop()
					accepts[num] = accept
					throttles[num] = throttle
					return
				default:
					if i%2 == 0 {
						th.RegisterBackendResponse(true)
						throttle++
					} else {
						th.RegisterBackendResponse(false)
						accept++
					}
				}
			}
		}(i)
	}
	wg.Wait()

	var wantAccepts, wantThrottles int64
	for i := 0; i < numRoutines; i++ {
		wantAccepts += accepts[i]
		wantThrottles += throttles[i]
	}

	if gotAccepts, gotThrottles := th.stats(); gotAccepts != wantAccepts || gotThrottles != wantThrottles {
		t.Errorf("th.stats() = {%d, %d}, want {%d, %d}", gotAccepts, gotThrottles, wantAccepts, wantThrottles)
	}
}

type mockClock struct {
	t time.Time
}

func (m *mockClock) Now() time.Time {
	return m.t
}

func (m *mockClock) SetNanos(n int64) {
	m.t = time.Unix(0, n)
}
