// Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at
//
//     http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 sampling

import (
	"testing"

	"github.com/aws/aws-xray-sdk-go/v2/utils"
	"github.com/stretchr/testify/assert"
)

func TestStaleRule(t *testing.T) {
	cr := &CentralizedRule{
		requests: 5,
		reservoir: &CentralizedReservoir{
			refreshedAt: 1500000000,
			interval:    10,
		},
	}

	s := cr.stale(1500000010)
	assert.True(t, s)
}

func TestFreshRule(t *testing.T) {
	cr := &CentralizedRule{
		requests: 5,
		reservoir: &CentralizedReservoir{
			refreshedAt: 1500000000,
			interval:    10,
		},
	}

	s := cr.stale(1500000009)
	assert.False(t, s)
}

func TestInactiveRule(t *testing.T) {
	cr := &CentralizedRule{
		requests: 0,
		reservoir: &CentralizedReservoir{
			refreshedAt: 1500000000,
			interval:    10,
		},
	}

	s := cr.stale(1500000011)
	assert.False(t, s)
}

func TestExpiredReservoirBernoulliSample(t *testing.T) {
	// One second past expiration
	clock := &utils.MockClock{
		NowTime: 1500000061,
	}

	// Set random to be within sampling rate
	rand := &utils.MockRand{
		F64: 0.05,
	}

	p := &Properties{
		Rate: 0.06,
	}

	// Expired reservoir
	cr := &CentralizedReservoir{
		expiresAt: 1500000060,
		borrowed:  true,
		reservoir: &reservoir{
			used:         0,
			capacity:     10,
			currentEpoch: 1500000061,
		},
	}

	csr := &CentralizedRule{
		ruleName:   "r1",
		reservoir:  cr,
		Properties: p,
		clock:      clock,
		rand:       rand,
	}

	sd := csr.Sample()

	assert.True(t, sd.Sample)
	assert.Equal(t, "r1", *sd.Rule)
	assert.Equal(t, int64(1), csr.sampled)
	assert.Equal(t, int64(1), csr.requests)
}

func TestTakeFromQuotaSample(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500000000,
	}

	// Reservoir with unused quota
	r := &reservoir{
		currentEpoch: clock.Now().Unix(),
		used:         0,
	}

	cr := &CentralizedReservoir{
		quota:     10,
		expiresAt: 1500000060,
		reservoir: r,
	}

	csr := &CentralizedRule{
		ruleName:  "r1",
		reservoir: cr,
		clock:     clock,
	}

	sd := csr.Sample()

	assert.True(t, sd.Sample)
	assert.Equal(t, "r1", *sd.Rule)
	assert.Equal(t, int64(1), csr.sampled)
	assert.Equal(t, int64(1), csr.requests)
	assert.Equal(t, int64(1), csr.reservoir.used)
}

func TestBernoulliSamplePositve(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500000000,
	}

	// Reservoir with unused quota
	r := &reservoir{
		currentEpoch: clock.Now().Unix(),
		used:         10,
	}

	// Set random to be within sampling rate
	rand := &utils.MockRand{
		F64: 0.05,
	}

	p := &Properties{
		Rate: 0.06,
	}

	cr := &CentralizedReservoir{
		quota:     10,
		expiresAt: 1500000060,
		reservoir: r,
	}

	csr := &CentralizedRule{
		ruleName:   "r1",
		reservoir:  cr,
		Properties: p,
		rand:       rand,
		clock:      clock,
	}

	sd := csr.Sample()

	assert.True(t, sd.Sample)
	assert.Equal(t, "r1", *sd.Rule)
	assert.Equal(t, int64(1), csr.sampled)
	assert.Equal(t, int64(1), csr.requests)
	assert.Equal(t, int64(10), csr.reservoir.used)
}

func TestBernoulliSampleNegative(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500000000,
	}

	// Reservoir with unused quota
	r := &reservoir{
		currentEpoch: clock.Now().Unix(),
		used:         10,
	}

	// Set random to be outside sampling rate
	rand := &utils.MockRand{
		F64: 0.07,
	}

	p := &Properties{
		Rate: 0.06,
	}

	cr := &CentralizedReservoir{
		quota:     10,
		expiresAt: 1500000060,
		reservoir: r,
	}

	csr := &CentralizedRule{
		ruleName:   "r1",
		reservoir:  cr,
		Properties: p,
		rand:       rand,
		clock:      clock,
	}

	sd := csr.Sample()

	assert.False(t, sd.Sample)
	assert.Equal(t, "r1", *sd.Rule)
	assert.Equal(t, int64(0), csr.sampled)
	assert.Equal(t, int64(1), csr.requests)
	assert.Equal(t, int64(10), csr.reservoir.used)
}

// Test sampling from local reservoir
func TestReservoirSample(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500000000,
	}

	r := &reservoir{
		capacity:     10,
		used:         5,
		currentEpoch: 1500000000,
	}

	lr := &Reservoir{
		clock:     clock,
		reservoir: r,
	}

	lsr := &Rule{
		reservoir: lr,
	}

	sd := lsr.Sample()

	assert.True(t, sd.Sample)
	assert.Nil(t, sd.Rule)
	assert.Equal(t, int64(6), lsr.reservoir.used)
}

// Test bernoulli sampling for local sampling rule
func TestLocalBernoulliSample(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500000000,
	}

	r := &reservoir{
		capacity:     10,
		used:         10,
		currentEpoch: 1500000000,
	}

	lr := &Reservoir{
		clock:     clock,
		reservoir: r,
	}

	// 6% sampling rate
	p := &Properties{
		Rate: 0.06,
	}

	// Set random to be outside sampling rate
	rand := &utils.MockRand{
		F64: 0.07,
	}

	lsr := &Rule{
		reservoir:  lr,
		rand:       rand,
		Properties: p,
	}

	sd := lsr.Sample()

	assert.False(t, sd.Sample)
	assert.Nil(t, sd.Rule)
	assert.Equal(t, int64(10), lsr.reservoir.used)
}

func TestSnapshot(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500000000,
	}

	csr := &CentralizedRule{
		ruleName: "rule1",
		requests: 100,
		sampled:  12,
		borrows:  2,
		clock:    clock,
	}

	ss := csr.snapshot()

	// Assert counters were reset
	assert.Equal(t, int64(0), csr.requests)
	assert.Equal(t, int64(0), csr.sampled)
	assert.Equal(t, int64(0), csr.borrows)

	// Assert on SamplingStatistics counters
	now := int64(1500000000)

	assert.Equal(t, int64(100), *ss.RequestCount)
	assert.Equal(t, int64(12), *ss.SampledCount)
	assert.Equal(t, int64(2), *ss.BorrowCount)
	assert.Equal(t, "rule1", *ss.RuleName)
	assert.Equal(t, now, *ss.Timestamp)
}

// Benchmarks
func BenchmarkCentralizedRule_Sample(b *testing.B) {

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			clock := &utils.MockClock{
				NowTime: 1500000000,
			}

			// Reservoir with unused quota
			r := &reservoir{
				currentEpoch: clock.Now().Unix(),
				used:         0,
			}

			cr := &CentralizedReservoir{
				quota:     10,
				expiresAt: 1500000060,
				reservoir: r,
			}

			csr := &CentralizedRule{
				ruleName:  "r1",
				reservoir: cr,
				clock:     clock,
			}
			csr.Sample()
		}
	})
}
