package wire

import (
	"bytes"
	"fmt"
	"math"
	"net/netip"
	"testing"
	"time"

	"golang.org/x/exp/rand"

	"github.com/quic-go/quic-go/internal/protocol"
	"github.com/quic-go/quic-go/internal/qerr"
	"github.com/quic-go/quic-go/quicvarint"

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

func getRandomValueUpTo(max int64) uint64 {
	maxVals := []int64{math.MaxUint8 / 4, math.MaxUint16 / 4, math.MaxUint32 / 4, math.MaxUint64 / 4}
	m := maxVals[int(rand.Int31n(4))]
	if m > max {
		m = max
	}
	return uint64(rand.Int63n(m))
}

func getRandomValue() uint64 { return getRandomValueUpTo(quicvarint.Max) }

func appendInitialSourceConnectionID(b []byte) []byte {
	b = quicvarint.Append(b, uint64(initialSourceConnectionIDParameterID))
	b = quicvarint.Append(b, 6)
	return append(b, []byte("foobar")...)
}

func TestTransportParametersStringRepresentation(t *testing.T) {
	rcid := protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde})
	p := &TransportParameters{
		InitialMaxStreamDataBidiLocal:   1234,
		InitialMaxStreamDataBidiRemote:  2345,
		InitialMaxStreamDataUni:         3456,
		InitialMaxData:                  4567,
		MaxBidiStreamNum:                1337,
		MaxUniStreamNum:                 7331,
		MaxIdleTimeout:                  42 * time.Second,
		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
		InitialSourceConnectionID:       protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad}),
		RetrySourceConnectionID:         &rcid,
		AckDelayExponent:                14,
		MaxAckDelay:                     37 * time.Millisecond,
		StatelessResetToken:             &protocol.StatelessResetToken{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00},
		ActiveConnectionIDLimit:         123,
		MaxDatagramFrameSize:            876,
	}
	expected := "&wire.TransportParameters{OriginalDestinationConnectionID: deadbeef, InitialSourceConnectionID: decafbad, RetrySourceConnectionID: deadc0de, InitialMaxStreamDataBidiLocal: 1234, InitialMaxStreamDataBidiRemote: 2345, InitialMaxStreamDataUni: 3456, InitialMaxData: 4567, MaxBidiStreamNum: 1337, MaxUniStreamNum: 7331, MaxIdleTimeout: 42s, AckDelayExponent: 14, MaxAckDelay: 37ms, ActiveConnectionIDLimit: 123, StatelessResetToken: 0x112233445566778899aabbccddeeff00, MaxDatagramFrameSize: 876}"
	require.Equal(t, expected, p.String())
}

func TestTransportParametersStringRepresentationWithoutOptionalFields(t *testing.T) {
	p := &TransportParameters{
		InitialMaxStreamDataBidiLocal:   1234,
		InitialMaxStreamDataBidiRemote:  2345,
		InitialMaxStreamDataUni:         3456,
		InitialMaxData:                  4567,
		MaxBidiStreamNum:                1337,
		MaxUniStreamNum:                 7331,
		MaxIdleTimeout:                  42 * time.Second,
		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
		InitialSourceConnectionID:       protocol.ParseConnectionID([]byte{}),
		AckDelayExponent:                14,
		MaxAckDelay:                     37 * time.Second,
		ActiveConnectionIDLimit:         89,
		MaxDatagramFrameSize:            protocol.InvalidByteCount,
	}
	expected := "&wire.TransportParameters{OriginalDestinationConnectionID: deadbeef, InitialSourceConnectionID: (empty), InitialMaxStreamDataBidiLocal: 1234, InitialMaxStreamDataBidiRemote: 2345, InitialMaxStreamDataUni: 3456, InitialMaxData: 4567, MaxBidiStreamNum: 1337, MaxUniStreamNum: 7331, MaxIdleTimeout: 42s, AckDelayExponent: 14, MaxAckDelay: 37s, ActiveConnectionIDLimit: 89}"
	require.Equal(t, expected, p.String())
}

func TestMarshalAndUnmarshalTransportParameters(t *testing.T) {
	var token protocol.StatelessResetToken
	rand.Read(token[:])
	rcid := protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde})
	params := &TransportParameters{
		InitialMaxStreamDataBidiLocal:   protocol.ByteCount(getRandomValue()),
		InitialMaxStreamDataBidiRemote:  protocol.ByteCount(getRandomValue()),
		InitialMaxStreamDataUni:         protocol.ByteCount(getRandomValue()),
		InitialMaxData:                  protocol.ByteCount(getRandomValue()),
		MaxIdleTimeout:                  0xcafe * time.Second,
		MaxBidiStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
		MaxUniStreamNum:                 protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
		DisableActiveMigration:          true,
		StatelessResetToken:             &token,
		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
		InitialSourceConnectionID:       protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad}),
		RetrySourceConnectionID:         &rcid,
		AckDelayExponent:                13,
		MaxAckDelay:                     42 * time.Millisecond,
		ActiveConnectionIDLimit:         2 + getRandomValueUpTo(quicvarint.Max-2),
		MaxUDPPayloadSize:               1200 + protocol.ByteCount(getRandomValueUpTo(quicvarint.Max-1200)),
		MaxDatagramFrameSize:            protocol.ByteCount(getRandomValue()),
	}
	data := params.Marshal(protocol.PerspectiveServer)

	p := &TransportParameters{}
	require.NoError(t, p.Unmarshal(data, protocol.PerspectiveServer))
	require.Equal(t, params.InitialMaxStreamDataBidiLocal, p.InitialMaxStreamDataBidiLocal)
	require.Equal(t, params.InitialMaxStreamDataBidiRemote, p.InitialMaxStreamDataBidiRemote)
	require.Equal(t, params.InitialMaxStreamDataUni, p.InitialMaxStreamDataUni)
	require.Equal(t, params.InitialMaxData, p.InitialMaxData)
	require.Equal(t, params.MaxUniStreamNum, p.MaxUniStreamNum)
	require.Equal(t, params.MaxBidiStreamNum, p.MaxBidiStreamNum)
	require.Equal(t, params.MaxIdleTimeout, p.MaxIdleTimeout)
	require.Equal(t, params.DisableActiveMigration, p.DisableActiveMigration)
	require.Equal(t, params.StatelessResetToken, p.StatelessResetToken)
	require.Equal(t, protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}), p.OriginalDestinationConnectionID)
	require.Equal(t, protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad}), p.InitialSourceConnectionID)
	require.Equal(t, &rcid, p.RetrySourceConnectionID)
	require.Equal(t, uint8(13), p.AckDelayExponent)
	require.Equal(t, 42*time.Millisecond, p.MaxAckDelay)
	require.Equal(t, params.ActiveConnectionIDLimit, p.ActiveConnectionIDLimit)
	require.Equal(t, params.MaxUDPPayloadSize, p.MaxUDPPayloadSize)
	require.Equal(t, params.MaxDatagramFrameSize, p.MaxDatagramFrameSize)
}

func TestMarshalAdditionalTransportParameters(t *testing.T) {
	origAdditionalTransportParametersClient := AdditionalTransportParametersClient
	t.Cleanup(func() { AdditionalTransportParametersClient = origAdditionalTransportParametersClient })
	AdditionalTransportParametersClient = map[uint64][]byte{1337: []byte("foobar")}

	result := quicvarint.Append([]byte{}, 1337)
	result = quicvarint.Append(result, 6)
	result = append(result, []byte("foobar")...)

	params := &TransportParameters{}
	require.True(t, bytes.Contains(params.Marshal(protocol.PerspectiveClient), result))
	require.False(t, bytes.Contains(params.Marshal(protocol.PerspectiveServer), result))
}

func TestMarshalWithoutRetrySourceConnectionID(t *testing.T) {
	data := (&TransportParameters{
		StatelessResetToken:     &protocol.StatelessResetToken{},
		ActiveConnectionIDLimit: 2,
	}).Marshal(protocol.PerspectiveServer)
	p := &TransportParameters{}
	require.NoError(t, p.Unmarshal(data, protocol.PerspectiveServer))
	require.Nil(t, p.RetrySourceConnectionID)
}

func TestMarshalZeroLengthRetrySourceConnectionID(t *testing.T) {
	rcid := protocol.ParseConnectionID([]byte{})
	data := (&TransportParameters{
		RetrySourceConnectionID: &rcid,
		StatelessResetToken:     &protocol.StatelessResetToken{},
		ActiveConnectionIDLimit: 2,
	}).Marshal(protocol.PerspectiveServer)
	p := &TransportParameters{}
	require.NoError(t, p.Unmarshal(data, protocol.PerspectiveServer))
	require.NotNil(t, p.RetrySourceConnectionID)
	require.Zero(t, p.RetrySourceConnectionID.Len())
}

func TestTransportParameterNoMaxAckDelayIfDefault(t *testing.T) {
	const num = 1000
	var defaultLen, dataLen int
	maxAckDelay := protocol.DefaultMaxAckDelay + time.Millisecond
	for i := 0; i < num; i++ {
		dataDefault := (&TransportParameters{
			MaxAckDelay:         protocol.DefaultMaxAckDelay,
			StatelessResetToken: &protocol.StatelessResetToken{},
		}).Marshal(protocol.PerspectiveServer)
		defaultLen += len(dataDefault)
		data := (&TransportParameters{
			MaxAckDelay:         maxAckDelay,
			StatelessResetToken: &protocol.StatelessResetToken{},
		}).Marshal(protocol.PerspectiveServer)
		dataLen += len(data)
	}
	entryLen := quicvarint.Len(uint64(ackDelayExponentParameterID)) +
		quicvarint.Len(uint64(quicvarint.Len(uint64(maxAckDelay.Milliseconds())))) +
		quicvarint.Len(uint64(maxAckDelay.Milliseconds()))
	require.InDelta(t, float32(defaultLen)/num+float32(entryLen), float32(dataLen)/num, 1)
}

func TestTransportParameterNoAckDelayExponentIfDefault(t *testing.T) {
	const num = 1000
	var defaultLen, dataLen int
	for i := 0; i < num; i++ {
		dataDefault := (&TransportParameters{
			AckDelayExponent:    protocol.DefaultAckDelayExponent,
			StatelessResetToken: &protocol.StatelessResetToken{},
		}).Marshal(protocol.PerspectiveServer)
		defaultLen += len(dataDefault)
		data := (&TransportParameters{
			AckDelayExponent:    protocol.DefaultAckDelayExponent + 1,
			StatelessResetToken: &protocol.StatelessResetToken{},
		}).Marshal(protocol.PerspectiveServer)
		dataLen += len(data)
	}
	entryLen := quicvarint.Len(uint64(ackDelayExponentParameterID)) +
		quicvarint.Len(uint64(quicvarint.Len(protocol.DefaultAckDelayExponent+1))) +
		quicvarint.Len(protocol.DefaultAckDelayExponent+1)
	require.InDelta(t, float32(defaultLen)/num+float32(entryLen), float32(dataLen)/num, 1)
}

func TestTransportParameterSetsDefaultValuesWhenNotSent(t *testing.T) {
	data := (&TransportParameters{
		AckDelayExponent:        protocol.DefaultAckDelayExponent,
		StatelessResetToken:     &protocol.StatelessResetToken{},
		ActiveConnectionIDLimit: protocol.DefaultActiveConnectionIDLimit,
	}).Marshal(protocol.PerspectiveServer)
	p := &TransportParameters{}
	require.NoError(t, p.Unmarshal(data, protocol.PerspectiveServer))
	require.EqualValues(t, protocol.DefaultAckDelayExponent, p.AckDelayExponent)
	require.EqualValues(t, protocol.DefaultActiveConnectionIDLimit, p.ActiveConnectionIDLimit)
}

func TestTransportParameterErrors(t *testing.T) {
	tests := []struct {
		name           string
		params         *TransportParameters
		perspective    protocol.Perspective
		data           []byte
		expectedErrMsg string
	}{
		{
			name: "invalid stateless reset token length",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(statelessResetTokenParameterID))
				b = quicvarint.Append(b, 15)
				return append(b, make([]byte, 15)...)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "wrong length for stateless_reset_token: 15 (expected 16)",
		},
		{
			name: "small max UDP payload size",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(maxUDPPayloadSizeParameterID))
				b = quicvarint.Append(b, uint64(quicvarint.Len(1199)))
				return quicvarint.Append(b, 1199)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "invalid value for max_udp_payload_size: 1199 (minimum 1200)",
		},
		{
			name: "active connection ID limit too small",
			params: &TransportParameters{
				ActiveConnectionIDLimit: 1,
				StatelessResetToken:     &protocol.StatelessResetToken{},
			},
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "invalid value for active_connection_id_limit: 1 (minimum 2)",
		},
		{
			name: "ack delay exponent too large",
			params: &TransportParameters{
				AckDelayExponent:    21,
				StatelessResetToken: &protocol.StatelessResetToken{},
			},
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "invalid value for ack_delay_exponent: 21 (maximum 20)",
		},
		{
			name: "disable active migration has content",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(disableActiveMigrationParameterID))
				b = quicvarint.Append(b, 6)
				return append(b, []byte("foobar")...)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "wrong length for disable_active_migration: 6 (expected empty)",
		},
		{
			name: "server doesn't set original destination connection ID",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(statelessResetTokenParameterID))
				b = quicvarint.Append(b, 16)
				b = append(b, make([]byte, 16)...)
				return appendInitialSourceConnectionID(b)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "missing original_destination_connection_id",
		},
		{
			name:           "initial source connection ID is missing",
			data:           []byte{},
			perspective:    protocol.PerspectiveClient,
			expectedErrMsg: "missing initial_source_connection_id",
		},
		{
			name: "max ack delay is too large",
			params: &TransportParameters{
				MaxAckDelay:         1 << 14 * time.Millisecond,
				StatelessResetToken: &protocol.StatelessResetToken{},
			},
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "invalid value for max_ack_delay: 16384ms (maximum 16383ms)",
		},
		{
			name: "varint value has wrong length",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(initialMaxStreamDataBidiLocalParameterID))
				b = quicvarint.Append(b, 2)
				val := uint64(0xdeadbeef)
				b = quicvarint.Append(b, val)
				return appendInitialSourceConnectionID(b)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: fmt.Sprintf("inconsistent transport parameter length for transport parameter %#x", initialMaxStreamDataBidiLocalParameterID),
		},
		{
			name: "initial max streams bidi is too large",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(initialMaxStreamsBidiParameterID))
				b = quicvarint.Append(b, uint64(quicvarint.Len(uint64(protocol.MaxStreamCount+1))))
				b = quicvarint.Append(b, uint64(protocol.MaxStreamCount+1))
				return appendInitialSourceConnectionID(b)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "initial_max_streams_bidi too large: 1152921504606846977 (maximum 1152921504606846976)",
		},
		{
			name: "initial max streams uni is too large",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(initialMaxStreamsUniParameterID))
				b = quicvarint.Append(b, uint64(quicvarint.Len(uint64(protocol.MaxStreamCount+1))))
				b = quicvarint.Append(b, uint64(protocol.MaxStreamCount+1))
				return appendInitialSourceConnectionID(b)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "initial_max_streams_uni too large: 1152921504606846977 (maximum 1152921504606846976)",
		},
		{
			name: "not enough data to read",
			data: func() []byte {
				b := quicvarint.Append(nil, 0x42)
				b = quicvarint.Append(b, 7)
				return append(b, []byte("foobar")...)
			}(),
			perspective:    protocol.PerspectiveServer,
			expectedErrMsg: "remaining length (6) smaller than parameter length (7)",
		},
		{
			name: "client sent stateless reset token",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(statelessResetTokenParameterID))
				b = quicvarint.Append(b, uint64(quicvarint.Len(16)))
				return append(b, make([]byte, 16)...)
			}(),
			perspective:    protocol.PerspectiveClient,
			expectedErrMsg: "client sent a stateless_reset_token",
		},
		{
			name: "client sent original destination connection ID",
			data: func() []byte {
				b := quicvarint.Append(nil, uint64(originalDestinationConnectionIDParameterID))
				b = quicvarint.Append(b, 6)
				return append(b, []byte("foobar")...)
			}(),
			perspective:    protocol.PerspectiveClient,
			expectedErrMsg: "client sent an original_destination_connection_id",
		},
		{
			name: "huge max ack delay value",
			data: func() []byte {
				val := uint64(math.MaxUint64) / 5
				b := quicvarint.Append(nil, uint64(maxAckDelayParameterID))
				b = quicvarint.Append(b, uint64(quicvarint.Len(val)))
				b = quicvarint.Append(b, val)
				return appendInitialSourceConnectionID(b)
			}(),
			perspective:    protocol.PerspectiveClient,
			expectedErrMsg: "invalid value for max_ack_delay: 3689348814741910323ms (maximum 16383ms)",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var err error
			if tt.params != nil {
				data := tt.params.Marshal(tt.perspective)
				err = (&TransportParameters{}).Unmarshal(data, tt.perspective)
			} else {
				err = (&TransportParameters{}).Unmarshal(tt.data, tt.perspective)
			}
			require.Error(t, err)
			transportErr, ok := err.(*qerr.TransportError)
			require.True(t, ok)
			require.Equal(t, qerr.TransportParameterError, transportErr.ErrorCode)
			require.Equal(t, tt.expectedErrMsg, transportErr.ErrorMessage)
		})
	}
}

func TestTransportParameterUnknownParameters(t *testing.T) {
	// write a known parameter
	b := quicvarint.Append(nil, uint64(initialMaxStreamDataBidiLocalParameterID))
	b = quicvarint.Append(b, uint64(quicvarint.Len(0x1337)))
	b = quicvarint.Append(b, 0x1337)
	// write an unknown parameter
	b = quicvarint.Append(b, 0x42)
	b = quicvarint.Append(b, 6)
	b = append(b, []byte("foobar")...)
	// write a known parameter
	b = quicvarint.Append(b, uint64(initialMaxStreamDataBidiRemoteParameterID))
	b = quicvarint.Append(b, uint64(quicvarint.Len(0x42)))
	b = quicvarint.Append(b, 0x42)
	b = appendInitialSourceConnectionID(b)
	p := &TransportParameters{}
	err := p.Unmarshal(b, protocol.PerspectiveClient)
	require.NoError(t, err)
	require.Equal(t, protocol.ByteCount(0x1337), p.InitialMaxStreamDataBidiLocal)
	require.Equal(t, protocol.ByteCount(0x42), p.InitialMaxStreamDataBidiRemote)
}

func TestTransportParameterRejectsDuplicateParameters(t *testing.T) {
	// write first parameter
	b := quicvarint.Append(nil, uint64(initialMaxStreamDataBidiLocalParameterID))
	b = quicvarint.Append(b, uint64(quicvarint.Len(0x1337)))
	b = quicvarint.Append(b, 0x1337)
	// write a second parameter
	b = quicvarint.Append(b, uint64(initialMaxStreamDataBidiRemoteParameterID))
	b = quicvarint.Append(b, uint64(quicvarint.Len(0x42)))
	b = quicvarint.Append(b, 0x42)
	// write first parameter again
	b = quicvarint.Append(b, uint64(initialMaxStreamDataBidiLocalParameterID))
	b = quicvarint.Append(b, uint64(quicvarint.Len(0x1337)))
	b = quicvarint.Append(b, 0x1337)
	b = appendInitialSourceConnectionID(b)
	err := (&TransportParameters{}).Unmarshal(b, protocol.PerspectiveClient)
	require.Error(t, err)
	transportErr, ok := err.(*qerr.TransportError)
	require.True(t, ok)
	require.Equal(t, qerr.TransportParameterError, transportErr.ErrorCode)
	require.Equal(t, fmt.Sprintf("received duplicate transport parameter %#x", initialMaxStreamDataBidiLocalParameterID), transportErr.ErrorMessage)
}

func TestPreferredAddressMarshalAndUnmarshal(t *testing.T) {
	pa := &PreferredAddress{
		IPv4:                netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 42),
		IPv6:                netip.AddrPortFrom(netip.AddrFrom16([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), 13),
		ConnectionID:        protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
		StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
	}

	data := (&TransportParameters{
		PreferredAddress:        pa,
		StatelessResetToken:     &protocol.StatelessResetToken{},
		ActiveConnectionIDLimit: 2,
	}).Marshal(protocol.PerspectiveServer)
	p := &TransportParameters{}
	err := p.Unmarshal(data, protocol.PerspectiveServer)
	require.NoError(t, err)
	require.Equal(t, pa.IPv4, p.PreferredAddress.IPv4)
	require.Equal(t, pa.IPv6, p.PreferredAddress.IPv6)
	require.Equal(t, pa.ConnectionID, p.PreferredAddress.ConnectionID)
	require.Equal(t, pa.StatelessResetToken, p.PreferredAddress.StatelessResetToken)
}

func TestPreferredAddressFromClient(t *testing.T) {
	b := quicvarint.Append(nil, uint64(preferredAddressParameterID))
	b = quicvarint.Append(b, 6)
	b = append(b, []byte("foobar")...)
	p := &TransportParameters{}
	err := p.Unmarshal(b, protocol.PerspectiveClient)
	require.Error(t, err)
	require.IsType(t, &qerr.TransportError{}, err)
	transportErr := err.(*qerr.TransportError)
	require.Equal(t, qerr.TransportParameterError, transportErr.ErrorCode)
	require.Equal(t, "client sent a preferred_address", transportErr.ErrorMessage)
}

func TestPreferredAddressZeroLengthConnectionID(t *testing.T) {
	pa := &PreferredAddress{
		IPv4:                netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 42),
		IPv6:                netip.AddrPortFrom(netip.AddrFrom16([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), 13),
		ConnectionID:        protocol.ParseConnectionID([]byte{}),
		StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
	}
	data := (&TransportParameters{
		PreferredAddress:    pa,
		StatelessResetToken: &protocol.StatelessResetToken{},
	}).Marshal(protocol.PerspectiveServer)
	p := &TransportParameters{}
	err := p.Unmarshal(data, protocol.PerspectiveServer)
	require.Error(t, err)
	require.IsType(t, &qerr.TransportError{}, err)
	transportErr := err.(*qerr.TransportError)
	require.Equal(t, qerr.TransportParameterError, transportErr.ErrorCode)
	require.Equal(t, "invalid connection ID length: 0", transportErr.ErrorMessage)
}

func TestPreferredAddressErrorOnEOF(t *testing.T) {
	raw := []byte{
		127, 0, 0, 1, // IPv4
		0, 42, // IPv4 Port
		1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // IPv6
		13, 37, // IPv6 Port,
		4, // conn ID len
		0xde, 0xad, 0xbe, 0xef,
		16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, // stateless reset token
	}
	for i := 1; i < len(raw); i++ {
		b := quicvarint.Append(nil, uint64(preferredAddressParameterID))
		b = append(b, raw[:i]...)
		p := &TransportParameters{}
		err := p.Unmarshal(b, protocol.PerspectiveServer)
		require.Error(t, err)
	}
}

func TestTransportParametersFromSessionTicket(t *testing.T) {
	params := &TransportParameters{
		InitialMaxStreamDataBidiLocal:  protocol.ByteCount(getRandomValue()),
		InitialMaxStreamDataBidiRemote: protocol.ByteCount(getRandomValue()),
		InitialMaxStreamDataUni:        protocol.ByteCount(getRandomValue()),
		InitialMaxData:                 protocol.ByteCount(getRandomValue()),
		MaxBidiStreamNum:               protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
		MaxUniStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
		ActiveConnectionIDLimit:        2 + getRandomValueUpTo(quicvarint.Max-2),
		MaxDatagramFrameSize:           protocol.ByteCount(getRandomValueUpTo(int64(MaxDatagramSize))),
	}
	require.True(t, params.ValidFor0RTT(params))
	b := params.MarshalForSessionTicket(nil)
	var tp TransportParameters
	require.NoError(t, tp.UnmarshalFromSessionTicket(b))
	require.Equal(t, params.InitialMaxStreamDataBidiLocal, tp.InitialMaxStreamDataBidiLocal)
	require.Equal(t, params.InitialMaxStreamDataBidiRemote, tp.InitialMaxStreamDataBidiRemote)
	require.Equal(t, params.InitialMaxStreamDataUni, tp.InitialMaxStreamDataUni)
	require.Equal(t, params.InitialMaxData, tp.InitialMaxData)
	require.Equal(t, params.MaxBidiStreamNum, tp.MaxBidiStreamNum)
	require.Equal(t, params.MaxUniStreamNum, tp.MaxUniStreamNum)
	require.Equal(t, params.ActiveConnectionIDLimit, tp.ActiveConnectionIDLimit)
	require.Equal(t, params.MaxDatagramFrameSize, tp.MaxDatagramFrameSize)
}

func TestSessionTicketInvalidTransportParameters(t *testing.T) {
	var p TransportParameters
	require.Error(t, p.UnmarshalFromSessionTicket([]byte("foobar")))
}

func TestSessionTicketTransportParameterVersionMismatch(t *testing.T) {
	var p TransportParameters
	data := p.MarshalForSessionTicket(nil)
	b := quicvarint.Append(nil, transportParameterMarshalingVersion+1)
	b = append(b, data[quicvarint.Len(transportParameterMarshalingVersion):]...)
	err := p.UnmarshalFromSessionTicket(b)
	require.EqualError(t, err, fmt.Sprintf("unknown transport parameter marshaling version: %d", transportParameterMarshalingVersion+1))
}

func TestTransportParametersValidFor0RTT(t *testing.T) {
	saved := &TransportParameters{
		InitialMaxStreamDataBidiLocal:  1,
		InitialMaxStreamDataBidiRemote: 2,
		InitialMaxStreamDataUni:        3,
		InitialMaxData:                 4,
		MaxBidiStreamNum:               5,
		MaxUniStreamNum:                6,
		ActiveConnectionIDLimit:        7,
		MaxDatagramFrameSize:           1000,
	}

	tests := []struct {
		name   string
		modify func(*TransportParameters)
		valid  bool
	}{
		{
			name:   "No Changes",
			modify: func(p *TransportParameters) {},
			valid:  true,
		},
		{
			name: "InitialMaxStreamDataBidiLocal reduced",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiLocal = saved.InitialMaxStreamDataBidiLocal - 1
			},
			valid: false,
		},
		{
			name: "InitialMaxStreamDataBidiLocal increased",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiLocal = saved.InitialMaxStreamDataBidiLocal + 1
			},
			valid: true,
		},
		{
			name: "InitialMaxStreamDataBidiRemote reduced",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiRemote = saved.InitialMaxStreamDataBidiRemote - 1
			},
			valid: false,
		},
		{
			name: "InitialMaxStreamDataBidiRemote increased",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiRemote = saved.InitialMaxStreamDataBidiRemote + 1
			},
			valid: true,
		},
		{
			name:   "InitialMaxStreamDataUni reduced",
			modify: func(p *TransportParameters) { p.InitialMaxStreamDataUni = saved.InitialMaxStreamDataUni - 1 },
			valid:  false,
		},
		{
			name:   "InitialMaxStreamDataUni increased",
			modify: func(p *TransportParameters) { p.InitialMaxStreamDataUni = saved.InitialMaxStreamDataUni + 1 },
			valid:  true,
		},
		{
			name:   "InitialMaxData reduced",
			modify: func(p *TransportParameters) { p.InitialMaxData = saved.InitialMaxData - 1 },
			valid:  false,
		},
		{
			name:   "InitialMaxData increased",
			modify: func(p *TransportParameters) { p.InitialMaxData = saved.InitialMaxData + 1 },
			valid:  true,
		},
		{
			name:   "MaxBidiStreamNum reduced",
			modify: func(p *TransportParameters) { p.MaxBidiStreamNum = saved.MaxBidiStreamNum - 1 },
			valid:  false,
		},
		{
			name:   "MaxBidiStreamNum increased",
			modify: func(p *TransportParameters) { p.MaxBidiStreamNum = saved.MaxBidiStreamNum + 1 },
			valid:  true,
		},
		{
			name:   "MaxUniStreamNum reduced",
			modify: func(p *TransportParameters) { p.MaxUniStreamNum = saved.MaxUniStreamNum - 1 },
			valid:  false,
		},
		{
			name:   "MaxUniStreamNum increased",
			modify: func(p *TransportParameters) { p.MaxUniStreamNum = saved.MaxUniStreamNum + 1 },
			valid:  true,
		},
		{
			name:   "ActiveConnectionIDLimit changed",
			modify: func(p *TransportParameters) { p.ActiveConnectionIDLimit = 0 },
			valid:  false,
		},
		{
			name:   "MaxDatagramFrameSize increased",
			modify: func(p *TransportParameters) { p.MaxDatagramFrameSize = saved.MaxDatagramFrameSize + 1 },
			valid:  true,
		},
		{
			name:   "MaxDatagramFrameSize reduced",
			modify: func(p *TransportParameters) { p.MaxDatagramFrameSize = saved.MaxDatagramFrameSize - 1 },
			valid:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := *saved
			tt.modify(&p)
			require.Equal(t, tt.valid, p.ValidFor0RTT(saved))
		})
	}
}

func TestTransportParametersValidAfter0RTT(t *testing.T) {
	saved := &TransportParameters{
		InitialMaxStreamDataBidiLocal:  1,
		InitialMaxStreamDataBidiRemote: 2,
		InitialMaxStreamDataUni:        3,
		InitialMaxData:                 4,
		MaxBidiStreamNum:               5,
		MaxUniStreamNum:                6,
		ActiveConnectionIDLimit:        7,
		MaxDatagramFrameSize:           1000,
	}

	tests := []struct {
		name   string
		modify func(*TransportParameters)
		reject bool
	}{
		{
			name:   "no changes",
			modify: func(p *TransportParameters) {},
			reject: false,
		},
		{
			name: "InitialMaxStreamDataBidiLocal reduced",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiLocal = saved.InitialMaxStreamDataBidiLocal - 1
			},
			reject: true,
		},
		{
			name: "InitialMaxStreamDataBidiLocal increased",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiLocal = saved.InitialMaxStreamDataBidiLocal + 1
			},
			reject: false,
		},
		{
			name: "InitialMaxStreamDataBidiRemote reduced",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiRemote = saved.InitialMaxStreamDataBidiRemote - 1
			},
			reject: true,
		},
		{
			name: "InitialMaxStreamDataBidiRemote increased",
			modify: func(p *TransportParameters) {
				p.InitialMaxStreamDataBidiRemote = saved.InitialMaxStreamDataBidiRemote + 1
			},
			reject: false,
		},
		{
			name:   "InitialMaxStreamDataUni reduced",
			modify: func(p *TransportParameters) { p.InitialMaxStreamDataUni = saved.InitialMaxStreamDataUni - 1 },
			reject: true,
		},
		{
			name:   "InitialMaxStreamDataUni increased",
			modify: func(p *TransportParameters) { p.InitialMaxStreamDataUni = saved.InitialMaxStreamDataUni + 1 },
			reject: false,
		},
		{
			name:   "InitialMaxData reduced",
			modify: func(p *TransportParameters) { p.InitialMaxData = saved.InitialMaxData - 1 },
			reject: true,
		},
		{
			name:   "InitialMaxData increased",
			modify: func(p *TransportParameters) { p.InitialMaxData = saved.InitialMaxData + 1 },
			reject: false,
		},
		{
			name:   "MaxBidiStreamNum reduced",
			modify: func(p *TransportParameters) { p.MaxBidiStreamNum = saved.MaxBidiStreamNum - 1 },
			reject: true,
		},
		{
			name:   "MaxBidiStreamNum increased",
			modify: func(p *TransportParameters) { p.MaxBidiStreamNum = saved.MaxBidiStreamNum + 1 },
			reject: false,
		},
		{
			name:   "MaxUniStreamNum reduced",
			modify: func(p *TransportParameters) { p.MaxUniStreamNum = saved.MaxUniStreamNum - 1 },
			reject: true,
		},
		{
			name:   "MaxUniStreamNum increased",
			modify: func(p *TransportParameters) { p.MaxUniStreamNum = saved.MaxUniStreamNum + 1 },
			reject: false,
		},
		{
			name:   "ActiveConnectionIDLimit reduced",
			modify: func(p *TransportParameters) { p.ActiveConnectionIDLimit = saved.ActiveConnectionIDLimit - 1 },
			reject: true,
		},
		{
			name:   "ActiveConnectionIDLimit increased",
			modify: func(p *TransportParameters) { p.ActiveConnectionIDLimit = saved.ActiveConnectionIDLimit + 1 },
			reject: false,
		},
		{
			name:   "MaxDatagramFrameSize reduced",
			modify: func(p *TransportParameters) { p.MaxDatagramFrameSize = saved.MaxDatagramFrameSize - 1 },
			reject: true,
		},
		{
			name:   "MaxDatagramFrameSize increased",
			modify: func(p *TransportParameters) { p.MaxDatagramFrameSize = saved.MaxDatagramFrameSize + 1 },
			reject: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := *saved
			tt.modify(&p)
			if tt.reject {
				require.False(t, p.ValidForUpdate(saved))
			} else {
				require.True(t, p.ValidForUpdate(saved))
			}
		})
	}
}

func BenchmarkTransportParameters(b *testing.B) {
	b.Run("without preferred address", func(b *testing.B) { benchmarkTransportParameters(b, false) })
	b.Run("with preferred address", func(b *testing.B) { benchmarkTransportParameters(b, true) })
}

func benchmarkTransportParameters(b *testing.B, withPreferredAddress bool) {
	var token protocol.StatelessResetToken
	rand.Read(token[:])
	rcid := protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde})
	params := &TransportParameters{
		InitialMaxStreamDataBidiLocal:   protocol.ByteCount(getRandomValue()),
		InitialMaxStreamDataBidiRemote:  protocol.ByteCount(getRandomValue()),
		InitialMaxStreamDataUni:         protocol.ByteCount(getRandomValue()),
		InitialMaxData:                  protocol.ByteCount(getRandomValue()),
		MaxIdleTimeout:                  0xcafe * time.Second,
		MaxBidiStreamNum:                protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
		MaxUniStreamNum:                 protocol.StreamNum(getRandomValueUpTo(int64(protocol.MaxStreamCount))),
		DisableActiveMigration:          true,
		StatelessResetToken:             &token,
		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
		InitialSourceConnectionID:       protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad}),
		RetrySourceConnectionID:         &rcid,
		AckDelayExponent:                13,
		MaxAckDelay:                     42 * time.Millisecond,
		ActiveConnectionIDLimit:         2 + getRandomValueUpTo(quicvarint.Max-2),
		MaxDatagramFrameSize:            protocol.ByteCount(getRandomValue()),
	}
	var token2 protocol.StatelessResetToken
	rand.Read(token2[:])
	if withPreferredAddress {
		var ip4 [4]byte
		var ip6 [16]byte
		rand.Read(ip4[:])
		rand.Read(ip6[:])
		params.PreferredAddress = &PreferredAddress{
			IPv4:                netip.AddrPortFrom(netip.AddrFrom4(ip4), 1234),
			IPv6:                netip.AddrPortFrom(netip.AddrFrom16(ip6), 4321),
			ConnectionID:        protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
			StatelessResetToken: token2,
		}
	}
	data := params.Marshal(protocol.PerspectiveServer)

	b.ResetTimer()
	b.ReportAllocs()
	var p TransportParameters
	for i := 0; i < b.N; i++ {
		if err := p.Unmarshal(data, protocol.PerspectiveServer); err != nil {
			b.Fatal(err)
		}
		// check a few fields
		if p.DisableActiveMigration != params.DisableActiveMigration ||
			p.InitialMaxStreamDataBidiLocal != params.InitialMaxStreamDataBidiLocal ||
			*p.StatelessResetToken != *params.StatelessResetToken ||
			p.AckDelayExponent != params.AckDelayExponent {
			b.Fatalf("params mismatch: %v vs %v", p, params)
		}
		if withPreferredAddress && *p.PreferredAddress != *params.PreferredAddress {
			b.Fatalf("preferred address mismatch: %v vs %v", p.PreferredAddress, params.PreferredAddress)
		}
	}
}
