package main

import "testing"

// sent is the sender's frontier: sequence numbers at or above it were never put
// on the wire.
func newWindow(sent uint64) *lossWindow {
	tx := &txStats{}
	tx.sent.Store(sent)
	w := newLossWindows([]*txStats{tx})
	return &w[0]
}

func TestLossWindowContiguousLosesNothing(t *testing.T) {
	w := newWindow(lossSlots + 1000)
	for seq := uint64(0); seq < lossSlots+1000; seq++ {
		w.observe(seq)
	}
	if got := w.lost.Load(); got != 0 {
		t.Errorf("lost = %d, want 0", got)
	}
	if got := w.late.Load(); got != 0 {
		t.Errorf("late = %d, want 0", got)
	}
}

func TestLossWindowCountsGapOnceEvicted(t *testing.T) {
	w := newWindow(lossSlots + 1000)
	for seq := uint64(0); seq < lossSlots+1000; seq++ {
		if seq == 100 {
			continue
		}
		w.observe(seq)
	}
	if got := w.lost.Load(); got != 1 {
		t.Errorf("lost = %d, want 1", got)
	}
}

// Arriving out of order inside the window is not loss: a sequence is only
// judged once it falls out the far end.
func TestLossWindowOutOfOrderIsNotLoss(t *testing.T) {
	w := newWindow(lossSlots + 1000)
	for seq := uint64(99); ; seq-- {
		w.observe(seq)
		if seq == 0 {
			break
		}
	}
	for seq := uint64(100); seq < lossSlots+1000; seq++ {
		w.observe(seq)
	}
	if got := w.lost.Load(); got != 0 {
		t.Errorf("lost = %d, want 0", got)
	}
}

// The other branch of evict: a jump past a whole window writes off everything
// the window held plus the sequences that never landed in it at all.
func TestLossWindowJumpBeyondWindow(t *testing.T) {
	w := newWindow(200001)
	w.observe(0)
	w.observe(200000)

	// Everything below the new base except the one sequence that was seen.
	want := uint64(200000 - lossSlots + 1 - 1)
	if got := w.lost.Load(); got != want {
		t.Errorf("lost = %d, want %d", got, want)
	}
	if got := w.late.Load(); got != 0 {
		t.Errorf("late = %d, want 0", got)
	}
}

func TestLossWindowBelowBaseIsLate(t *testing.T) {
	w := newWindow(100001)
	w.observe(100000)
	w.observe(1000)
	if got := w.late.Load(); got != 1 {
		t.Errorf("late = %d, want 1", got)
	}
	if got := w.lost.Load(); got != 0 {
		t.Errorf("lost = %d, want 0", got)
	}
}

// The first sequence seen starts the window half a span below it, so frames
// another worker is still holding land inside rather than arriving late.
func TestLossWindowStartsHalfAWindowBack(t *testing.T) {
	w := newWindow(100001)
	w.observe(100000)
	if w.base != 100000-lossSlots/2 {
		t.Errorf("base = %d, want %d", w.base, 100000-lossSlots/2)
	}
}

// The failure this guard exists for. A sequence number the sender never reached
// used to drag the base up to it, write off the span in between as lost, and
// leave every real frame after it below the base and counted late for the rest
// of the run. It has to be refused outright, and the stream has to go on
// counting as if it had never arrived.
func TestLossWindowRefusesUnsentSeq(t *testing.T) {
	const first = 100000
	w := newWindow(first + 2000)
	for seq := uint64(first); seq < first+1000; seq++ {
		w.observe(seq)
	}
	base := w.base

	if w.observe(1 << 62) {
		t.Error("a sequence number far past the sender's frontier was accepted")
	}
	if got := w.lost.Load(); got != 0 {
		t.Errorf("lost = %d after one impossible sequence number, want 0", got)
	}
	if w.base != base {
		t.Errorf("base moved to %d, want it left at %d where the real traffic put it",
			w.base, base)
	}

	// Still tracking the real traffic, rather than reporting every frame late
	// against a base that ran away.
	for seq := uint64(first + 1000); seq < first+2000; seq++ {
		w.observe(seq)
	}
	if got := w.late.Load(); got != 0 {
		t.Errorf("late = %d, want 0", got)
	}
	if got := w.lost.Load(); got != 0 {
		t.Errorf("lost = %d, want 0", got)
	}
}

// The sender's own frontier is the bound, so the sequence one past it is
// refused while the one below it is not.
func TestLossWindowBoundIsExclusive(t *testing.T) {
	w := newWindow(500)
	if !w.observe(499) {
		t.Error("the last sequence the sender put on the wire was refused")
	}
	if w.observe(500) {
		t.Error("a sequence the sender had not reached was accepted")
	}
}
