package main

import (
	"testing"
	"time"
)

// A frame lands in the bucket its receive stamp falls in, whenever the worker
// got round to draining it.
func TestRxObserveBucketsByStamp(t *testing.T) {
	var s rxStats
	run := rateRun{stats: &s}
	run.add(3*int64(time.Millisecond), 100)
	run.add(5*int64(time.Millisecond), 200)
	run.add(rateBucketNs+int64(time.Millisecond), 300)
	run.flush()

	if f, b := s.bucket(0); f != 2 || b != 300 {
		t.Errorf("epoch 0 = %d frames, %d bytes; want 2, 300", f, b)
	}
	if f, b := s.bucket(1); f != 1 || b != 300 {
		t.Errorf("epoch 1 = %d frames, %d bytes; want 1, 300", f, b)
	}
	if got := s.newest.Load(); got != 1 {
		t.Errorf("newest = %d, want 1", got)
	}
}

// A slot coming round again belongs to its new epoch, and the epoch it replaced
// reports nothing rather than the stale counts.
func TestRxBucketWraps(t *testing.T) {
	var s rxStats
	run := rateRun{stats: &s}
	run.add(1, 100)
	run.add(rateBuckets*rateBucketNs+1, 200)
	run.flush()

	if f, b := s.bucket(0); f != 0 || b != 0 {
		t.Errorf("evicted epoch = %d frames, %d bytes; want 0, 0", f, b)
	}
	if f, b := s.bucket(rateBuckets); f != 1 || b != 200 {
		t.Errorf("new epoch = %d frames, %d bytes; want 1, 200", f, b)
	}
}

// Every stream's frontier is past the epoch being read, so the bucket is
// complete across the board and the rate comes from the one before the lowest
// frontier.
func TestDirectionRateReadsOneBucketBack(t *testing.T) {
	d := &direction{rxStats: []*rxStats{{}, {}}}
	r0 := rateRun{stats: d.rxStats[0]}
	r1 := rateRun{stats: d.rxStats[1]}
	r0.add(rateBucketNs+1, 500)
	r1.add(rateBucketNs+2, 700)
	r0.add(2*rateBucketNs+1, 900)
	r1.add(2*rateBucketNs+2, 1100)
	r0.flush()
	r1.flush()

	d.readRateBucket(time.Now())
	if d.rateFrames != 2 || d.rateBytes != 1200 {
		t.Errorf("rate bucket = %d frames, %d bytes; want the completed epoch, 2 and 1200",
			d.rateFrames, d.rateBytes)
	}
}

// Reading a bucket is a claim that every stream has delivered through it, so a
// stream still short of the epoch holds the read back rather than having its
// half-filled bucket summed.
func TestDirectionRateWaitsForSlowestStream(t *testing.T) {
	d := &direction{rxStats: []*rxStats{{}, {}}}
	r0 := rateRun{stats: d.rxStats[0]}
	r1 := rateRun{stats: d.rxStats[1]}
	r0.add(rateBucketNs+1, 500)
	r0.add(2*rateBucketNs+1, 900)
	r0.flush()

	// One stream has never delivered at all, so there is no epoch every stream
	// has reached and nothing to read.
	d.readRateBucket(time.Now())
	if d.rateFrames != 0 || d.rateBytes != 0 {
		t.Errorf("rate = %d frames, %d bytes; want nothing while a stream has no frontier",
			d.rateFrames, d.rateBytes)
	}

	// The straggler is still filling the epoch the leader finished, so its
	// bucket must not be read yet.
	r1.add(rateBucketNs+2, 700)
	r1.flush()
	d.readRateBucket(time.Now())
	if d.rateFrames != 0 || d.rateBytes != 0 {
		t.Errorf("rate = %d frames, %d bytes; want nothing while a stream is still filling the epoch",
			d.rateFrames, d.rateBytes)
	}

	// Once it moves past, the bucket is complete for both streams and is read
	// whole.
	r1.add(2*rateBucketNs+2, 1100)
	r1.flush()
	d.readRateBucket(time.Now())
	if d.rateFrames != 2 || d.rateBytes != 1200 {
		t.Errorf("rate = %d frames, %d bytes; want the whole completed epoch, 2 and 1200",
			d.rateFrames, d.rateBytes)
	}
}

// A stamp only advances when a frame arrives, so an epoch that stops moving is
// a quiet wire and must not keep reporting the last bucket.
func TestDirectionRateGoesStale(t *testing.T) {
	d := &direction{rxStats: []*rxStats{{}}}
	run := rateRun{stats: d.rxStats[0]}
	run.add(rateBucketNs+1, 500)
	run.add(2*rateBucketNs+1, 900)
	run.flush()

	now := time.Now()
	d.readRateBucket(now)
	if d.rateFrames == 0 {
		t.Fatal("expected a rate while the epoch was still moving")
	}
	d.readRateBucket(now.Add(2 * rateStale))
	if d.rateFrames != 0 || d.rateBytes != 0 {
		t.Errorf("stale rate = %d frames, %d bytes; want 0, 0", d.rateFrames, d.rateBytes)
	}
}

// Indexed oldest first, so the error window still spans the whole ring once it
// has wrapped.
func TestRateWindowIndexesOldestFirst(t *testing.T) {
	w := newRateWindow(3)
	for i := 1; i <= 5; i++ {
		w.push(counterSet{s: sample{rxFrames: uint64(i)}})
	}
	if got := w.count(); got != 3 {
		t.Fatalf("count = %d, want 3", got)
	}
	if got := w.at(0).s.rxFrames; got != 3 {
		t.Errorf("oldest = %d, want 3", got)
	}
	if got := w.at(2).s.rxFrames; got != 5 {
		t.Errorf("newest = %d, want 5", got)
	}
}

// Which counter feeds which bucket is the whole taxonomy the panel reports, so
// it is pinned here rather than left to whoever reads errsBetween next.
func TestErrsBetweenBuckets(t *testing.T) {
	n := counterSet{
		s: sample{
			lost: 1, late: 7,
			crcErr: 2, badMagic: 3, badLen: 4, badHdr: 10,
			txErrs: 6, rxErrs: 5,
		},
		drops: 9,
		nic:   8,
	}
	got := errsBetween(counterSet{}, n)
	want := errs{lost: 1, corrupt: 2 + 3 + 4 + 10, link: 8 + 5, internal: 9 + 7 + 6}
	if got != want {
		t.Errorf("errsBetween = %+v, want %+v", got, want)
	}
	if got.total() != 55 {
		t.Errorf("total = %d, want 55", got.total())
	}
}

// A reset re-bases from a fresh capture while the ring still holds buckets from
// just before it, so the newest bucket must not be left behind the new origin.
func TestResetDoesNotUnderflowTotals(t *testing.T) {
	d := &direction{
		win:     newRateWindow(8),
		cable:   newCableStats(),
		rxStats: []*rxStats{{}},
	}

	d.rxStats[0].frames.Store(100)
	d.rxStats[0].bytes.Store(6400)
	d.sample()

	d.rxStats[0].frames.Store(150)
	d.rxStats[0].bytes.Store(9600)
	d.reset()

	v := d.displayView()
	if v.rxFrames != 0 {
		t.Errorf("rxFrames = %d, want 0", v.rxFrames)
	}
	if v.rxBytes != 0 {
		t.Errorf("rxBytes = %d, want 0", v.rxBytes)
	}

	// The rolling window and the rates are about now rather than since the
	// reset, so the buckets from before it have to survive.
	if got := d.win.count(); got < 2 {
		t.Errorf("ring holds %d buckets after reset, want the pre-reset history kept", got)
	}
}
