package gortsplib

import (
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	"net"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/bluenviron/gortsplib/v5/internal/base64streamreader"
	"github.com/bluenviron/gortsplib/v5/pkg/auth"
	"github.com/bluenviron/gortsplib/v5/pkg/base"
	"github.com/bluenviron/gortsplib/v5/pkg/conn"
	"github.com/bluenviron/gortsplib/v5/pkg/description"
)

func mustParseURL(s string) *base.URL {
	u, err := base.ParseURL(s)
	if err != nil {
		panic(err)
	}
	return u
}

func TestClientURLToAddress(t *testing.T) {
	for _, ca := range []struct {
		name string
		url  string
		addr string
	}{
		{
			"rtsp ipv6 with port",
			"rtsp://[::1]:8888/path",
			"[::1]:8888",
		},
		{
			"rtsp ipv6 without port",
			"rtsp://[::1]/path",
			"[::1]:554",
		},
		{
			"rtsps without port",
			"rtsps://2.2.2.2/path",
			"2.2.2.2:322",
		},
	} {
		t.Run(ca.name, func(t *testing.T) {
			addr := canonicalAddr(mustParseURL(ca.url))
			require.Equal(t, ca.addr, addr)
		})
	}
}

func TestClientClose(t *testing.T) {
	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
	require.NoError(t, err)

	c := Client{
		Scheme: u.Scheme,
		Host:   u.Host,
	}

	err = c.Start()
	require.NoError(t, err)

	c.Close()

	_, err = c.Options(u)
	require.EqualError(t, err, "terminated")

	_, _, err = c.Describe(u)
	require.EqualError(t, err, "terminated")

	_, err = c.Announce(u, nil)
	require.EqualError(t, err, "terminated")

	_, err = c.Setup(nil, nil, 0, 0)
	require.EqualError(t, err, "terminated")

	_, err = c.Play(nil)
	require.EqualError(t, err, "terminated")

	_, err = c.Record()
	require.EqualError(t, err, "terminated")

	_, err = c.Pause()
	require.EqualError(t, err, "terminated")
}

func TestClientCloseDuringRequest(t *testing.T) {
	l, err := net.Listen("tcp", "localhost:8554")
	require.NoError(t, err)
	defer l.Close()

	requestReceived := make(chan struct{})
	releaseConn := make(chan struct{})

	serverDone := make(chan struct{})
	defer func() { <-serverDone }()

	go func() {
		defer close(serverDone)

		nconn, err2 := l.Accept()
		require.NoError(t, err2)
		defer nconn.Close()
		conn := conn.NewConn(bufio.NewReader(nconn), nconn)

		req, err2 := conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Options, req.Method)

		close(requestReceived)
		<-releaseConn
	}()

	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
	require.NoError(t, err)

	c := Client{
		Scheme: u.Scheme,
		Host:   u.Host,
	}

	err = c.Start()
	require.NoError(t, err)

	optionsDone := make(chan struct{})
	go func() {
		defer close(optionsDone)
		_, err2 := c.Options(u)
		require.Error(t, err2)
	}()

	<-requestReceived
	c.Close()
	<-optionsDone
	close(releaseConn)
}

func TestClientSession(t *testing.T) {
	l, err := net.Listen("tcp", "localhost:8554")
	require.NoError(t, err)
	defer l.Close()

	serverDone := make(chan struct{})
	defer func() { <-serverDone }()

	go func() {
		defer close(serverDone)

		nconn, err2 := l.Accept()
		require.NoError(t, err2)
		conn := conn.NewConn(bufio.NewReader(nconn), nconn)
		defer nconn.Close()

		req, err2 := conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Options, req.Method)

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Public": base.HeaderValue{strings.Join([]string{
					string(base.Describe),
				}, ", ")},
				"Session": base.HeaderValue{"123456"},
			},
		})
		require.NoError(t, err2)

		req, err2 = conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Describe, req.Method)
		require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"])

		medias := []*description.Media{testH264Media}

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Content-Type": base.HeaderValue{"application/sdp"},
				"Session":      base.HeaderValue{"123456"},
			},
			Body: mediasToSDP(medias),
		})
		require.NoError(t, err2)
	}()

	u, err := base.ParseURL("rtsp://localhost:8554/stream")
	require.NoError(t, err)

	c := Client{
		Scheme: u.Scheme,
		Host:   u.Host,
	}

	err = c.Start()
	require.NoError(t, err)
	defer c.Close()

	_, _, err = c.Describe(u)
	require.NoError(t, err)
}

func TestClientAuth(t *testing.T) {
	l, err := net.Listen("tcp", "localhost:8554")
	require.NoError(t, err)
	defer l.Close()

	serverDone := make(chan struct{})
	defer func() { <-serverDone }()

	go func() {
		defer close(serverDone)

		nconn, err2 := l.Accept()
		require.NoError(t, err2)
		conn := conn.NewConn(bufio.NewReader(nconn), nconn)
		defer nconn.Close()

		req, err2 := conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Options, req.Method)

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Public": base.HeaderValue{strings.Join([]string{
					string(base.Describe),
				}, ", ")},
			},
		})
		require.NoError(t, err2)

		req, err2 = conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Describe, req.Method)

		nonce, err2 := auth.GenerateNonce()
		require.NoError(t, err2)

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusUnauthorized,
			Header: base.Header{
				"WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
			},
		})
		require.NoError(t, err2)

		req, err2 = conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Describe, req.Method)

		err2 = auth.Verify(req, "myuser", "mypass", nil, "IPCAM", nonce)
		require.NoError(t, err2)

		medias := []*description.Media{testH264Media}

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Content-Type": base.HeaderValue{"application/sdp"},
			},
			Body: mediasToSDP(medias),
		})
		require.NoError(t, err2)
	}()

	u, err := base.ParseURL("rtsp://myuser:mypass@localhost:8554/stream")
	require.NoError(t, err)

	c := Client{
		Scheme: u.Scheme,
		Host:   u.Host,
	}

	err = c.Start()
	require.NoError(t, err)
	defer c.Close()

	_, _, err = c.Describe(u)
	require.NoError(t, err)
}

func TestClientCSeq(t *testing.T) {
	for _, ca := range []string{
		"different cseq",
		"space at the end",
	} {
		t.Run(ca, func(t *testing.T) {
			l, err := net.Listen("tcp", "localhost:8554")
			require.NoError(t, err)
			defer l.Close()

			serverDone := make(chan struct{})
			defer func() { <-serverDone }()

			go func() {
				defer close(serverDone)

				nconn, err2 := l.Accept()
				require.NoError(t, err2)
				defer nconn.Close()
				conn := conn.NewConn(bufio.NewReader(nconn), nconn)

				req, err2 := conn.ReadRequest()
				require.NoError(t, err2)
				require.Equal(t, base.Options, req.Method)

				switch ca {
				case "different cseq":
					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Public": base.HeaderValue{strings.Join([]string{
								string(base.Describe),
							}, ", ")},
							"CSeq": base.HeaderValue{"150"},
						},
					})
					require.NoError(t, err2)

					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Public": base.HeaderValue{strings.Join([]string{
								string(base.Describe),
							}, ", ")},
							"CSeq": req.Header["CSeq"],
						},
					})
					require.NoError(t, err2)

				case "space at the end":
					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Public": base.HeaderValue{strings.Join([]string{
								string(base.Describe),
							}, ", ")},
							"CSeq": base.HeaderValue{req.Header["CSeq"][0] + " "},
						},
					})
					require.NoError(t, err2)
				}
			}()

			u, err := base.ParseURL("rtsp://localhost:8554/teststream")
			require.NoError(t, err)

			c := Client{
				Scheme: u.Scheme,
				Host:   u.Host,
			}

			err = c.Start()
			require.NoError(t, err)
			defer c.Close()

			_, err = c.Options(u)
			require.NoError(t, err)
		})
	}
}

func TestClientDescribeCharset(t *testing.T) {
	l, err := net.Listen("tcp", "localhost:8554")
	require.NoError(t, err)
	defer l.Close()

	serverDone := make(chan struct{})
	defer func() { <-serverDone }()

	go func() {
		defer close(serverDone)

		nconn, err2 := l.Accept()
		require.NoError(t, err2)
		defer nconn.Close()
		conn := conn.NewConn(bufio.NewReader(nconn), nconn)

		req, err2 := conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Options, req.Method)

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Public": base.HeaderValue{strings.Join([]string{
					string(base.Describe),
				}, ", ")},
			},
		})
		require.NoError(t, err2)

		req, err2 = conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Describe, req.Method)
		require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)

		medias := []*description.Media{testH264Media}

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
			},
			Body: mediasToSDP(medias),
		})
		require.NoError(t, err2)
	}()

	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
	require.NoError(t, err)

	c := Client{
		Scheme: u.Scheme,
		Host:   u.Host,
	}

	err = c.Start()
	require.NoError(t, err)
	defer c.Close()

	_, _, err = c.Describe(u)
	require.NoError(t, err)
}

func TestClientReplyToServerRequest(t *testing.T) {
	for _, ca := range []string{"after response", "before response"} {
		t.Run(ca, func(t *testing.T) {
			l, err := net.Listen("tcp", "localhost:8554")
			require.NoError(t, err)
			defer l.Close()

			serverDone := make(chan struct{})

			go func() {
				defer close(serverDone)

				nconn, err2 := l.Accept()
				require.NoError(t, err2)
				conn := conn.NewConn(bufio.NewReader(nconn), nconn)
				defer nconn.Close()

				req, err2 := conn.ReadRequest()
				require.NoError(t, err2)
				require.Equal(t, base.Options, req.Method)

				if ca == "after response" {
					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Public": base.HeaderValue{strings.Join([]string{
								string(base.Describe),
							}, ", ")},
						},
					})
					require.NoError(t, err2)

					err2 = conn.WriteRequest(&base.Request{
						Method: base.Options,
						URL:    nil,
						Header: base.Header{
							"CSeq": base.HeaderValue{"4"},
						},
					})
					require.NoError(t, err2)

					var res *base.Response
					res, err2 = conn.ReadResponse()
					require.NoError(t, err2)
					require.Equal(t, base.StatusOK, res.StatusCode)
					require.Equal(t, "4", res.Header["CSeq"][0])
				} else {
					err2 = conn.WriteRequest(&base.Request{
						Method: base.Options,
						URL:    nil,
						Header: base.Header{
							"CSeq": base.HeaderValue{"4"},
						},
					})
					require.NoError(t, err2)

					var res *base.Response
					res, err2 = conn.ReadResponse()
					require.NoError(t, err2)
					require.Equal(t, base.StatusOK, res.StatusCode)
					require.Equal(t, "4", res.Header["CSeq"][0])

					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Public": base.HeaderValue{strings.Join([]string{
								string(base.Describe),
							}, ", ")},
						},
					})
					require.NoError(t, err2)
				}
			}()

			u, err := base.ParseURL("rtsp://localhost:8554/stream")
			require.NoError(t, err)

			c := Client{
				Scheme: u.Scheme,
				Host:   u.Host,
			}

			err = c.Start()
			require.NoError(t, err)
			defer c.Close()

			_, err = c.Options(u)
			require.NoError(t, err)

			<-serverDone
		})
	}
}

func TestClientRelativeContentBase(t *testing.T) {
	l, err := net.Listen("tcp", "localhost:8554")
	require.NoError(t, err)
	defer l.Close()

	serverDone := make(chan struct{})
	defer func() { <-serverDone }()

	go func() {
		defer close(serverDone)

		nconn, err2 := l.Accept()
		require.NoError(t, err2)
		defer nconn.Close()
		conn := conn.NewConn(bufio.NewReader(nconn), nconn)

		req, err2 := conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Options, req.Method)

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Public": base.HeaderValue{strings.Join([]string{
					string(base.Describe),
				}, ", ")},
			},
		})
		require.NoError(t, err2)

		req, err2 = conn.ReadRequest()
		require.NoError(t, err2)
		require.Equal(t, base.Describe, req.Method)
		require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)

		medias := []*description.Media{testH264Media}

		err2 = conn.WriteResponse(&base.Response{
			StatusCode: base.StatusOK,
			Header: base.Header{
				"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
				"Content-Base": base.HeaderValue{"/relative-content-base"},
			},
			Body: mediasToSDP(medias),
		})
		require.NoError(t, err2)
	}()

	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
	require.NoError(t, err)

	c := Client{
		Scheme: u.Scheme,
		Host:   u.Host,
	}

	err = c.Start()
	require.NoError(t, err)
	defer c.Close()

	desc, _, err := c.Describe(u)
	require.NoError(t, err)

	require.Equal(t, "rtsp://localhost:8554/relative-content-base", desc.BaseURL.String())
}

func TestClientTunnelHTTP(t *testing.T) {
	for _, ca := range []string{"http", "https"} {
		t.Run(ca, func(t *testing.T) {
			var l net.Listener
			var err error

			if ca == "http" {
				l, err = net.Listen("tcp", "localhost:8554")
				require.NoError(t, err)
				defer l.Close()
			} else {
				var cert tls.Certificate
				cert, err = tls.X509KeyPair(serverCert, serverKey)
				require.NoError(t, err)

				l, err = tls.Listen("tcp", "localhost:8554", &tls.Config{Certificates: []tls.Certificate{cert}})
				require.NoError(t, err)
				defer l.Close()
			}

			var scheme string
			if ca == "http" {
				scheme = "rtsp"
			} else {
				scheme = "rtsps"
			}

			serverDone := make(chan struct{})
			defer func() { <-serverDone }()

			go func() {
				defer close(serverDone)

				nconn1, err2 := l.Accept()
				require.NoError(t, err2)
				defer nconn1.Close()

				buf1 := bufio.NewReader(nconn1)
				req1, err2 := http.ReadRequest(buf1)
				require.NoError(t, err2)

				require.Equal(t, &http.Request{
					Method:     http.MethodGet,
					Proto:      "HTTP/1.1",
					ProtoMajor: 1,
					ProtoMinor: 1,
					URL: &url.URL{
						Path: "/",
					},
					Host:       "localhost:8554",
					RequestURI: "/",
					Header: http.Header{
						"Accept":          []string{"application/x-rtsp-tunnelled"},
						"Content-Length":  []string{"30000"},
						"X-Sessioncookie": req1.Header["X-Sessioncookie"],
					},
					ContentLength: 30000,
					Body:          req1.Body,
				}, req1)

				require.NotEmpty(t, req1.Header.Get("X-Sessioncookie"))

				h := http.Header{}
				h.Set("Cache-Control", "no-cache")
				h.Set("Connection", "close")
				h.Set("Content-Type", "application/x-rtsp-tunnelled")
				h.Set("Pragma", "no-cache")
				res := http.Response{
					StatusCode:    http.StatusOK,
					ProtoMajor:    1,
					ProtoMinor:    req1.ProtoMinor,
					Header:        h,
					ContentLength: -1,
				}
				var resBuf bytes.Buffer
				res.Write(&resBuf) //nolint:errcheck
				_, err = nconn1.Write(resBuf.Bytes())
				require.NoError(t, err)

				nconn2, err2 := l.Accept()
				require.NoError(t, err2)
				defer nconn2.Close()

				buf2 := bufio.NewReader(nconn2)
				req2, err2 := http.ReadRequest(buf2)
				require.NoError(t, err2)

				require.Equal(t, &http.Request{
					Method:     http.MethodPost,
					Proto:      "HTTP/1.1",
					ProtoMajor: 1,
					ProtoMinor: 1,
					URL: &url.URL{
						Path: "/",
					},
					Host:       "localhost:8554",
					RequestURI: "/",
					Header: http.Header{
						"Content-Type":    []string{"application/x-rtsp-tunnelled"},
						"Content-Length":  []string{"30000"},
						"X-Sessioncookie": req2.Header["X-Sessioncookie"],
					},
					ContentLength: 30000,
					Body:          req2.Body,
				}, req2)

				require.Equal(t, req1.Header.Get("X-Sessioncookie"), req2.Header.Get("X-Sessioncookie"))

				h = http.Header{}
				h.Set("Cache-Control", "no-cache")
				h.Set("Connection", "close")
				h.Set("Content-Type", "application/x-rtsp-tunnelled")
				h.Set("Pragma", "no-cache")
				res = http.Response{
					StatusCode:    http.StatusOK,
					ProtoMajor:    1,
					ProtoMinor:    req1.ProtoMinor,
					Header:        h,
					ContentLength: -1,
				}
				resBuf = bytes.Buffer{}
				res.Write(&resBuf) //nolint:errcheck
				_, err = nconn2.Write(resBuf.Bytes())
				require.NoError(t, err)

				conn := conn.NewConn(bufio.NewReader(base64streamreader.New(buf2)), nconn1)

				req, err2 := conn.ReadRequest()
				require.NoError(t, err2)
				require.Equal(t, base.Options, req.Method)

				err2 = conn.WriteResponse(&base.Response{
					StatusCode: base.StatusOK,
					Header: base.Header{
						"Public": base.HeaderValue{strings.Join([]string{
							string(base.Describe),
						}, ", ")},
					},
				})
				require.NoError(t, err2)

				req, err2 = conn.ReadRequest()
				require.NoError(t, err2)
				require.Equal(t, base.Describe, req.Method)
				require.Equal(t, mustParseURL(scheme+"://localhost:8554/teststream"), req.URL)

				medias := []*description.Media{testH264Media}

				err2 = conn.WriteResponse(&base.Response{
					StatusCode: base.StatusOK,
					Header: base.Header{
						"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
						"Content-Base": base.HeaderValue{"/relative-content-base"},
					},
					Body: mediasToSDP(medias),
				})
				require.NoError(t, err2)
			}()

			u, err := base.ParseURL(scheme + "://localhost:8554/teststream")
			require.NoError(t, err)

			c := Client{
				Scheme: u.Scheme,
				Host:   u.Host,
				Tunnel: TunnelHTTP,
				TLSConfig: &tls.Config{
					InsecureSkipVerify: true,
				},
			}

			err = c.Start()
			require.NoError(t, err)
			defer c.Close()

			_, res, err := c.Describe(u)
			require.NoError(t, err)
			require.Equal(t, base.StatusOK, res.StatusCode)
		})
	}
}

func TestClientTunnelWebSocket(t *testing.T) {
	for _, ca := range []string{"ws", "wss"} {
		t.Run(ca, func(t *testing.T) {
			var scheme string
			if ca == "ws" {
				scheme = "rtsp"
			} else {
				scheme = "rtsps"
			}

			s := &http.Server{
				Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					require.Equal(t, r.Header.Get("Sec-WebSocket-Protocol"), "rtsp.onvif.org")

					wconn, err := upgrader.Upgrade(w, r, nil)
					require.NoError(t, err)
					defer wconn.Close() //nolint:errcheck

					conn := conn.NewConn(bufio.NewReader(&wsReader{wc: wconn}), &wsWriter{wc: wconn})

					req, err2 := conn.ReadRequest()
					require.NoError(t, err2)
					require.Equal(t, base.Options, req.Method)

					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Public": base.HeaderValue{strings.Join([]string{
								string(base.Describe),
							}, ", ")},
						},
					})
					require.NoError(t, err2)

					req, err2 = conn.ReadRequest()
					require.NoError(t, err2)
					require.Equal(t, base.Describe, req.Method)
					require.Equal(t, mustParseURL(scheme+"://localhost:8554/teststream"), req.URL)

					medias := []*description.Media{testH264Media}

					err2 = conn.WriteResponse(&base.Response{
						StatusCode: base.StatusOK,
						Header: base.Header{
							"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
							"Content-Base": base.HeaderValue{"/relative-content-base"},
						},
						Body: mediasToSDP(medias),
					})
					require.NoError(t, err2)
				}),
			}

			var ln net.Listener

			if ca == "ws" {
				var err error
				ln, err = net.Listen("tcp", "localhost:8554")
				require.NoError(t, err)
			} else {
				cert, err := tls.X509KeyPair(serverCert, serverKey)
				require.NoError(t, err)

				ln, err = tls.Listen("tcp", "localhost:8554", &tls.Config{Certificates: []tls.Certificate{cert}})
				require.NoError(t, err)
				defer ln.Close()
			}

			go s.Serve(ln)
			defer s.Shutdown(context.Background())

			u, err := base.ParseURL(scheme + "://localhost:8554/teststream")
			require.NoError(t, err)

			c := Client{
				Scheme: u.Scheme,
				Host:   u.Host,
				Tunnel: TunnelWebSocket,
				TLSConfig: &tls.Config{
					InsecureSkipVerify: true,
				},
			}

			err = c.Start()
			require.NoError(t, err)
			defer c.Close()

			_, res, err := c.Describe(u)
			require.NoError(t, err)
			require.Equal(t, base.StatusOK, res.StatusCode)
		})
	}
}
