package self_test

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

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

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

func randomDuration(min, max time.Duration) time.Duration {
	return min + time.Duration(rand.Int63n(int64(max-min)))
}

var _ = Describe("Drop Tests", func() {
	var (
		proxy *quicproxy.QuicProxy
		ln    quic.Listener
	)

	startListenerAndProxy := func(dropCallback quicproxy.DropCallback, version protocol.VersionNumber) {
		var err error
		ln, err = quic.ListenAddr(
			"localhost:0",
			getTLSConfig(),
			getQuicConfig(&quic.Config{Versions: []protocol.VersionNumber{version}}),
		)
		Expect(err).ToNot(HaveOccurred())
		serverPort := ln.Addr().(*net.UDPAddr).Port
		proxy, err = quicproxy.NewQuicProxy("localhost:0", &quicproxy.Opts{
			RemoteAddr: fmt.Sprintf("localhost:%d", serverPort),
			DelayPacket: func(dir quicproxy.Direction, _ []byte) time.Duration {
				return 5 * time.Millisecond // 10ms RTT
			},
			DropPacket: dropCallback,
		},
		)
		Expect(err).ToNot(HaveOccurred())
	}

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

	for _, v := range protocol.SupportedVersions {
		version := v

		Context(fmt.Sprintf("with QUIC version %s", version), func() {
			for _, d := range directions {
				direction := d

				// The purpose of this test is to create a lot of tails, by sending 1 byte messages.
				// The interval, the length of the drop period, and the time when the drop period starts are randomized.
				// To cover different scenarios, repeat this test a few times.
				for rep := 0; rep < 3; rep++ {
					It(fmt.Sprintf("sends short messages, dropping packets in %s direction", direction), func() {
						const numMessages = 15

						messageInterval := randomDuration(10*time.Millisecond, 100*time.Millisecond)
						dropDuration := randomDuration(messageInterval*3/2, 2*time.Second)
						dropDelay := randomDuration(25*time.Millisecond, numMessages*messageInterval/2) // makes sure we don't interfere with the handshake
						fmt.Fprintf(GinkgoWriter, "Sending a message every %s, %d times.\n", messageInterval, numMessages)
						fmt.Fprintf(GinkgoWriter, "Dropping packets for %s, after a delay of %s\n", dropDuration, dropDelay)
						startTime := time.Now()

						var numDroppedPackets int32
						startListenerAndProxy(func(d quicproxy.Direction, _ []byte) bool {
							if !d.Is(direction) {
								return false
							}
							drop := time.Now().After(startTime.Add(dropDelay)) && time.Now().Before(startTime.Add(dropDelay).Add(dropDuration))
							if drop {
								atomic.AddInt32(&numDroppedPackets, 1)
							}
							return drop
						}, version)

						done := make(chan struct{})
						go func() {
							defer GinkgoRecover()
							conn, err := ln.Accept(context.Background())
							Expect(err).ToNot(HaveOccurred())
							str, err := conn.OpenStream()
							Expect(err).ToNot(HaveOccurred())
							for i := uint8(1); i <= numMessages; i++ {
								n, err := str.Write([]byte{i})
								Expect(err).ToNot(HaveOccurred())
								Expect(n).To(Equal(1))
								time.Sleep(messageInterval)
							}
							<-done
							Expect(conn.CloseWithError(0, "")).To(Succeed())
						}()

						conn, err := quic.DialAddr(
							fmt.Sprintf("localhost:%d", proxy.LocalPort()),
							getTLSClientConfig(),
							getQuicConfig(&quic.Config{Versions: []protocol.VersionNumber{version}}),
						)
						Expect(err).ToNot(HaveOccurred())
						defer conn.CloseWithError(0, "")
						str, err := conn.AcceptStream(context.Background())
						Expect(err).ToNot(HaveOccurred())
						for i := uint8(1); i <= numMessages; i++ {
							b := []byte{0}
							n, err := str.Read(b)
							Expect(err).ToNot(HaveOccurred())
							Expect(n).To(Equal(1))
							Expect(b[0]).To(Equal(i))
						}
						close(done)
						numDropped := atomic.LoadInt32(&numDroppedPackets)
						fmt.Fprintf(GinkgoWriter, "Dropped %d packets.\n", numDropped)
						Expect(numDropped).To(BeNumerically(">", 0))
					})
				}
			}
		})
	}
})
