package bbr

import (
	"testing"
	"time"

	"github.com/apernet/quic-go/congestion"
	"github.com/apernet/quic-go/monotime"
)

func steadySender(t *testing.T) (*bbrSender, *fakeClock) {
	t.Helper()
	clock := &fakeClock{now: monotime.Now()}
	b := newBbrSender(clock, testPacket,
		initialCongestionWindowPackets*testPacket,
		congestion.MaxCongestionWindowPackets*testPacket, ProfileRealtime)
	b.SetRTTStatsProvider(stubRTT{min: testRTT})
	b.maxBandwidth.Update(testBandwidth, b.roundTripCount)
	b.minRtt = testRTT
	b.minRttTimestamp = clock.now
	b.isAtFullBandwidth = true
	b.enterProbeBandwidthMode(clock.now)
	b.congestionWindow = b.getTargetCongestionWindow(b.congestionWindowGain)
	return b, clock
}

func ack(pn congestion.PacketNumber, at monotime.Time) congestion.AckedPacketInfo {
	return congestion.AckedPacketInfo{PacketNumber: pn, BytesAcked: testPacket,
		ReceivedTime: at}
}

// The measured shape of a Wi-Fi hold: a burst in flight, then no ack at all
// for ~0.6 s, then everything arrives at once. The tracker must report the
// hold, once, and nothing from the ordinary round trips around it (the
// stub's smoothed RTT is testRTT, so a first ack one RTT after the send is
// zero stall).
func TestAckStallMeasuresAHold(t *testing.T) {
	b, clock := steadySender(t)
	var inFlight congestion.ByteCount
	for pn := congestion.PacketNumber(1); pn <= 5; pn++ {
		inFlight += testPacket
		b.OnPacketSent(clock.now, inFlight, pn, testPacket, true)
	}
	clock.now = clock.now.Add(testRTT)
	b.OnCongestionEventEx(inFlight, clock.now,
		[]congestion.AckedPacketInfo{ack(1, clock.now), ack(2, clock.now),
			ack(3, clock.now)}, nil)
	inFlight -= 3 * testPacket
	if got := b.TakeMaxAckStall(); got != 0 {
		t.Fatalf("ordinary round trip: stall=%v, want 0", got)
	}

	hold := 600 * time.Millisecond
	clock.now = clock.now.Add(hold + testRTT)
	b.OnCongestionEventEx(inFlight, clock.now,
		[]congestion.AckedPacketInfo{ack(4, clock.now), ack(5, clock.now)}, nil)
	if got := b.TakeMaxAckStall(); got != hold {
		t.Fatalf("hold: stall=%v, want %v", got, hold)
	}
	if got := b.TakeMaxAckStall(); got != 0 {
		t.Fatalf("a taken stall must reset: got %v", got)
	}
}

// An app-limited sender that simply has nothing to send is not held: the
// silence between its bursts carries no data in flight and must not count,
// and neither does the one round trip its next burst waits for its ack.
func TestAckStallIgnoresIdleTimeAndTheRoundTrip(t *testing.T) {
	b, clock := steadySender(t)
	b.OnPacketSent(clock.now, testPacket, 1, testPacket, true)
	clock.now = clock.now.Add(testRTT)
	b.OnCongestionEventEx(testPacket, clock.now,
		[]congestion.AckedPacketInfo{ack(1, clock.now)}, nil)
	b.TakeMaxAckStall()

	// Two seconds of nothing to send, then one packet acked one RTT later.
	clock.now = clock.now.Add(2 * time.Second)
	b.OnPacketSent(clock.now, testPacket, 2, testPacket, true)
	clock.now = clock.now.Add(testRTT)
	b.OnCongestionEventEx(testPacket, clock.now,
		[]congestion.AckedPacketInfo{ack(2, clock.now)}, nil)
	if got := b.TakeMaxAckStall(); got != 0 {
		t.Fatalf("idle time or the round trip counted as a hold: stall=%v", got)
	}
}

// A standing queue inflates the round trip; the stall is measured beyond
// the smoothed RTT, so a late-but-arriving ack is not a hold.
func TestAckStallIsMeasuredBeyondTheSmoothedRTT(t *testing.T) {
	b, clock := steadySender(t)
	bloated := 450 * time.Millisecond
	b.SetRTTStatsProvider(stubRTT{min: bloated})
	b.OnPacketSent(clock.now, testPacket, 1, testPacket, true)
	clock.now = clock.now.Add(bloated + 100*time.Millisecond)
	b.OnCongestionEventEx(testPacket, clock.now,
		[]congestion.AckedPacketInfo{ack(1, clock.now)}, nil)
	if got := b.TakeMaxAckStall(); got != 100*time.Millisecond {
		t.Fatalf("stall=%v, want the 100ms beyond the 450ms round trip", got)
	}
}

// A loss-only event (a PTO declared packets lost, nothing acked) is not an
// ack: the wait for the link continues through it.
func TestAckStallSpansLossOnlyEvents(t *testing.T) {
	b, clock := steadySender(t)
	var inFlight congestion.ByteCount
	for pn := congestion.PacketNumber(1); pn <= 3; pn++ {
		inFlight += testPacket
		b.OnPacketSent(clock.now, inFlight, pn, testPacket, true)
	}
	clock.now = clock.now.Add(300 * time.Millisecond)
	b.OnCongestionEventEx(inFlight, clock.now, nil,
		[]congestion.LostPacketInfo{{PacketNumber: 1, BytesLost: testPacket}})
	inFlight -= testPacket
	clock.now = clock.now.Add(300 * time.Millisecond)
	b.OnCongestionEventEx(inFlight, clock.now,
		[]congestion.AckedPacketInfo{ack(2, clock.now), ack(3, clock.now)}, nil)
	if got := b.TakeMaxAckStall(); got != 600*time.Millisecond-testRTT {
		t.Fatalf("stall=%v, want %v across the loss-only event", got,
			600*time.Millisecond-testRTT)
	}
}
