package self_test

import (
	"bufio"
	"bytes"
	"compress/gzip"
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"strconv"
	"time"

	"github.com/lucas-clemente/quic-go"
	"github.com/lucas-clemente/quic-go/http3"
	"github.com/lucas-clemente/quic-go/internal/protocol"
	"github.com/lucas-clemente/quic-go/internal/testdata"

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

var _ = Describe("HTTP tests", func() {
	var (
		mux            *http.ServeMux
		client         *http.Client
		server         *http3.Server
		stoppedServing chan struct{}
		port           string
	)

	versions := protocol.SupportedVersions

	BeforeEach(func() {
		mux = http.NewServeMux()
		mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
			defer GinkgoRecover()
			io.WriteString(w, "Hello, World!\n") // don't check the error here. Stream may be reset.
		})

		mux.HandleFunc("/prdata", func(w http.ResponseWriter, r *http.Request) {
			defer GinkgoRecover()
			sl := r.URL.Query().Get("len")
			if sl != "" {
				var err error
				l, err := strconv.Atoi(sl)
				Expect(err).NotTo(HaveOccurred())
				w.Write(GeneratePRData(l)) // don't check the error here. Stream may be reset.
			} else {
				w.Write(PRData) // don't check the error here. Stream may be reset.
			}
		})

		mux.HandleFunc("/prdatalong", func(w http.ResponseWriter, r *http.Request) {
			defer GinkgoRecover()
			w.Write(PRDataLong) // don't check the error here. Stream may be reset.
		})

		mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
			defer GinkgoRecover()
			body, err := io.ReadAll(r.Body)
			Expect(err).NotTo(HaveOccurred())
			w.Write(body) // don't check the error here. Stream may be reset.
		})

		server = &http3.Server{
			Handler:    mux,
			TLSConfig:  testdata.GetTLSConfig(),
			QuicConfig: getQuicConfig(&quic.Config{Versions: versions}),
		}

		addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0")
		Expect(err).NotTo(HaveOccurred())
		conn, err := net.ListenUDP("udp", addr)
		Expect(err).NotTo(HaveOccurred())
		port = strconv.Itoa(conn.LocalAddr().(*net.UDPAddr).Port)

		stoppedServing = make(chan struct{})

		go func() {
			defer GinkgoRecover()
			server.Serve(conn)
			close(stoppedServing)
		}()
	})

	AfterEach(func() {
		Expect(server.Close()).NotTo(HaveOccurred())
		Eventually(stoppedServing).Should(BeClosed())
	})

	for _, v := range versions {
		version := v

		Context(fmt.Sprintf("with QUIC version %s", version), func() {
			BeforeEach(func() {
				client = &http.Client{
					Transport: &http3.RoundTripper{
						TLSClientConfig: &tls.Config{
							RootCAs: testdata.GetRootCA(),
						},
						DisableCompression: true,
						QuicConfig: getQuicConfig(&quic.Config{
							Versions:       []protocol.VersionNumber{version},
							MaxIdleTimeout: 10 * time.Second,
						}),
					},
				}
			})

			It("downloads a hello", func() {
				resp, err := client.Get("https://localhost:" + port + "/hello")
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
				Expect(err).ToNot(HaveOccurred())
				Expect(string(body)).To(Equal("Hello, World!\n"))
			})

			It("sets and gets request headers", func() {
				handlerCalled := make(chan struct{})
				mux.HandleFunc("/headers/request", func(w http.ResponseWriter, r *http.Request) {
					defer GinkgoRecover()
					Expect(r.Header.Get("foo")).To(Equal("bar"))
					Expect(r.Header.Get("lorem")).To(Equal("ipsum"))
					close(handlerCalled)
				})

				req, err := http.NewRequest(http.MethodGet, "https://localhost:"+port+"/headers/request", nil)
				Expect(err).ToNot(HaveOccurred())
				req.Header.Set("foo", "bar")
				req.Header.Set("lorem", "ipsum")
				resp, err := client.Do(req)
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				Eventually(handlerCalled).Should(BeClosed())
			})

			It("sets and gets response headers", func() {
				mux.HandleFunc("/headers/response", func(w http.ResponseWriter, r *http.Request) {
					defer GinkgoRecover()
					w.Header().Set("foo", "bar")
					w.Header().Set("lorem", "ipsum")
				})

				resp, err := client.Get("https://localhost:" + port + "/headers/response")
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				Expect(resp.Header.Get("foo")).To(Equal("bar"))
				Expect(resp.Header.Get("lorem")).To(Equal("ipsum"))
			})

			It("downloads a small file", func() {
				resp, err := client.Get("https://localhost:" + port + "/prdata")
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second))
				Expect(err).ToNot(HaveOccurred())
				Expect(body).To(Equal(PRData))
			})

			It("downloads a large file", func() {
				resp, err := client.Get("https://localhost:" + port + "/prdatalong")
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 20*time.Second))
				Expect(err).ToNot(HaveOccurred())
				Expect(body).To(Equal(PRDataLong))
			})

			It("downloads many hellos", func() {
				const num = 150

				for i := 0; i < num; i++ {
					resp, err := client.Get("https://localhost:" + port + "/hello")
					Expect(err).ToNot(HaveOccurred())
					Expect(resp.StatusCode).To(Equal(200))
					body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
					Expect(err).ToNot(HaveOccurred())
					Expect(string(body)).To(Equal("Hello, World!\n"))
				}
			})

			It("downloads many files, if the response is not read", func() {
				const num = 150

				for i := 0; i < num; i++ {
					resp, err := client.Get("https://localhost:" + port + "/prdata")
					Expect(err).ToNot(HaveOccurred())
					Expect(resp.StatusCode).To(Equal(200))
					Expect(resp.Body.Close()).To(Succeed())
				}
			})

			It("posts a small message", func() {
				resp, err := client.Post(
					"https://localhost:"+port+"/echo",
					"text/plain",
					bytes.NewReader([]byte("Hello, world!")),
				)
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second))
				Expect(err).ToNot(HaveOccurred())
				Expect(body).To(Equal([]byte("Hello, world!")))
			})

			It("uploads a file", func() {
				resp, err := client.Post(
					"https://localhost:"+port+"/echo",
					"text/plain",
					bytes.NewReader(PRData),
				)
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second))
				Expect(err).ToNot(HaveOccurred())
				Expect(body).To(Equal(PRData))
			})

			It("uses gzip compression", func() {
				mux.HandleFunc("/gzipped/hello", func(w http.ResponseWriter, r *http.Request) {
					defer GinkgoRecover()
					Expect(r.Header.Get("Accept-Encoding")).To(Equal("gzip"))
					w.Header().Set("Content-Encoding", "gzip")
					w.Header().Set("foo", "bar")

					gw := gzip.NewWriter(w)
					defer gw.Close()
					gw.Write([]byte("Hello, World!\n"))
				})

				client.Transport.(*http3.RoundTripper).DisableCompression = false
				resp, err := client.Get("https://localhost:" + port + "/gzipped/hello")
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				Expect(resp.Uncompressed).To(BeTrue())

				body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second))
				Expect(err).ToNot(HaveOccurred())
				Expect(string(body)).To(Equal("Hello, World!\n"))
			})

			It("cancels requests", func() {
				handlerCalled := make(chan struct{})
				mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
					defer GinkgoRecover()
					defer close(handlerCalled)
					for {
						if _, err := w.Write([]byte("foobar")); err != nil {
							Expect(r.Context().Done()).To(BeClosed())
							var strErr *quic.StreamError
							Expect(errors.As(err, &strErr)).To(BeTrue())
							Expect(strErr.ErrorCode).To(Equal(quic.StreamErrorCode(0x10c)))
							return
						}
					}
				})

				req, err := http.NewRequest(http.MethodGet, "https://localhost:"+port+"/cancel", nil)
				Expect(err).ToNot(HaveOccurred())
				ctx, cancel := context.WithCancel(context.Background())
				req = req.WithContext(ctx)
				resp, err := client.Do(req)
				Expect(err).ToNot(HaveOccurred())
				Expect(resp.StatusCode).To(Equal(200))
				cancel()
				Eventually(handlerCalled).Should(BeClosed())
				_, err = resp.Body.Read([]byte{0})
				Expect(err).To(HaveOccurred())
			})

			It("allows streamed HTTP requests", func() {
				done := make(chan struct{})
				mux.HandleFunc("/echoline", func(w http.ResponseWriter, r *http.Request) {
					defer GinkgoRecover()
					defer close(done)
					w.WriteHeader(200)
					w.(http.Flusher).Flush()
					reader := bufio.NewReader(r.Body)
					for {
						msg, err := reader.ReadString('\n')
						if err != nil {
							return
						}
						_, err = w.Write([]byte(msg))
						Expect(err).ToNot(HaveOccurred())
						w.(http.Flusher).Flush()
					}
				})

				r, w := io.Pipe()
				req, err := http.NewRequest("PUT", "https://localhost:"+port+"/echoline", r)
				Expect(err).ToNot(HaveOccurred())
				rsp, err := client.Do(req)
				Expect(err).ToNot(HaveOccurred())
				Expect(rsp.StatusCode).To(Equal(200))

				reader := bufio.NewReader(rsp.Body)
				for i := 0; i < 5; i++ {
					msg := fmt.Sprintf("Hello world, %d!\n", i)
					fmt.Fprint(w, msg)
					msgRcvd, err := reader.ReadString('\n')
					Expect(err).ToNot(HaveOccurred())
					Expect(msgRcvd).To(Equal(msg))
				}
				Expect(req.Body.Close()).To(Succeed())
				Eventually(done).Should(BeClosed())
			})

			It("allows taking over the stream", func() {
				mux.HandleFunc("/httpstreamer", func(w http.ResponseWriter, r *http.Request) {
					defer GinkgoRecover()
					w.WriteHeader(200)
					w.(http.Flusher).Flush()

					str := r.Body.(http3.HTTPStreamer).HTTPStream()
					str.Write([]byte("foobar"))

					// Do this in a Go routine, so that the handler returns early.
					// This way, we can also check that the HTTP/3 doesn't close the stream.
					go func() {
						defer GinkgoRecover()
						_, err := io.Copy(str, str)
						Expect(err).ToNot(HaveOccurred())
						Expect(str.Close()).To(Succeed())
					}()
				})

				req, err := http.NewRequest(http.MethodGet, "https://localhost:"+port+"/httpstreamer", nil)
				Expect(err).ToNot(HaveOccurred())
				rsp, err := client.Transport.(*http3.RoundTripper).RoundTripOpt(req, http3.RoundTripOpt{DontCloseRequestStream: true})
				Expect(err).ToNot(HaveOccurred())
				Expect(rsp.StatusCode).To(Equal(200))

				str := rsp.Body.(http3.HTTPStreamer).HTTPStream()
				b := make([]byte, 6)
				_, err = io.ReadFull(str, b)
				Expect(err).ToNot(HaveOccurred())
				Expect(b).To(Equal([]byte("foobar")))

				data := GeneratePRData(8 * 1024)
				_, err = str.Write(data)
				Expect(err).ToNot(HaveOccurred())
				Expect(str.Close()).To(Succeed())
				repl, err := io.ReadAll(str)
				Expect(err).ToNot(HaveOccurred())
				Expect(repl).To(Equal(data))
			})

			if version != protocol.VersionDraft29 {
				It("serves other QUIC connections", func() {
					tlsConf := testdata.GetTLSConfig()
					tlsConf.NextProtos = []string{"h3"}
					ln, err := quic.ListenAddr("localhost:0", tlsConf, nil)
					Expect(err).ToNot(HaveOccurred())
					done := make(chan struct{})
					go func() {
						defer GinkgoRecover()
						defer close(done)
						conn, err := ln.Accept(context.Background())
						Expect(err).ToNot(HaveOccurred())
						Expect(server.ServeQUICConn(conn)).To(Succeed())
					}()

					resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", ln.Addr().(*net.UDPAddr).Port))
					Expect(err).ToNot(HaveOccurred())
					Expect(resp.StatusCode).To(Equal(http.StatusOK))
					client.Transport.(io.Closer).Close()
					Eventually(done).Should(BeClosed())
				})
			}
		})
	}
})
