package self_test

import (
	"context"
	"fmt"
	"net"
	"sync/atomic"
	"time"

	"github.com/lucas-clemente/quic-go"
	quicproxy "github.com/lucas-clemente/quic-go/integrationtests/tools/proxy"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Packetization", func() {
	var (
		server   quic.Listener
		proxy    *quicproxy.QuicProxy
		incoming uint32
		outgoing uint32
	)

	BeforeEach(func() {
		incoming = 0
		outgoing = 0
		var err error
		server, err = quic.ListenAddr(
			"localhost:0",
			getTLSConfig(),
			getQuicConfig(&quic.Config{AcceptToken: func(net.Addr, *quic.Token) bool { return true }}),
		)
		Expect(err).ToNot(HaveOccurred())
		serverAddr := fmt.Sprintf("localhost:%d", server.Addr().(*net.UDPAddr).Port)

		proxy, err = quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
			RemoteAddr: serverAddr,
			DelayPacket: func(dir quicproxy.Direction, _ []byte) time.Duration {
				//nolint:exhaustive
				switch dir {
				case quicproxy.DirectionIncoming:
					atomic.AddUint32(&incoming, 1)
				case quicproxy.DirectionOutgoing:
					atomic.AddUint32(&outgoing, 1)
				}
				return 5 * time.Millisecond
			},
		})
		Expect(err).ToNot(HaveOccurred())
	})

	AfterEach(func() {
		Expect(proxy.Close()).To(Succeed())
		Expect(server.Close()).To(Succeed())
	})

	// In this test, the client sends 100 small messages. The server echoes these messages.
	// This means that every endpoint will send 100 ack-eliciting packets in short succession.
	// This test then tests that no more than 110 packets are sent in every direction, making sure that ACK are bundled.
	It("bundles ACKs", func() {
		const numMsg = 100

		sess, err := quic.DialAddr(
			fmt.Sprintf("localhost:%d", proxy.LocalPort()),
			getTLSClientConfig(),
			getQuicConfig(nil),
		)
		Expect(err).ToNot(HaveOccurred())

		go func() {
			defer GinkgoRecover()
			sess, err := server.Accept(context.Background())
			Expect(err).ToNot(HaveOccurred())
			str, err := sess.AcceptStream(context.Background())
			Expect(err).ToNot(HaveOccurred())
			b := make([]byte, 1)
			// Echo every byte received from the client.
			for {
				if _, err := str.Read(b); err != nil {
					break
				}
				_, err = str.Write(b)
				Expect(err).ToNot(HaveOccurred())
			}
		}()

		str, err := sess.OpenStreamSync(context.Background())
		Expect(err).ToNot(HaveOccurred())
		b := make([]byte, 1)
		// Send numMsg 1-byte messages.
		for i := 0; i < numMsg; i++ {
			_, err = str.Write([]byte{uint8(i)})
			Expect(err).ToNot(HaveOccurred())
			_, err = str.Read(b)
			Expect(err).ToNot(HaveOccurred())
			Expect(b[0]).To(Equal(uint8(i)))
		}
		Expect(sess.CloseWithError(0, "")).To(Succeed())

		numIncoming := atomic.LoadUint32(&incoming)
		numOutgoing := atomic.LoadUint32(&outgoing)
		fmt.Fprintf(GinkgoWriter, "incoming packets: %d\n", numIncoming)
		fmt.Fprintf(GinkgoWriter, "outgoing packets: %d\n", numOutgoing)
		Expect(numIncoming).To(And(
			BeNumerically(">", numMsg),
			BeNumerically("<", numMsg+10),
		))
		Expect(numOutgoing).To(And(
			BeNumerically(">", numMsg),
			BeNumerically("<", numMsg+10),
		))
	})
})
