// Copyright 2024 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.

package http3

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"maps"
	"math"
	"net/http"
	"reflect"
	"slices"
	"testing"
	"testing/synctest"

	"golang.org/x/net/internal/quic/quicwire"
	"golang.org/x/net/quic"
)

func TestTransportServerCreatesBidirectionalStream(t *testing.T) {
	// "Clients MUST treat receipt of a server-initiated bidirectional
	// stream as a connection error of type H3_STREAM_CREATION_ERROR [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3
	synctest.Test(t, func(t *testing.T) {
		tc := newTestClientConn(t)
		tc.greet()
		st := tc.newStream(streamTypeRequest)
		st.Flush()
		tc.wantClosed("after server creates bidi stream", errH3StreamCreationError)
	})
}

func TestClientConnMethods(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		var called bool
		hook := func() {
			if called {
				t.Error("state hook was unexpectedly called")
			}
			called = true
		}
		verifyHookWasCalled := func() {
			if !called {
				t.Error("state hook was unexpectedly not called")
			}
			called = false
		}
		tc := newTestClientConnWithHook(t, hook)
		tc.greet()

		// Initial state after establishing connection.
		if err := tc.cc.Err(); err != nil {
			t.Errorf("cc.Err() = %v, want nil", err)
		}
		if avail := tc.cc.Available(); avail != math.MaxInt {
			t.Errorf("cc.Available() = %v, want %v", avail, math.MaxInt)
		}
		if inFlight := tc.cc.InFlight(); inFlight != 0 {
			t.Errorf("cc.InFlight() = %v, want 0", inFlight)
		}

		// Release, with Reserve before and without.
		for _, reserveBefore := range []bool{true, false} {
			if reserveBefore {
				if err := tc.cc.Reserve(); err != nil {
					t.Fatalf("cc.Reserve() failed: %v", err)
				}
				if avail := tc.cc.Available(); avail != math.MaxInt {
					t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt)
				}
				if inFlight := tc.cc.InFlight(); inFlight != 1 {
					t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight)
				}
			}
			tc.cc.Release()
			if avail := tc.cc.Available(); avail != math.MaxInt {
				t.Errorf("after Release, cc.Available() = %v, want %v", avail, math.MaxInt)
			}
			if inFlight := tc.cc.InFlight(); inFlight != 0 {
				t.Errorf("after Release, cc.InFlight() = %v, want 0", inFlight)
			}
		}

		// RoundTrip, with Reserve before and without.
		for _, reserveBefore := range []bool{true, false} {
			if reserveBefore {
				if err := tc.cc.Reserve(); err != nil {
					t.Fatalf("cc.Reserve() failed: %v", err)
				}
				if avail := tc.cc.Available(); avail != math.MaxInt {
					t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt)
				}
				if inFlight := tc.cc.InFlight(); inFlight != 1 {
					t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight)
				}
			}
			req, _ := http.NewRequest("GET", "https://example.com/", nil)
			rt := tc.roundTrip(req)
			if avail := tc.cc.Available(); avail != math.MaxInt {
				t.Errorf("after RoundTrip, cc.Available() = %v, want %v", avail, math.MaxInt)
			}
			st := tc.wantStream(streamTypeRequest)
			st.wantHeaders(nil)
			st.writeHeaders(http.Header{":status": []string{"200"}})
			resp := rt.response()
			if resp.StatusCode != 200 {
				t.Errorf("resp.StatusCode = %v, want 200", resp.StatusCode)
			}
			if inFlight := tc.cc.InFlight(); inFlight != 1 { // InFlight should decrement only after the body is closed.
				t.Errorf("before body close, cc.InFlight() = %v, want 1", inFlight)
			}
			resp.Body.Close()
			if inFlight := tc.cc.InFlight(); inFlight != 0 {
				t.Errorf("after body close, cc.InFlight() = %v, want 0", inFlight)
			}
			verifyHookWasCalled()
		}

		// Connection closure.
		if err := tc.cc.Reserve(); err != nil {
			t.Fatalf("cc.Reserve() failed: %v", err)
		}
		tc.cc.Close()
		synctest.Wait()
		verifyHookWasCalled()
		if err := tc.cc.Err(); err == nil {
			t.Error("after connection is closed, cc.Err() = nil, want err")
		}
		if avail := tc.cc.Available(); avail != 0 {
			t.Errorf("after connection is closed, cc.Available() = %v, want 0", avail)
		}
		if inFlight := tc.cc.InFlight(); inFlight != 0 {
			t.Errorf("after connection is closed, cc.InFlight() = %v, want 0", inFlight)
		}
	})
}

// A testQUICConn wraps a *quic.Conn and provides methods for inspecting it.
type testQUICConn struct {
	t       testing.TB
	qconn   *quic.Conn
	streams map[streamType][]*testQUICStream
}

func newTestQUICConn(t testing.TB, qconn *quic.Conn) *testQUICConn {
	tq := &testQUICConn{
		t:       t,
		qconn:   qconn,
		streams: make(map[streamType][]*testQUICStream),
	}

	go tq.acceptStreams(t.Context())

	t.Cleanup(func() {
		tq.qconn.Close()
	})
	return tq
}

func (tq *testQUICConn) acceptStreams(ctx context.Context) {
	for {
		qst, err := tq.qconn.AcceptStream(ctx)
		if err != nil {
			return
		}
		st := newStream(qst)
		stype := streamTypeRequest
		if qst.IsReadOnly() {
			v, err := st.readVarint()
			if err != nil {
				tq.t.Errorf("error reading stream type from unidirectional stream: %v", err)
				continue
			}
			stype = streamType(v)
		}
		tq.streams[stype] = append(tq.streams[stype], newTestQUICStream(tq.t, st))
	}
}

func (tq *testQUICConn) newStream(stype streamType) *testQUICStream {
	tq.t.Helper()
	var qs *quic.Stream
	var err error
	if stype == streamTypeRequest {
		qs, err = tq.qconn.NewStream(canceledCtx)
	} else {
		qs, err = tq.qconn.NewSendOnlyStream(canceledCtx)
	}
	if err != nil {
		tq.t.Fatal(err)
	}
	st := newStream(qs)
	if stype != streamTypeRequest {
		st.writeVarint(int64(stype))
		if err := st.Flush(); err != nil {
			tq.t.Fatal(err)
		}
	}
	return newTestQUICStream(tq.t, st)
}

// wantNotClosed asserts that the peer has not closed the connection.
func (tq *testQUICConn) wantNotClosed(reason string) {
	t := tq.t
	t.Helper()
	synctest.Wait()
	err := tq.qconn.Wait(canceledCtx)
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("%v: want QUIC connection to be alive; closed with error: %v", reason, err)
	}
}

// wantClosed asserts that the peer has closed the connection
// with the provided error code.
func (tq *testQUICConn) wantClosed(reason string, want error) {
	t := tq.t
	t.Helper()
	synctest.Wait()

	if e, ok := want.(http3Error); ok {
		want = &quic.ApplicationError{Code: uint64(e)}
	}
	got := tq.qconn.Wait(canceledCtx)
	if errors.Is(got, context.Canceled) {
		t.Fatalf("%v: want QUIC connection closed, but it is not", reason)
	}
	if !errors.Is(got, want) {
		t.Fatalf("%v: connection closed with error: %v; want %v", reason, got, want)
	}
}

// wantStream asserts that a stream of a given type has been created,
// and returns that stream.
func (tq *testQUICConn) wantStream(stype streamType) *testQUICStream {
	tq.t.Helper()
	synctest.Wait()
	if len(tq.streams[stype]) == 0 {
		tq.t.Fatalf("expected a %v stream to be created, but none were", stype)
	}
	ts := tq.streams[stype][0]
	tq.streams[stype] = tq.streams[stype][1:]
	return ts
}

// testQUICStream wraps a QUIC stream and provides methods for inspecting it.
type testQUICStream struct {
	t testing.TB
	*stream
}

func newTestQUICStream(t testing.TB, st *stream) *testQUICStream {
	st.stream.SetReadContext(canceledCtx)
	st.stream.SetWriteContext(canceledCtx)
	return &testQUICStream{
		t:      t,
		stream: st,
	}
}

func (ts *testQUICStream) wantIdle(reason string) {
	ts.t.Helper()
	synctest.Wait()
	qs := ts.stream.stream
	qs.SetReadContext(canceledCtx)
	if _, err := qs.Read(make([]byte, 1)); !errors.Is(err, context.Canceled) {
		ts.t.Fatalf("%v: want stream to be idle, but stream has content", reason)
	}
	qs.SetReadContext(nil)
}

// wantFrameHeader calls readFrameHeader and asserts that the frame is of a given type.
func (ts *testQUICStream) wantFrameHeader(reason string, wantType frameType) {
	ts.t.Helper()
	synctest.Wait()
	gotType, err := ts.readFrameHeader()
	if err != nil {
		ts.t.Fatalf("%v: failed to read frame header: %v", reason, err)
	}
	if gotType != wantType {
		ts.t.Fatalf("%v: got frame type %v, want %v", reason, gotType, wantType)
	}
}

// wantHeaders reads a HEADERS frame.
// If want is nil, the contents of the frame are ignored.
func (ts *testQUICStream) wantHeaders(want http.Header) {
	ts.t.Helper()
	synctest.Wait()
	ftype, err := ts.readFrameHeader()
	if err != nil {
		ts.t.Fatalf("want HEADERS frame, got error: %v", err)
	}
	if ftype != frameTypeHeaders {
		ts.t.Fatalf("want HEADERS frame, got: %v", ftype)
	}

	if want == nil {
		if err := ts.discardFrame(); err != nil {
			ts.t.Fatalf("discardFrame: %v", err)
		}
		return
	}

	got := make(http.Header)
	var dec qpackDecoder
	err = dec.decode(ts.stream, func(_ indexType, name, value string) error {
		got.Add(name, value)
		return nil
	})
	if diff := diffHeaders(got, want); diff != "" {
		ts.t.Fatalf("unexpected response headers:\n%v", diff)
	}
	if err := ts.endFrame(); err != nil {
		ts.t.Fatalf("endFrame: %v", err)
	}
}

// wantSomeHeaders reads a HEADERS frame and asserts that want is a subset of
// the read HEADERS frame.
// This is like wantHeaders, but headers that are in the HEADERS frame but not
// in want are ignored.
func (ts *testQUICStream) wantSomeHeaders(want http.Header) {
	ts.t.Helper()
	synctest.Wait()
	ftype, err := ts.readFrameHeader()
	if err != nil {
		ts.t.Fatalf("want HEADERS frame, got error: %v", err)
	}
	if ftype != frameTypeHeaders {
		ts.t.Fatalf("want HEADERS frame, got: %v", ftype)
	}

	if want == nil {
		panic("use wantHeaders(nil) instead to ignore the content of the frame")
	}

	got := make(http.Header)
	var dec qpackDecoder
	err = dec.decode(ts.stream, func(_ indexType, name, value string) error {
		got.Add(name, value)
		return nil
	})
	for name := range got {
		if _, ok := want[name]; !ok {
			delete(got, name)
		}
	}
	if diff := diffHeaders(got, want); diff != "" {
		ts.t.Fatalf("unexpected response headers:\n%v", diff)
	}
	if err := ts.endFrame(); err != nil {
		ts.t.Fatalf("endFrame: %v", err)
	}
}

func (ts *testQUICStream) encodeHeaders(h http.Header) []byte {
	ts.t.Helper()
	var enc qpackEncoder
	return enc.encode(func(yield func(itype indexType, name, value string)) {
		names := slices.Collect(maps.Keys(h))
		slices.Sort(names)
		for _, k := range names {
			for _, v := range h[k] {
				yield(mayIndex, k, v)
			}
		}
	})
}

func (ts *testQUICStream) writeHeaders(h http.Header) {
	ts.t.Helper()
	headers := ts.encodeHeaders(h)
	ts.writeVarint(int64(frameTypeHeaders))
	ts.writeVarint(int64(len(headers)))
	ts.Write(headers)
	if err := ts.Flush(); err != nil {
		ts.t.Fatalf("flushing HEADERS frame: %v", err)
	}
}

// writeHeadersRaw is just like writeHeaders, but avoids qpackEncoder.encode,
// which will automatically make sure that headers are encoded correctly, e.g.
// making field names all lowercase, skipping non-ASCII names.
// This method can be used to test that we properly reject invalid headers.
func (ts *testQUICStream) writeHeadersRaw(h http.Header) {
	ts.t.Helper()
	var b []byte
	b = appendPrefixedInt(b, 0, 8, 0) // Required Insert Count
	b = appendPrefixedInt(b, 0, 7, 0) // Delta Base

	names := slices.Collect(maps.Keys(h))
	slices.Sort(names)
	for _, k := range names {
		for _, v := range h[k] {
			if i, ok := staticTableByNameValue[tableEntry{k, v}]; ok {
				b = appendIndexedFieldLine(b, staticTable, i)
			} else if i, ok := staticTableByName[k]; ok {
				b = appendLiteralFieldLineWithNameReference(b, staticTable, mayIndex, i, v)
			} else {
				b = appendLiteralFieldLineWithLiteralName(b, mayIndex, k, v)
			}
		}
	}
	headers := b
	ts.writeVarint(int64(frameTypeHeaders))
	ts.writeVarint(int64(len(headers)))
	ts.Write(headers)
	if err := ts.Flush(); err != nil {
		ts.t.Fatalf("flushing HEADERS frame: %v", err)
	}
}

func (ts *testQUICStream) writeData(b []byte) {
	ts.t.Helper()
	ts.writeVarint(int64(frameTypeData))
	ts.writeVarint(int64(len(b)))
	ts.Write(b)
	if err := ts.Flush(); err != nil {
		ts.t.Fatalf("flushing DATA frame: %v", err)
	}
}

func (ts *testQUICStream) wantData(want []byte) {
	ts.t.Helper()
	synctest.Wait()
	ftype, err := ts.readFrameHeader()
	if err != nil {
		ts.t.Fatalf("want DATA frame, got error: %v", err)
	}
	if ftype != frameTypeData {
		ts.t.Fatalf("want DATA frame, got: %v", ftype)
	}
	got, err := ts.readFrameData()
	if err != nil {
		ts.t.Fatalf("error reading DATA frame: %v", err)
	}
	if !bytes.Equal(got, want) {
		ts.t.Fatalf("got data: {%x}, want {%x}", got, want)
	}
	if err := ts.endFrame(); err != nil {
		ts.t.Fatalf("endFrame: %v", err)
	}
}

func (ts *testQUICStream) wantClosed(reason string) {
	ts.t.Helper()
	synctest.Wait()
	ftype, err := ts.readFrameHeader()
	if err != io.EOF {
		ts.t.Fatalf("%v: want io.EOF, got %v %v", reason, ftype, err)
	}
}

func (ts *testQUICStream) wantError(want quic.StreamErrorCode) {
	ts.t.Helper()
	synctest.Wait()
	_, err := ts.stream.stream.ReadByte()
	if err == nil {
		ts.t.Fatalf("successfully read from stream; want stream error code %v", want)
	}
	var got quic.StreamErrorCode
	if !errors.As(err, &got) {
		ts.t.Fatalf("stream error = %v; want %v", err, want)
	}
	if got != want {
		ts.t.Fatalf("stream error code = %v; want %v", got, want)
	}
}

func (ts *testQUICStream) wantSettings(f func(settingType, value int64) error) {
	ts.t.Helper()
	synctest.Wait()
	if f == nil {
		f = func(settingType, value int64) error { return nil }
	}
	if err := ts.readSettings(f); err != nil {
		ts.t.Fatalf("f returned an error: %v", err)
	}
}

func (ts *testQUICStream) wantGoaway(wantID int64) {
	ts.t.Helper()
	synctest.Wait()
	ftype, err := ts.readFrameHeader()
	if err != nil {
		ts.t.Fatalf("want GOAWAY frame, got error: %v", err)
	}
	if ftype != frameTypeGoaway {
		ts.t.Fatalf("want GOAWAY frame, got: %v", ftype)
	}
	gotID, err := ts.readVarint()
	if err != nil {
		ts.t.Fatalf("failed reading GOAWAY frame, got error: %v", err)
	}
	if gotID != wantID {
		ts.t.Fatalf("got stream ID %v from GOAWAY frame, want %v stream ID", gotID, wantID)
	}
}

func (ts *testQUICStream) writePushPromise(pushID int64, h http.Header) {
	ts.t.Helper()
	headers := ts.encodeHeaders(h)
	ts.writeVarint(int64(frameTypePushPromise))
	ts.writeVarint(int64(quicwire.SizeVarint(uint64(pushID)) + len(headers)))
	ts.writeVarint(pushID)
	ts.Write(headers)
	if err := ts.Flush(); err != nil {
		ts.t.Fatalf("flushing PUSH_PROMISE frame: %v", err)
	}
}

func diffHeaders(got, want http.Header) string {
	// nil and 0-length non-nil are equal.
	if len(got) == 0 && len(want) == 0 {
		return ""
	}
	// We could do a more sophisticated diff here.
	// DeepEqual is good enough for now.
	if reflect.DeepEqual(got, want) {
		return ""
	}
	return fmt.Sprintf("got:  %v\nwant: %v", got, want)
}

func (ts *testQUICStream) Flush() error {
	err := ts.stream.Flush()
	ts.t.Helper()
	if err != nil {
		ts.t.Errorf("unexpected error flushing stream: %v", err)
	}
	return err
}

// A testClientConn is a ClientConn on a test network.
type testClientConn struct {
	tr *transport
	cc *clientConn

	// *testQUICConn is the server half of the connection.
	*testQUICConn
	control *testQUICStream
}

func newTestClientConnWithHook(t testing.TB, stateHook func()) *testClientConn {
	e1, e2 := newQUICEndpointPair(t)
	tr := &transport{
		endpoint: e1,
		config: &quic.Config{
			TLSConfig: testTLSConfig,
		},
		tr1:         new(http.Transport),
		activeConns: make(map[*clientConn]struct{}),
	}

	cc, err := tr.dial(t.Context(), e2.LocalAddr().String(), stateHook)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		cc.Close()
	})
	srvConn, err := e2.Accept(t.Context())
	if err != nil {
		t.Fatal(err)
	}

	tc := &testClientConn{
		tr:           tr,
		cc:           cc,
		testQUICConn: newTestQUICConn(t, srvConn),
	}
	synctest.Wait()
	return tc
}

func newTestClientConn(t testing.TB) *testClientConn {
	return newTestClientConnWithHook(t, nil)
}

// greet performs initial connection handshaking with the client.
func (tc *testClientConn) greet() {
	// Client creates a control stream.
	clientControlStream := tc.wantStream(streamTypeControl)
	clientControlStream.wantFrameHeader(
		"client sends SETTINGS frame on control stream",
		frameTypeSettings)
	clientControlStream.discardFrame()

	// Server creates a control stream.
	tc.control = tc.newStream(streamTypeControl)
	tc.control.writeVarint(int64(frameTypeSettings))
	tc.control.writeVarint(0) // size
	tc.control.Flush()

	synctest.Wait()
}

type testRoundTrip struct {
	t       testing.TB
	resp    *http.Response
	respErr error
}

func (rt *testRoundTrip) done() bool {
	synctest.Wait()
	return rt.resp != nil || rt.respErr != nil
}

func (rt *testRoundTrip) result() (*http.Response, error) {
	rt.t.Helper()
	if !rt.done() {
		rt.t.Fatal("RoundTrip is not done; want it to be")
	}
	return rt.resp, rt.respErr
}

func (rt *testRoundTrip) response() *http.Response {
	rt.t.Helper()
	if !rt.done() {
		rt.t.Fatal("RoundTrip is not done; want it to be")
	}
	if rt.respErr != nil {
		rt.t.Fatalf("RoundTrip returned unexpected error: %v", rt.respErr)
	}
	return rt.resp
}

// err returns the (possibly nil) error result of RoundTrip.
func (rt *testRoundTrip) err() error {
	rt.t.Helper()
	_, err := rt.result()
	return err
}

func (rt *testRoundTrip) wantError(reason string) {
	rt.t.Helper()
	synctest.Wait()
	if !rt.done() {
		rt.t.Fatalf("%v: RoundTrip is not done; want it to have returned an error", reason)
	}
	if rt.respErr == nil {
		rt.t.Fatalf("%v: RoundTrip succeeded; want it to have returned an error", reason)
	}
}

// wantStatus indicates the expected response StatusCode.
func (rt *testRoundTrip) wantStatus(want int) {
	rt.t.Helper()
	if got := rt.response().StatusCode; got != want {
		rt.t.Fatalf("got response status %v, want %v", got, want)
	}
}

func (rt *testRoundTrip) wantHeaders(want http.Header) {
	rt.t.Helper()
	if diff := diffHeaders(rt.response().Header, want); diff != "" {
		rt.t.Fatalf("unexpected response headers:\n%v", diff)
	}
}

func (rt *testRoundTrip) wantTrailers(want http.Header) {
	rt.t.Helper()
	if diff := diffHeaders(rt.response().Trailer, want); diff != "" {
		rt.t.Fatalf("unexpected response trailers:\n%v", diff)
	}
}

// readBody reads the contents of the response body.
func (rt *testRoundTrip) readBody() ([]byte, error) {
	t := rt.t
	t.Helper()
	return io.ReadAll(rt.response().Body)
}

// wantBody consumes the a body and asserts that it is as expected.
func (rt *testRoundTrip) wantBody(want []byte) {
	t := rt.t
	t.Helper()
	got, err := rt.readBody()
	if err != nil {
		t.Fatalf("unexpected error reading response body: %v", err)
	}
	if !bytes.Equal(got, want) {
		t.Fatalf("unexpected response body:\ngot:  %q\nwant: %q", got, want)
	}
}

func (tc *testClientConn) roundTrip(req *http.Request) *testRoundTrip {
	rt := &testRoundTrip{t: tc.t}
	go func() {
		rt.resp, rt.respErr = tc.cc.RoundTrip(req)
	}()
	return rt
}
