// Copyright 2025 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.24 && goexperiment.synctest

package http3

import (
	"reflect"
	"strings"
	"testing"
)

func TestQPACKDecode(t *testing.T) {
	type header struct {
		itype       indexType
		name, value string
	}
	// Many test cases here taken from Google QUICHE,
	// quiche/quic/core/qpack/qpack_encoder_test.cc.
	for _, test := range []struct {
		name string
		enc  []byte
		want []header
	}{{
		name: "empty",
		enc:  unhex("0000"),
		want: []header{},
	}, {
		name: "literal entry empty value",
		enc:  unhex("000023666f6f00"),
		want: []header{
			{mayIndex, "foo", ""},
		},
	}, {
		name: "simple literal entry",
		enc:  unhex("000023666f6f03626172"),
		want: []header{
			{mayIndex, "foo", "bar"},
		},
	}, {
		name: "multiple literal entries",
		enc: unhex("0000" + // prefix
			// foo: bar
			"23666f6f03626172" +
			// 7 octet long header name, the smallest number
			// that does not fit on a 3-bit prefix.
			"2700666f6f62616172" +
			// 127 octet long header value, the smallest number
			// that does not fit on a 7-bit prefix.
			"7f00616161616161616161616161616161616161616161616161616161616161616161" +
			"6161616161616161616161616161616161616161616161616161616161616161616161" +
			"6161616161616161616161616161616161616161616161616161616161616161616161" +
			"616161616161616161616161616161616161616161616161",
		),
		want: []header{
			{mayIndex, "foo", "bar"},
			{mayIndex, "foobaar", strings.Repeat("a", 127)},
		},
	}, {
		name: "line feed in value",
		enc:  unhex("000023666f6f0462610a72"),
		want: []header{
			{mayIndex, "foo", "ba\nr"},
		},
	}, {
		name: "huffman simple",
		enc:  unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4bf"),
		want: []header{
			{mayIndex, "custom-key", "custom-value"},
		},
	}, {
		name: "alternating huffman nonhuffman",
		enc: unhex("0000" + // Prefix.
			"2f0125a849e95ba97d7f" + // Huffman-encoded name.
			"8925a849e95bb8e8b4bf" + // Huffman-encoded value.
			"2703637573746f6d2d6b6579" + // Non-Huffman encoded name.
			"0c637573746f6d2d76616c7565" + // Non-Huffman encoded value.
			"2f0125a849e95ba97d7f" + // Huffman-encoded name.
			"0c637573746f6d2d76616c7565" + // Non-Huffman encoded value.
			"2703637573746f6d2d6b6579" + // Non-Huffman encoded name.
			"8925a849e95bb8e8b4bf", // Huffman-encoded value.
		),
		want: []header{
			{mayIndex, "custom-key", "custom-value"},
			{mayIndex, "custom-key", "custom-value"},
			{mayIndex, "custom-key", "custom-value"},
			{mayIndex, "custom-key", "custom-value"},
		},
	}, {
		name: "static table",
		enc:  unhex("0000d1d45f00055452414345dfcc5f108621e9aec2a11f5c8294e75f1000"),
		want: []header{
			{mayIndex, ":method", "GET"},
			{mayIndex, ":method", "POST"},
			{mayIndex, ":method", "TRACE"},
			{mayIndex, "accept-encoding", "gzip, deflate, br"},
			{mayIndex, "location", ""},
			{mayIndex, "accept-encoding", "compress"},
			{mayIndex, "location", "foo"},
			{mayIndex, "accept-encoding", ""},
		},
	}} {
		runSynctestSubtest(t, test.name, func(t testing.TB) {
			st1, st2 := newStreamPair(t)
			st1.Write(test.enc)
			st1.Flush()

			st2.lim = int64(len(test.enc))

			var dec qpackDecoder
			got := []header{}
			err := dec.decode(st2, func(itype indexType, name, value string) error {
				got = append(got, header{itype, name, value})
				return nil
			})
			if err != nil {
				t.Fatalf("decode: %v", err)
			}
			if !reflect.DeepEqual(got, test.want) {
				t.Errorf("encoded: %x", test.enc)
				t.Errorf("got headers:")
				for _, h := range got {
					t.Errorf("  %v: %q", h.name, h.value)
				}
				t.Errorf("want headers:")
				for _, h := range test.want {
					t.Errorf("  %v: %q", h.name, h.value)
				}
			}
		})
	}
}

func TestQPACKDecodeErrors(t *testing.T) {
	// Many test cases here taken from Google QUICHE,
	// quiche/quic/core/qpack/qpack_encoder_test.cc.
	for _, test := range []struct {
		name string
		enc  []byte
	}{{
		name: "literal entry empty name",
		enc:  unhex("00002003666f6f"),
	}, {
		name: "literal entry empty name and value",
		enc:  unhex("00002000"),
	}, {
		name: "name length too large for varint",
		enc:  unhex("000027ffffffffffffffffffff"),
	}, {
		name: "string literal too long",
		enc:  unhex("000027ffff7f"),
	}, {
		name: "value length too large for varint",
		enc:  unhex("000023666f6f7fffffffffffffffffffff"),
	}, {
		name: "value length too long",
		enc:  unhex("000023666f6f7fffff7f"),
	}, {
		name: "incomplete header block",
		enc:  unhex("00002366"),
	}, {
		name: "huffman name does not have eos prefix",
		enc:  unhex("00002f0125a849e95ba97d7e8925a849e95bb8e8b4bf"),
	}, {
		name: "huffman value does not have eos prefix",
		enc:  unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4be"),
	}, {
		name: "huffman name eos prefix too long",
		enc:  unhex("00002f0225a849e95ba97d7fff8925a849e95bb8e8b4bf"),
	}, {
		name: "huffman value eos prefix too long",
		enc:  unhex("00002f0125a849e95ba97d7f8a25a849e95bb8e8b4bfff"),
	}, {
		name: "too high static table index",
		enc:  unhex("0000ff23ff24"),
	}} {
		runSynctestSubtest(t, test.name, func(t testing.TB) {
			st1, st2 := newStreamPair(t)
			st1.Write(test.enc)
			st1.Flush()

			st2.lim = int64(len(test.enc))

			var dec qpackDecoder
			err := dec.decode(st2, func(itype indexType, name, value string) error {
				return nil
			})
			if err == nil {
				t.Errorf("encoded: %x", test.enc)
				t.Fatalf("decode succeeded; want error")
			}
		})
	}
}
