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

//go:build go1.24 && goexperiment.synctest

package http3

import (
	"testing"
	"testing/synctest"
)

// Tests which apply to both client and server connections.

func TestConnCreatesControlStream(t *testing.T) {
	runConnTest(t, func(t testing.TB, tc *testQUICConn) {
		controlStream := tc.wantStream(streamTypeControl)
		controlStream.wantFrameHeader(
			"server sends SETTINGS frame on control stream",
			frameTypeSettings)
		controlStream.discardFrame()
	})
}

func TestConnUnknownUnidirectionalStream(t *testing.T) {
	// "Recipients of unknown stream types MUST either abort reading of the stream
	// or discard incoming data without further processing."
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-7
	runConnTest(t, func(t testing.TB, tc *testQUICConn) {
		st := tc.newStream(0x21) // reserved stream type

		// The endpoint should send a STOP_SENDING for this stream,
		// but it should not close the connection.
		synctest.Wait()
		if _, err := st.Write([]byte("hello")); err == nil {
			t.Fatalf("write to send-only stream with an unknown type succeeded; want error")
		}
		tc.wantNotClosed("after receiving unknown unidirectional stream type")
	})
}

func TestConnUnknownSettings(t *testing.T) {
	// "An implementation MUST ignore any [settings] parameter with
	// an identifier it does not understand."
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-9
	runConnTest(t, func(t testing.TB, tc *testQUICConn) {
		controlStream := tc.newStream(streamTypeControl)
		controlStream.writeSettings(0x1f+0x21, 0) // reserved settings type
		controlStream.Flush()
		tc.wantNotClosed("after receiving unknown settings")
	})
}

func TestConnInvalidSettings(t *testing.T) {
	// "These reserved settings MUST NOT be sent, and their receipt MUST
	// be treated as a connection error of type H3_SETTINGS_ERROR."
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-5
	runConnTest(t, func(t testing.TB, tc *testQUICConn) {
		controlStream := tc.newStream(streamTypeControl)
		controlStream.writeSettings(0x02, 0) // HTTP/2 SETTINGS_ENABLE_PUSH
		controlStream.Flush()
		tc.wantClosed("invalid setting", errH3SettingsError)
	})
}

func TestConnDuplicateStream(t *testing.T) {
	for _, stype := range []streamType{
		streamTypeControl,
		streamTypeEncoder,
		streamTypeDecoder,
	} {
		t.Run(stype.String(), func(t *testing.T) {
			runConnTest(t, func(t testing.TB, tc *testQUICConn) {
				_ = tc.newStream(stype)
				tc.wantNotClosed("after creating one " + stype.String() + " stream")

				// Opening a second control, encoder, or decoder stream
				// is a protocol violation.
				_ = tc.newStream(stype)
				tc.wantClosed("duplicate stream", errH3StreamCreationError)
			})
		})
	}
}

func TestConnUnknownFrames(t *testing.T) {
	for _, stype := range []streamType{
		streamTypeControl,
	} {
		t.Run(stype.String(), func(t *testing.T) {
			runConnTest(t, func(t testing.TB, tc *testQUICConn) {
				st := tc.newStream(stype)

				if stype == streamTypeControl {
					// First frame on the control stream must be settings.
					st.writeVarint(int64(frameTypeSettings))
					st.writeVarint(0) // size
				}

				data := "frame content"
				st.writeVarint(0x1f + 0x21)      // reserved frame type
				st.writeVarint(int64(len(data))) // size
				st.Write([]byte(data))
				st.Flush()

				tc.wantNotClosed("after writing unknown frame")
			})
		})
	}
}

func TestConnInvalidFrames(t *testing.T) {
	runConnTest(t, func(t testing.TB, tc *testQUICConn) {
		control := tc.newStream(streamTypeControl)

		// SETTINGS frame.
		control.writeVarint(int64(frameTypeSettings))
		control.writeVarint(0) // size

		// DATA frame (invalid on the control stream).
		control.writeVarint(int64(frameTypeData))
		control.writeVarint(0) // size
		control.Flush()
		tc.wantClosed("after writing DATA frame to control stream", errH3FrameUnexpected)
	})
}

func TestConnPeerCreatesBadUnidirectionalStream(t *testing.T) {
	runConnTest(t, func(t testing.TB, tc *testQUICConn) {
		// Create and close a stream without sending the unidirectional stream header.
		qs, err := tc.qconn.NewSendOnlyStream(canceledCtx)
		if err != nil {
			t.Fatal(err)
		}
		st := newTestQUICStream(tc.t, newStream(qs))
		st.stream.stream.Close()

		tc.wantClosed("after peer creates and closes uni stream", errH3StreamCreationError)
	})
}

func runConnTest(t *testing.T, f func(testing.TB, *testQUICConn)) {
	t.Helper()
	runSynctestSubtest(t, "client", func(t testing.TB) {
		tc := newTestClientConn(t)
		f(t, tc.testQUICConn)
	})
}
