// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build go1.21

package quic

import (
	"crypto/rand"
	"reflect"
	"testing"
)

func TestCryptoStreamReceive(t *testing.T) {
	data := make([]byte, 1<<20)
	rand.Read(data) // doesn't need to be crypto/rand, but non-deprecated and harmless
	type frame struct {
		start int64
		end   int64
		want  int
	}
	for _, test := range []struct {
		name   string
		frames []frame
	}{{
		name: "linear",
		frames: []frame{{
			start: 0,
			end:   1000,
			want:  1000,
		}, {
			start: 1000,
			end:   2000,
			want:  2000,
		}, {
			// larger than any realistic packet can hold
			start: 2000,
			end:   1 << 20,
			want:  1 << 20,
		}},
	}, {
		name: "out of order",
		frames: []frame{{
			start: 1000,
			end:   2000,
		}, {
			start: 2000,
			end:   3000,
		}, {
			start: 0,
			end:   1000,
			want:  3000,
		}},
	}, {
		name: "resent",
		frames: []frame{{
			start: 0,
			end:   1000,
			want:  1000,
		}, {
			start: 0,
			end:   1000,
			want:  1000,
		}, {
			start: 1000,
			end:   2000,
			want:  2000,
		}, {
			start: 0,
			end:   1000,
			want:  2000,
		}, {
			start: 1000,
			end:   2000,
			want:  2000,
		}},
	}, {
		name: "overlapping",
		frames: []frame{{
			start: 0,
			end:   1000,
			want:  1000,
		}, {
			start: 3000,
			end:   4000,
			want:  1000,
		}, {
			start: 2000,
			end:   3000,
			want:  1000,
		}, {
			start: 1000,
			end:   3000,
			want:  4000,
		}},
	}} {
		t.Run(test.name, func(t *testing.T) {
			var s cryptoStream
			var got []byte
			for _, f := range test.frames {
				t.Logf("receive [%v,%v)", f.start, f.end)
				s.handleCrypto(
					f.start,
					data[f.start:f.end],
					func(b []byte) error {
						t.Logf("got new bytes [%v,%v)", len(got), len(got)+len(b))
						got = append(got, b...)
						return nil
					},
				)
				if len(got) != f.want {
					t.Fatalf("have bytes [0,%v), want [0,%v)", len(got), f.want)
				}
				for i := range got {
					if got[i] != data[i] {
						t.Fatalf("byte %v of received data = %v, want %v", i, got[i], data[i])
					}
				}
			}
		})
	}
}

func TestCryptoStreamSends(t *testing.T) {
	data := make([]byte, 1<<20)
	rand.Read(data) // doesn't need to be crypto/rand, but non-deprecated and harmless
	type (
		sendOp i64range[int64]
		ackOp  i64range[int64]
		lossOp i64range[int64]
	)
	for _, test := range []struct {
		name        string
		size        int64
		ops         []any
		wantSend    []i64range[int64]
		wantPTOSend []i64range[int64]
	}{{
		name: "writes with data remaining",
		size: 4000,
		ops: []any{
			sendOp{0, 1000},
			sendOp{1000, 2000},
			sendOp{2000, 3000},
		},
		wantSend: []i64range[int64]{
			{3000, 4000},
		},
		wantPTOSend: []i64range[int64]{
			{0, 4000},
		},
	}, {
		name: "lost data is resent",
		size: 4000,
		ops: []any{
			sendOp{0, 1000},
			sendOp{1000, 2000},
			sendOp{2000, 3000},
			sendOp{3000, 4000},
			lossOp{1000, 2000},
			lossOp{3000, 4000},
		},
		wantSend: []i64range[int64]{
			{1000, 2000},
			{3000, 4000},
		},
		wantPTOSend: []i64range[int64]{
			{0, 4000},
		},
	}, {
		name: "acked data at start of range",
		size: 4000,
		ops: []any{
			sendOp{0, 4000},
			ackOp{0, 1000},
			ackOp{1000, 2000},
			ackOp{2000, 3000},
		},
		wantSend: nil,
		wantPTOSend: []i64range[int64]{
			{3000, 4000},
		},
	}, {
		name: "acked data is not resent on pto",
		size: 4000,
		ops: []any{
			sendOp{0, 4000},
			ackOp{1000, 2000},
		},
		wantSend: nil,
		wantPTOSend: []i64range[int64]{
			{0, 1000},
		},
	}, {
		// This is an unusual, but possible scenario:
		// Data is sent, resent, one of the two sends is acked, and the other is lost.
		name: "acked and then lost data is not resent",
		size: 4000,
		ops: []any{
			sendOp{0, 4000},
			sendOp{1000, 2000}, // resent, no-op
			ackOp{1000, 2000},
			lossOp{1000, 2000},
		},
		wantSend: nil,
		wantPTOSend: []i64range[int64]{
			{0, 1000},
		},
	}, {
		// The opposite of the above scenario: data is marked lost, and then acked
		// before being resent.
		name: "lost and then acked data is not resent",
		size: 4000,
		ops: []any{
			sendOp{0, 4000},
			sendOp{1000, 2000}, // resent, no-op
			lossOp{1000, 2000},
			ackOp{1000, 2000},
		},
		wantSend: nil,
		wantPTOSend: []i64range[int64]{
			{0, 1000},
		},
	}} {
		t.Run(test.name, func(t *testing.T) {
			var s cryptoStream
			s.write(data[:test.size])
			for _, op := range test.ops {
				switch op := op.(type) {
				case sendOp:
					t.Logf("send [%v,%v)", op.start, op.end)
					b := make([]byte, op.end-op.start)
					s.sendData(op.start, b)
				case ackOp:
					t.Logf("ack  [%v,%v)", op.start, op.end)
					s.ackOrLoss(op.start, op.end, packetAcked)
				case lossOp:
					t.Logf("loss [%v,%v)", op.start, op.end)
					s.ackOrLoss(op.start, op.end, packetLost)
				default:
					t.Fatalf("unhandled type %T", op)
				}
			}
			var gotSend []i64range[int64]
			s.dataToSend(true, func(off, size int64) (wrote int64) {
				gotSend = append(gotSend, i64range[int64]{off, off + size})
				return 0
			})
			if !reflect.DeepEqual(gotSend, test.wantPTOSend) {
				t.Fatalf("got data to send on PTO: %v, want %v", gotSend, test.wantPTOSend)
			}
			gotSend = nil
			s.dataToSend(false, func(off, size int64) (wrote int64) {
				gotSend = append(gotSend, i64range[int64]{off, off + size})
				b := make([]byte, size)
				s.sendData(off, b)
				return int64(len(b))
			})
			if !reflect.DeepEqual(gotSend, test.wantSend) {
				t.Fatalf("got data to send: %v, want %v", gotSend, test.wantSend)
			}
		})
	}
}
