/*
Copyright 2015 The Kubernetes Authors 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.
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 queue

import (
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

const (
	tolerance = 100 * time.Millisecond // go time delays aren't perfect, this is our tolerance for errors WRT expected timeouts
)

func timedPriority(t time.Time) Priority {
	return Priority{ts: t}
}

func TestPQ(t *testing.T) {
	t.Parallel()

	var pq priorityQueue
	if pq.Len() != 0 {
		t.Fatalf("pq should be empty")
	}

	now := timedPriority(time.Now())
	now2 := timedPriority(now.ts.Add(2 * time.Second))
	pq.Push(&qitem{priority: now2})
	if pq.Len() != 1 {
		t.Fatalf("pq.len should be 1")
	}
	x := pq.Pop()
	if x == nil {
		t.Fatalf("x is nil")
	}
	if pq.Len() != 0 {
		t.Fatalf("pq should be empty")
	}
	item := x.(*qitem)
	if !item.priority.Equal(now2) {
		t.Fatalf("item.priority != now2")
	}

	pq.Push(&qitem{priority: now2})
	pq.Push(&qitem{priority: now2})
	pq.Push(&qitem{priority: now2})
	pq.Push(&qitem{priority: now2})
	pq.Push(&qitem{priority: now2})
	pq.Pop()
	pq.Pop()
	pq.Pop()
	pq.Pop()
	pq.Pop()
	if pq.Len() != 0 {
		t.Fatalf("pq should be empty")
	}
	now4 := timedPriority(now.ts.Add(4 * time.Second))
	now6 := timedPriority(now.ts.Add(4 * time.Second))
	pq.Push(&qitem{priority: now2})
	pq.Push(&qitem{priority: now4})
	pq.Push(&qitem{priority: now6})
	pq.Swap(0, 2)
	if !pq[0].priority.Equal(now6) || !pq[2].priority.Equal(now2) {
		t.Fatalf("swap failed")
	}
	if pq.Less(1, 2) {
		t.Fatalf("now4 < now2")
	}
}

func TestPopEmptyPQ(t *testing.T) {
	t.Parallel()
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("Expected panic from popping an empty PQ")
		}
	}()
	var pq priorityQueue
	pq.Pop()
}

type testjob struct {
	d        time.Duration
	t        time.Time
	deadline *time.Time
	uid      string
	instance int
}

func (j *testjob) GetDelay() time.Duration {
	return j.d
}

func (j testjob) GetUID() string {
	return j.uid
}

func (td *testjob) Deadline() (deadline time.Time, ok bool) {
	if td.deadline != nil {
		return *td.deadline, true
	} else {
		return time.Now(), false
	}
}

func TestDQ_sanity_check(t *testing.T) {
	t.Parallel()

	dq := NewDelayQueue()
	delay := 2 * time.Second
	dq.Add(&testjob{d: delay})

	before := time.Now()
	x := dq.Pop()

	now := time.Now()
	waitPeriod := now.Sub(before)

	if waitPeriod+tolerance < delay {
		t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay)
	}
	if x == nil {
		t.Fatalf("x is nil")
	}
	item := x.(*testjob)
	if item.d != delay {
		t.Fatalf("d != delay")
	}
}

func TestDQ_Offer(t *testing.T) {
	t.Parallel()
	assert := assert.New(t)

	dq := NewDelayQueue()
	delay := time.Second

	added := dq.Offer(&testjob{})
	if added {
		t.Fatalf("DelayQueue should not add offered job without deadline")
	}

	deadline := time.Now().Add(delay)
	added = dq.Offer(&testjob{deadline: &deadline})
	if !added {
		t.Fatalf("DelayQueue should add offered job with deadline")
	}

	before := time.Now()
	x := dq.Pop()

	now := time.Now()
	waitPeriod := now.Sub(before)

	if waitPeriod+tolerance < delay {
		t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay)
	}
	assert.NotNil(x)
	assert.Equal(x.(*testjob).deadline, &deadline)
}

func TestDQ_ordered_add_pop(t *testing.T) {
	t.Parallel()

	dq := NewDelayQueue()
	dq.Add(&testjob{d: 2 * time.Second})
	dq.Add(&testjob{d: 1 * time.Second})
	dq.Add(&testjob{d: 3 * time.Second})

	var finished [3]*testjob
	before := time.Now()
	idx := int32(-1)
	ch := make(chan bool, 3)
	//TODO: replace with `for range finished` once Go 1.3 support is dropped
	for n := 0; n < len(finished); n++ {
		go func() {
			var ok bool
			x := dq.Pop()
			i := atomic.AddInt32(&idx, 1)
			if finished[i], ok = x.(*testjob); !ok {
				t.Fatalf("expected a *testjob, not %v", x)
			}
			finished[i].t = time.Now()
			ch <- true
		}()
	}
	<-ch
	<-ch
	<-ch

	after := time.Now()
	totalDelay := after.Sub(before)
	if totalDelay+tolerance < (3 * time.Second) {
		t.Fatalf("totalDelay < 3s: %v", totalDelay)
	}
	for i, v := range finished {
		if v == nil {
			t.Fatalf("task %d was nil", i)
		}
		expected := time.Duration(i+1) * time.Second
		if v.d != expected {
			t.Fatalf("task %d had delay-priority %v, expected %v", i, v.d, expected)
		}
		actualDelay := v.t.Sub(before)
		if actualDelay+tolerance < v.d {
			t.Fatalf("task %d had actual-delay %v < expected delay %v", i, actualDelay, v.d)
		}
	}
}

func TestDQ_always_pop_earliest_deadline(t *testing.T) {
	t.Skip("disabled due to flakiness; see #11857")
	t.Parallel()

	// add a testjob with delay of 2s
	// spawn a func f1 that attempts to Pop() and wait for f1 to begin
	// add a testjob with a delay of 1s
	// check that the func f1 actually popped the 1s task (not the 2s task)

	dq := NewDelayQueue()
	dq.Add(&testjob{d: 2 * time.Second})
	ch := make(chan *testjob)
	started := make(chan bool)

	go func() {
		started <- true
		x := dq.Pop()
		job := x.(*testjob)
		job.t = time.Now()
		ch <- job
	}()

	<-started
	time.Sleep(500 * time.Millisecond) // give plently of time for Pop() to enter
	expected := 1 * time.Second
	dq.Add(&testjob{d: expected})
	job := <-ch

	if expected != job.d {
		t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d)
	}

	job = dq.Pop().(*testjob)
	expected = 2 * time.Second
	if expected != job.d {
		t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d)
	}
}

func TestDQ_always_pop_earliest_deadline_multi(t *testing.T) {
	t.Skip("disabled due to flakiness; see #11821")
	t.Parallel()

	dq := NewDelayQueue()
	dq.Add(&testjob{d: 2 * time.Second})

	ch := make(chan *testjob)
	multi := 10
	started := make(chan bool, multi)

	go func() {
		started <- true
		for i := 0; i < multi; i++ {
			x := dq.Pop()
			job := x.(*testjob)
			job.t = time.Now()
			ch <- job
		}
	}()

	<-started
	time.Sleep(500 * time.Millisecond) // give plently of time for Pop() to enter
	expected := 1 * time.Second

	for i := 0; i < multi; i++ {
		dq.Add(&testjob{d: expected})
	}
	for i := 0; i < multi; i++ {
		job := <-ch
		if expected != job.d {
			t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d)
		}
	}

	job := dq.Pop().(*testjob)
	expected = 2 * time.Second
	if expected != job.d {
		t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d)
	}
}

func TestDQ_negative_delay(t *testing.T) {
	t.Parallel()

	dq := NewDelayQueue()
	delay := -2 * time.Second
	dq.Add(&testjob{d: delay})

	before := time.Now()
	x := dq.Pop()

	now := time.Now()
	waitPeriod := now.Sub(before)

	if waitPeriod > tolerance {
		t.Fatalf("delay too long: %v, expected something less than: %v", waitPeriod, tolerance)
	}
	if x == nil {
		t.Fatalf("x is nil")
	}
	item := x.(*testjob)
	if item.d != delay {
		t.Fatalf("d != delay")
	}
}

func TestDFIFO_sanity_check(t *testing.T) {
	t.Parallel()
	assert := assert.New(t)

	df := NewDelayFIFO()
	delay := 2 * time.Second
	df.Add(&testjob{d: delay, uid: "a", instance: 1}, ReplaceExisting)
	assert.True(df.ContainedIDs().Has("a"))

	// re-add by ReplaceExisting
	df.Add(&testjob{d: delay, uid: "a", instance: 2}, ReplaceExisting)
	assert.True(df.ContainedIDs().Has("a"))

	a, ok := df.Get("a")
	assert.True(ok)
	assert.Equal(a.(*testjob).instance, 2)

	// re-add by KeepExisting
	df.Add(&testjob{d: delay, uid: "a", instance: 3}, KeepExisting)
	assert.True(df.ContainedIDs().Has("a"))

	a, ok = df.Get("a")
	assert.True(ok)
	assert.Equal(a.(*testjob).instance, 2)

	// pop last
	before := time.Now()
	x := df.Pop(WithoutCancel())
	assert.Equal(a.(*testjob).instance, 2)

	now := time.Now()
	waitPeriod := now.Sub(before)

	if waitPeriod+tolerance < delay {
		t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay)
	}
	if x == nil {
		t.Fatalf("x is nil")
	}
	item := x.(*testjob)
	if item.d != delay {
		t.Fatalf("d != delay")
	}
}

func TestDFIFO_Offer(t *testing.T) {
	t.Parallel()
	assert := assert.New(t)

	dq := NewDelayFIFO()
	delay := time.Second

	added := dq.Offer(&testjob{instance: 1}, ReplaceExisting)
	if added {
		t.Fatalf("DelayFIFO should not add offered job without deadline")
	}

	deadline := time.Now().Add(delay)
	added = dq.Offer(&testjob{deadline: &deadline, instance: 2}, ReplaceExisting)
	if !added {
		t.Fatalf("DelayFIFO should add offered job with deadline")
	}

	before := time.Now()
	x := dq.Pop(WithoutCancel())

	now := time.Now()
	waitPeriod := now.Sub(before)

	if waitPeriod+tolerance < delay {
		t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay)
	}
	assert.NotNil(x)
	assert.Equal(x.(*testjob).instance, 2)
}
