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

package httputil

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"net/url"
	"runtime"
	"runtime/pprof"
	"strings"
	"testing"
	"time"
)

type eofReader struct{}

func (n eofReader) Close() error { return nil }

func (n eofReader) Read([]byte) (int, error) { return 0, io.EOF }

type dumpTest struct {
	// Either Req or GetReq can be set/nil but not both.
	Req    *http.Request
	GetReq func() *http.Request

	Body any // optional []byte or func() io.ReadCloser to populate Req.Body

	WantDump    string
	WantDumpOut string
	MustError   bool // if true, the test is expected to throw an error
	NoBody      bool // if true, set DumpRequest{,Out} body to false
}

var dumpTests = []dumpTest{
	// HTTP/1.1 => chunked coding; body; empty trailer
	{
		Req: &http.Request{
			Method: "GET",
			URL: &url.URL{
				Scheme: "http",
				Host:   "www.google.com",
				Path:   "/search",
			},
			ProtoMajor:       1,
			ProtoMinor:       1,
			TransferEncoding: []string{"chunked"},
		},

		Body: []byte("abcdef"),

		WantDump: "GET /search HTTP/1.1\r\n" +
			"Host: www.google.com\r\n" +
			"Transfer-Encoding: chunked\r\n\r\n" +
			chunk("abcdef") + chunk(""),
	},

	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
	// and doesn't add a User-Agent.
	{
		Req: &http.Request{
			Method:     "GET",
			URL:        mustParseURL("/foo"),
			ProtoMajor: 1,
			ProtoMinor: 0,
			Header: http.Header{
				"X-Foo": []string{"X-Bar"},
			},
		},

		WantDump: "GET /foo HTTP/1.0\r\n" +
			"X-Foo: X-Bar\r\n\r\n",
	},

	{
		Req: mustNewRequest("GET", "http://example.com/foo", nil),

		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
			"Host: example.com\r\n" +
			"User-Agent: Go-http-client/1.1\r\n" +
			"Accept-Encoding: gzip\r\n\r\n",
	},

	// Test that an https URL doesn't try to do an SSL negotiation
	// with a bytes.Buffer and hang with all goroutines not
	// runnable.
	{
		Req: mustNewRequest("GET", "https://example.com/foo", nil),
		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
			"Host: example.com\r\n" +
			"User-Agent: Go-http-client/1.1\r\n" +
			"Accept-Encoding: gzip\r\n\r\n",
	},

	// Request with Body, but Dump requested without it.
	{
		Req: &http.Request{
			Method: "POST",
			URL: &url.URL{
				Scheme: "http",
				Host:   "post.tld",
				Path:   "/",
			},
			ContentLength: 6,
			ProtoMajor:    1,
			ProtoMinor:    1,
		},

		Body: []byte("abcdef"),

		WantDumpOut: "POST / HTTP/1.1\r\n" +
			"Host: post.tld\r\n" +
			"User-Agent: Go-http-client/1.1\r\n" +
			"Content-Length: 6\r\n" +
			"Accept-Encoding: gzip\r\n\r\n",

		NoBody: true,
	},

	// Request with Body > 8196 (default buffer size)
	{
		Req: &http.Request{
			Method: "POST",
			URL: &url.URL{
				Scheme: "http",
				Host:   "post.tld",
				Path:   "/",
			},
			Header: http.Header{
				"Content-Length": []string{"8193"},
			},

			ContentLength: 8193,
			ProtoMajor:    1,
			ProtoMinor:    1,
		},

		Body: bytes.Repeat([]byte("a"), 8193),

		WantDumpOut: "POST / HTTP/1.1\r\n" +
			"Host: post.tld\r\n" +
			"User-Agent: Go-http-client/1.1\r\n" +
			"Content-Length: 8193\r\n" +
			"Accept-Encoding: gzip\r\n\r\n" +
			strings.Repeat("a", 8193),
		WantDump: "POST / HTTP/1.1\r\n" +
			"Host: post.tld\r\n" +
			"Content-Length: 8193\r\n\r\n" +
			strings.Repeat("a", 8193),
	},

	{
		GetReq: func() *http.Request {
			return mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
				"User-Agent: blah\r\n\r\n")
		},
		NoBody: true,
		WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
			"User-Agent: blah\r\n\r\n",
	},

	// Issue #7215. DumpRequest should return the "Content-Length" when set
	{
		GetReq: func() *http.Request {
			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
				"Host: passport.myhost.com\r\n" +
				"Content-Length: 3\r\n" +
				"\r\nkey1=name1&key2=name2")
		},
		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
			"Host: passport.myhost.com\r\n" +
			"Content-Length: 3\r\n" +
			"\r\nkey",
	},
	// Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
	{
		GetReq: func() *http.Request {
			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
				"Host: passport.myhost.com\r\n" +
				"Content-Length: 0\r\n" +
				"\r\nkey1=name1&key2=name2")
		},
		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
			"Host: passport.myhost.com\r\n" +
			"Content-Length: 0\r\n\r\n",
	},

	// Issue #7215. DumpRequest should not return the "Content-Length" if unset
	{
		GetReq: func() *http.Request {
			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
				"Host: passport.myhost.com\r\n" +
				"\r\nkey1=name1&key2=name2")
		},
		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
			"Host: passport.myhost.com\r\n\r\n",
	},

	// Issue 18506: make drainBody recognize NoBody. Otherwise
	// this was turning into a chunked request.
	{
		Req: mustNewRequest("POST", "http://example.com/foo", http.NoBody),
		WantDumpOut: "POST /foo HTTP/1.1\r\n" +
			"Host: example.com\r\n" +
			"User-Agent: Go-http-client/1.1\r\n" +
			"Content-Length: 0\r\n" +
			"Accept-Encoding: gzip\r\n\r\n",
	},

	// Issue 34504: a non-nil Body without ContentLength set should be chunked
	{
		Req: &http.Request{
			Method: "PUT",
			URL: &url.URL{
				Scheme: "http",
				Host:   "post.tld",
				Path:   "/test",
			},
			ContentLength: 0,
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			Body:          &eofReader{},
		},
		NoBody: true,
		WantDumpOut: "PUT /test HTTP/1.1\r\n" +
			"Host: post.tld\r\n" +
			"User-Agent: Go-http-client/1.1\r\n" +
			"Transfer-Encoding: chunked\r\n" +
			"Accept-Encoding: gzip\r\n\r\n",
	},

	// Issue 54616: request with Connection header doesn't result in duplicate header.
	{
		GetReq: func() *http.Request {
			return mustReadRequest("GET / HTTP/1.1\r\n" +
				"Host: example.com\r\n" +
				"Connection: close\r\n\r\n")
		},
		NoBody: true,
		WantDump: "GET / HTTP/1.1\r\n" +
			"Host: example.com\r\n" +
			"Connection: close\r\n\r\n",
	},
}

func TestDumpRequest(t *testing.T) {
	// Make a copy of dumpTests and add 10 new cases with an empty URL
	// to test that no goroutines are leaked. See golang.org/issue/32571.
	// 10 seems to be a decent number which always triggers the failure.
	dumpTests := dumpTests[:]
	for i := 0; i < 10; i++ {
		dumpTests = append(dumpTests, dumpTest{
			Req:       mustNewRequest("GET", "", nil),
			MustError: true,
		})
	}
	numg0 := runtime.NumGoroutine()
	for i, tt := range dumpTests {
		if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil {
			t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq)
			continue
		}

		freshReq := func(ti dumpTest) *http.Request {
			req := ti.Req
			if req == nil {
				req = ti.GetReq()
			}

			if req.Header == nil {
				req.Header = make(http.Header)
			}

			if ti.Body == nil {
				return req
			}
			switch b := ti.Body.(type) {
			case []byte:
				req.Body = io.NopCloser(bytes.NewReader(b))
			case func() io.ReadCloser:
				req.Body = b()
			default:
				t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body)
			}
			return req
		}

		if tt.WantDump != "" {
			req := freshReq(tt)
			dump, err := DumpRequest(req, !tt.NoBody)
			if err != nil {
				t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump)
				continue
			}
			if string(dump) != tt.WantDump {
				t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
				continue
			}
		}

		if tt.MustError {
			req := freshReq(tt)
			_, err := DumpRequestOut(req, !tt.NoBody)
			if err == nil {
				t.Errorf("DumpRequestOut #%d: expected an error, got nil", i)
			}
			continue
		}

		if tt.WantDumpOut != "" {
			req := freshReq(tt)
			dump, err := DumpRequestOut(req, !tt.NoBody)
			if err != nil {
				t.Errorf("DumpRequestOut #%d: %s", i, err)
				continue
			}
			if string(dump) != tt.WantDumpOut {
				t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
				continue
			}
		}
	}

	// Validate we haven't leaked any goroutines.
	var dg int
	dl := deadline(t, 5*time.Second, time.Second)
	for time.Now().Before(dl) {
		if dg = runtime.NumGoroutine() - numg0; dg <= 4 {
			// No unexpected goroutines.
			return
		}

		// Allow goroutines to schedule and die off.
		runtime.Gosched()
	}

	buf := make([]byte, 4096)
	buf = buf[:runtime.Stack(buf, true)]
	t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
}

// deadline returns the time which is needed before t.Deadline()
// if one is configured and it is s greater than needed in the future,
// otherwise defaultDelay from the current time.
func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time {
	if dl, ok := t.Deadline(); ok {
		if dl = dl.Add(-needed); dl.After(time.Now()) {
			// Allow an arbitrarily long delay.
			return dl
		}
	}

	// No deadline configured or its closer than needed from now
	// so just use the default.
	return time.Now().Add(defaultDelay)
}

func chunk(s string) string {
	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
}

func mustParseURL(s string) *url.URL {
	u, err := url.Parse(s)
	if err != nil {
		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
	}
	return u
}

func mustNewRequest(method, url string, body io.Reader) *http.Request {
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
	}
	return req
}

func mustReadRequest(s string) *http.Request {
	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
	if err != nil {
		panic(err)
	}
	return req
}

var dumpResTests = []struct {
	res  *http.Response
	body bool
	want string
}{
	{
		res: &http.Response{
			Status:        "200 OK",
			StatusCode:    200,
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			ContentLength: 50,
			Header: http.Header{
				"Foo": []string{"Bar"},
			},
			Body: io.NopCloser(strings.NewReader("foo")), // shouldn't be used
		},
		body: false, // to verify we see 50, not empty or 3.
		want: `HTTP/1.1 200 OK
Content-Length: 50
Foo: Bar`,
	},

	{
		res: &http.Response{
			Status:        "200 OK",
			StatusCode:    200,
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			ContentLength: 3,
			Body:          io.NopCloser(strings.NewReader("foo")),
		},
		body: true,
		want: `HTTP/1.1 200 OK
Content-Length: 3

foo`,
	},

	{
		res: &http.Response{
			Status:           "200 OK",
			StatusCode:       200,
			Proto:            "HTTP/1.1",
			ProtoMajor:       1,
			ProtoMinor:       1,
			ContentLength:    -1,
			Body:             io.NopCloser(strings.NewReader("foo")),
			TransferEncoding: []string{"chunked"},
		},
		body: true,
		want: `HTTP/1.1 200 OK
Transfer-Encoding: chunked

3
foo
0`,
	},
	{
		res: &http.Response{
			Status:        "200 OK",
			StatusCode:    200,
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			ContentLength: 0,
			Header: http.Header{
				// To verify if headers are not filtered out.
				"Foo1": []string{"Bar1"},
				"Foo2": []string{"Bar2"},
			},
			Body: nil,
		},
		body: false, // to verify we see 0, not empty.
		want: `HTTP/1.1 200 OK
Foo1: Bar1
Foo2: Bar2
Content-Length: 0`,
	},
}

func TestDumpResponse(t *testing.T) {
	for i, tt := range dumpResTests {
		gotb, err := DumpResponse(tt.res, tt.body)
		if err != nil {
			t.Errorf("%d. DumpResponse = %v", i, err)
			continue
		}
		got := string(gotb)
		got = strings.TrimSpace(got)
		got = strings.ReplaceAll(got, "\r", "")

		if got != tt.want {
			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
		}
	}
}

// Issue 38352: Check for deadlock on canceled requests.
func TestDumpRequestOutIssue38352(t *testing.T) {
	if testing.Short() {
		return
	}
	t.Parallel()

	timeout := 10 * time.Second
	if deadline, ok := t.Deadline(); ok {
		timeout = time.Until(deadline)
		timeout -= time.Second * 2 // Leave 2 seconds to report failures.
	}
	for i := 0; i < 1000; i++ {
		delay := time.Duration(rand.Intn(5)) * time.Millisecond
		ctx, cancel := context.WithTimeout(context.Background(), delay)
		defer cancel()

		r := bytes.NewBuffer(make([]byte, 10000))
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r)
		if err != nil {
			t.Fatal(err)
		}

		out := make(chan error)
		go func() {
			_, err = DumpRequestOut(req, true)
			out <- err
		}()

		select {
		case <-out:
		case <-time.After(timeout):
			b := &strings.Builder{}
			fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay)
			pprof.Lookup("goroutine").WriteTo(b, 1)
			t.Fatal(b.String())
		}
	}
}
