// 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"
)

// Assert that putRule() creates a new user-defined rule and adds to manifest
func TestCreateUserRule(t *testing.T) {
	resARN := "*"
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	rules := []*CentralizedRule{r1, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// New CentralizedSamplingRule. Input to putRule().
	serviceName := "www.foo.com"
	httpMethod := "POST"
	urlPath := "/bar/*"
	reservoirSize := int64(10)
	fixedRate := float64(0.05)
	ruleName := "r2"
	host := "local"
	priority := int64(6)
	serviceTye := "*"
	new := &SamplingRule{
		ServiceName:   &serviceName,
		HTTPMethod:    &httpMethod,
		URLPath:       &urlPath,
		ReservoirSize: &reservoirSize,
		FixedRate:     &fixedRate,
		RuleName:      &ruleName,
		Priority:      &priority,
		Host:          &host,
		ServiceType:   &serviceTye,
		ResourceARN:   &resARN,
	}

	// Expected centralized sampling rule
	clock := &utils.DefaultClock{}
	rand := &utils.DefaultRand{}

	p := &Properties{
		ServiceName: serviceName,
		HTTPMethod:  httpMethod,
		URLPath:     urlPath,
		FixedTarget: reservoirSize,
		Rate:        fixedRate,
		Host:        host,
	}

	cr := &CentralizedReservoir{
		reservoir: &reservoir{
			capacity: 10,
		},
		interval: 10,
	}

	exp := &CentralizedRule{
		reservoir:   cr,
		ruleName:    ruleName,
		priority:    priority,
		Properties:  p,
		clock:       clock,
		rand:        rand,
		serviceType: serviceTye,
		resourceARN: resARN,
	}

	// Add to manifest, index, and sort
	r2, err := m.putRule(new)
	assert.Nil(t, err)
	assert.Equal(t, exp, r2)

	// Assert new rule is present in index
	r2, ok := m.Index["r2"]
	assert.True(t, ok)
	assert.Equal(t, exp, r2)

	// Assert new rule present at end of array. putRule() does not preserve order.
	r2 = m.Rules[2]
	assert.Equal(t, exp, r2)
}

// Assert that putRule() creates a new default rule and adds to manifest
func TestCreateDefaultRule(t *testing.T) {
	m := &CentralizedManifest{
		Index: map[string]*CentralizedRule{},
	}

	// New CentralizedSamplingRule. Input to putRule().
	reservoirSize := int64(10)
	fixedRate := float64(0.05)
	ruleName := "Default"
	new := &SamplingRule{
		ReservoirSize: &reservoirSize,
		FixedRate:     &fixedRate,
		RuleName:      &ruleName,
	}

	// Expected centralized sampling rule
	clock := &utils.DefaultClock{}
	rand := &utils.DefaultRand{}

	p := &Properties{
		FixedTarget: reservoirSize,
		Rate:        fixedRate,
	}

	cr := &CentralizedReservoir{
		reservoir: &reservoir{
			capacity: reservoirSize,
		},
		interval: 10,
	}

	exp := &CentralizedRule{
		reservoir:  cr,
		ruleName:   ruleName,
		Properties: p,
		clock:      clock,
		rand:       rand,
	}

	// Add to manifest
	r, err := m.putRule(new)
	assert.Nil(t, err)
	assert.Equal(t, exp, r)
	assert.Equal(t, exp, m.Default)
}

// Assert that putRule() creates a new default rule and adds to manifest
func TestUpdateDefaultRule(t *testing.T) {
	clock := &utils.DefaultClock{}
	rand := &utils.DefaultRand{}

	// Original default sampling rule
	r := &CentralizedRule{
		ruleName: "Default",
		Properties: &Properties{
			FixedTarget: 10,
			Rate:        0.05,
		},
		reservoir: &CentralizedReservoir{
			reservoir: &reservoir{
				capacity: 10,
			},
		},
		clock: clock,
		rand:  rand,
	}

	m := &CentralizedManifest{
		Default: r,
	}

	// Updated CentralizedSamplingRule. Input to putRule().
	reservoirSize := int64(20)
	fixedRate := float64(0.06)
	ruleName := "Default"
	updated := &SamplingRule{
		ReservoirSize: &reservoirSize,
		FixedRate:     &fixedRate,
		RuleName:      &ruleName,
	}

	// Expected centralized sampling rule
	p := &Properties{
		FixedTarget: reservoirSize,
		Rate:        fixedRate,
	}

	cr := &CentralizedReservoir{
		reservoir: &reservoir{
			capacity: reservoirSize,
		},
	}

	exp := &CentralizedRule{
		reservoir:  cr,
		ruleName:   ruleName,
		Properties: p,
		clock:      clock,
		rand:       rand,
	}

	// Update default rule in manifest
	r, err := m.putRule(updated)
	assert.Nil(t, err)
	assert.Equal(t, exp, r)
	assert.Equal(t, exp, m.Default)
}

// Assert that creating a user-defined rule which already exists is a no-op
func TestCreateUserRuleNoOp(t *testing.T) {
	resARN := "*"
	serviceTye := ""
	attributes := make(map[string]*string)
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
		reservoir: &CentralizedReservoir{
			reservoir: &reservoir{},
		},
	}

	rules := []*CentralizedRule{r1, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Duplicate CentralizedSamplingRule. 'r3' already exists. Input to updateRule().
	serviceName := "www.foo.com"
	httpMethod := "POST"
	urlPath := "/bar/*"
	reservoirSize := int64(10)
	fixedRate := float64(0.05)
	ruleName := "r3"
	priority := int64(6)
	host := "h"
	new := &SamplingRule{
		ServiceName:   &serviceName,
		HTTPMethod:    &httpMethod,
		URLPath:       &urlPath,
		ReservoirSize: &reservoirSize,
		FixedRate:     &fixedRate,
		RuleName:      &ruleName,
		Priority:      &priority,
		Host:          &host,
		ResourceARN:   &resARN,
		ServiceType:   &serviceTye,
		Attributes:    attributes,
	}

	// Assert manifest has not changed
	r, err := m.putRule(new)
	assert.Nil(t, err)
	assert.Equal(t, r3, r)
	assert.Equal(t, 2, len(m.Rules))
	assert.Equal(t, 2, len(m.Index))
	assert.Equal(t, r1, m.Rules[0])
	assert.Equal(t, r3, m.Rules[1])
}

// Assert that putRule() updates the user-defined rule in the manifest
func TestUpdateUserRule(t *testing.T) {
	resARN := "*"
	serviceTye := ""
	attributes := make(map[string]*string)
	// Original rule
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
		Properties: &Properties{
			ServiceName: "*.foo.com",
			HTTPMethod:  "GET",
			URLPath:     "/resource/*",
			FixedTarget: 15,
			Rate:        0.04,
		},
		reservoir: &CentralizedReservoir{
			reservoir: &reservoir{
				capacity: 5,
			},
		},
		resourceARN: resARN,
		serviceType: serviceTye,
		attributes:  attributes,
	}

	rules := []*CentralizedRule{r1}

	index := map[string]*CentralizedRule{
		"r1": r1,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Updated CentralizedSamplingRule. Input to updateRule().
	serviceName := "www.foo.com"
	httpMethod := "POST"
	urlPath := "/bar/*"
	reservoirSize := int64(10)
	fixedRate := float64(0.05)
	ruleName := "r1"
	priority := int64(6)
	host := "h"
	updated := &SamplingRule{
		ServiceName:   &serviceName,
		HTTPMethod:    &httpMethod,
		URLPath:       &urlPath,
		ReservoirSize: &reservoirSize,
		FixedRate:     &fixedRate,
		RuleName:      &ruleName,
		Priority:      &priority,
		Host:          &host,
		ResourceARN:   &resARN,
		ServiceType:   &serviceTye,
		Attributes:    attributes,
	}

	// Expected updated centralized sampling rule
	p := &Properties{
		ServiceName: serviceName,
		HTTPMethod:  httpMethod,
		URLPath:     urlPath,
		FixedTarget: reservoirSize,
		Rate:        fixedRate,
		Host:        host,
	}

	cr := &CentralizedReservoir{
		reservoir: &reservoir{
			capacity: 10,
		},
	}

	exp := &CentralizedRule{
		reservoir:   cr,
		ruleName:    ruleName,
		priority:    priority,
		Properties:  p,
		resourceARN: resARN,
		serviceType: serviceTye,
		attributes:  attributes,
	}

	// Assert that rule has been updated
	r, err := m.putRule(updated)
	assert.Nil(t, err)
	assert.Equal(t, exp, r)
	assert.Equal(t, exp, m.Index["r1"])
	assert.Equal(t, exp, m.Rules[0])
	assert.Equal(t, 1, len(m.Rules))
	assert.Equal(t, 1, len(m.Index))
}

// Assert that putRule() recovers from panic.
func TestPutRuleRecovery(t *testing.T) {
	resARN := "*"
	serviceTye := ""
	attributes := make(map[string]*string)
	rules := []*CentralizedRule{}

	index := map[string]*CentralizedRule{}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Invalid CentralizedSamplingRule with nil fileds. Input to putRule().
	serviceName := "www.foo.com"
	httpMethod := "POST"
	fixedRate := float64(0.05)
	ruleName := "r2"
	priority := int64(6)
	new := &SamplingRule{
		ServiceName: &serviceName,
		HTTPMethod:  &httpMethod,
		FixedRate:   &fixedRate,
		RuleName:    &ruleName,
		Priority:    &priority,
		ResourceARN: &resARN,
		ServiceType: &serviceTye,
		Attributes:  attributes,
	}

	// Attempt to add to manifest
	r, err := m.putRule(new)
	assert.NotNil(t, err)
	assert.Nil(t, r)
	assert.Nil(t, m.Default)

	// Assert index is unchanged
	assert.Equal(t, 0, len(m.Index))

	// Assert sorted array is unchanged
	assert.Equal(t, 0, len(m.Rules))
}

// Assert that deleting a rule from the end of the array removes the rule
// and preserves ordering of the sorted array
func TestDeleteLastRule(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r2 := &CentralizedRule{
		ruleName: "r2",
		priority: 6,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	rules := []*CentralizedRule{r1, r2, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r2": r2,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Active rules to exclude from deletion
	a := map[*CentralizedRule]bool{
		r1: true,
		r2: true,
	}

	// Delete r3
	m.prune(a)

	// Assert size of manifest
	assert.Equal(t, 2, len(m.Rules))
	assert.Equal(t, 2, len(m.Index))

	// Assert index consistency
	_, ok := m.Index["r3"]
	assert.False(t, ok)
	assert.Equal(t, r1, m.Index["r1"])
	assert.Equal(t, r2, m.Index["r2"])

	// Assert ordering of array
	assert.Equal(t, r1, m.Rules[0])
	assert.Equal(t, r2, m.Rules[1])
}

// Assert that deleting a rule from the middle of the array removes the rule
// and preserves ordering of the sorted array
func TestDeleteMiddleRule(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r2 := &CentralizedRule{
		ruleName: "r2",
		priority: 6,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	rules := []*CentralizedRule{r1, r2, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r2": r2,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Active rules to exclude from deletion
	a := map[*CentralizedRule]bool{
		r1: true,
		r3: true,
	}

	// Delete r2
	m.prune(a)

	// Assert size of manifest
	assert.Equal(t, 2, len(m.Rules))
	assert.Equal(t, 2, len(m.Index))

	// Assert index consistency
	_, ok := m.Index["r2"]
	assert.False(t, ok)
	assert.Equal(t, r1, m.Index["r1"])
	assert.Equal(t, r3, m.Index["r3"])

	// Assert ordering of array
	assert.Equal(t, r1, m.Rules[0])
	assert.Equal(t, r3, m.Rules[1])
}

// Assert that deleting a rule from the beginning of the array removes the rule
// and preserves ordering of the sorted array
func TestDeleteFirstRule(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r2 := &CentralizedRule{
		ruleName: "r2",
		priority: 6,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	rules := []*CentralizedRule{r1, r2, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r2": r2,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Active rules to exclude from deletion
	a := map[*CentralizedRule]bool{
		r2: true,
		r3: true,
	}

	// Delete r1
	m.prune(a)

	// Assert size of manifest
	assert.Equal(t, 2, len(m.Rules))
	assert.Equal(t, 2, len(m.Index))

	// Assert index consistency
	_, ok := m.Index["r1"]
	assert.False(t, ok)
	assert.Equal(t, r2, m.Index["r2"])
	assert.Equal(t, r3, m.Index["r3"])

	// Assert ordering of array
	assert.Equal(t, r2, m.Rules[0])
	assert.Equal(t, r3, m.Rules[1])
}

// Assert that deleting the only rule from the array removes the rule
func TestDeleteOnlyRule(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	rules := []*CentralizedRule{r1}

	index := map[string]*CentralizedRule{
		"r1": r1,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Active rules to exclude from deletion
	a := map[*CentralizedRule]bool{}

	// Delete r1
	m.prune(a)

	// Assert size of manifest
	assert.Equal(t, 0, len(m.Rules))
	assert.Equal(t, 0, len(m.Index))

	// Assert index consistency
	_, ok := m.Index["r1"]
	assert.False(t, ok)
}

// Assert that deleting rules from an empty array does not panic
func TestDeleteEmptyRulesArray(t *testing.T) {
	rules := []*CentralizedRule{}

	index := map[string]*CentralizedRule{}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Active rules to exclude from deletion
	a := map[*CentralizedRule]bool{}

	// Delete from empty array
	m.prune(a)

	// Assert size of manifest
	assert.Equal(t, 0, len(m.Rules))
	assert.Equal(t, 0, len(m.Index))
}

// Assert that deleting all rules results in an empty array and does not panic
func TestDeleteAllRules(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r2 := &CentralizedRule{
		ruleName: "r2",
		priority: 6,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	rules := []*CentralizedRule{r1, r2, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r2": r2,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}

	// Active rules to exclude from deletion
	a := map[*CentralizedRule]bool{}

	// Delete r3
	m.prune(a)

	// Assert size of manifest
	assert.Equal(t, 0, len(m.Rules))
	assert.Equal(t, 0, len(m.Index))
}

// Assert that sorting an unsorted array results in a sorted array - check priority
func TestSort1(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r2 := &CentralizedRule{
		ruleName: "r2",
		priority: 6,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	// Unsorted rules array
	rules := []*CentralizedRule{r2, r1, r3}

	m := &CentralizedManifest{
		Rules: rules,
	}

	// Sort array
	m.sort()

	// Assert on order
	assert.Equal(t, r1, m.Rules[0])
	assert.Equal(t, r2, m.Rules[1])
	assert.Equal(t, r3, m.Rules[2])
}

// Assert that sorting an unsorted array results in a sorted array - check priority and rule name
func TestSort2(t *testing.T) {
	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r2 := &CentralizedRule{
		ruleName: "r2",
		priority: 5,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	// Unsorted rules array
	rules := []*CentralizedRule{r2, r1, r3}

	m := &CentralizedManifest{
		Rules: rules,
	}

	// Sort array
	m.sort() // r1 should precede r2

	// Assert on order
	assert.Equal(t, r1, m.Rules[0])
	assert.Equal(t, r2, m.Rules[1])
	assert.Equal(t, r3, m.Rules[2])
}

// Assert that an expired manifest is recognized as such
func TestExpired(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500003601,
	}

	m := &CentralizedManifest{
		refreshedAt: 1500000000,
		clock:       clock,
	}

	assert.True(t, m.expired())
}

// Assert that a fresh manifest is recognized as such
func TestFresh(t *testing.T) {
	clock := &utils.MockClock{
		NowTime: 1500003600,
	}

	m := &CentralizedManifest{
		refreshedAt: 1500000000,
		clock:       clock,
	}

	assert.False(t, m.expired())
}

// benchmarks
func BenchmarkCentralizedManifest_putRule(b *testing.B) {

	r1 := &CentralizedRule{
		ruleName: "r1",
		priority: 5,
	}

	r3 := &CentralizedRule{
		ruleName: "r3",
		priority: 7,
	}

	rules := []*CentralizedRule{r1, r3}

	index := map[string]*CentralizedRule{
		"r1": r1,
		"r3": r3,
	}

	m := &CentralizedManifest{
		Rules: rules,
		Index: index,
	}
	// New CentralizedSamplingRule. Input to putRule().
	resARN := "*"
	serviceName := "www.foo.com"
	httpMethod := "POST"
	urlPath := "/bar/*"
	reservoirSize := int64(10)
	fixedRate := float64(0.05)
	ruleName := "r2"
	host := "local"
	priority := int64(6)
	serviceTye := "*"
	new := &SamplingRule{
		ServiceName:   &serviceName,
		HTTPMethod:    &httpMethod,
		URLPath:       &urlPath,
		ReservoirSize: &reservoirSize,
		FixedRate:     &fixedRate,
		RuleName:      &ruleName,
		Priority:      &priority,
		Host:          &host,
		ServiceType:   &serviceTye,
		ResourceARN:   &resARN,
	}
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			_, err := m.putRule(new)
			if err != nil {
				return
			}
		}
	})

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			m.createUserRule(new)
		}
	})
}
