package main

import (
	"bytes"
	"crypto/rand"
	"io/ioutil"
	_ "log"
	"net"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
	_ "reflect"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/buger/goreplay/proto"
)

func TestHTTPClientURLPort(t *testing.T) {
	c1 := NewHTTPClient("http://example.com", &HTTPClientConfig{})
	if c1.baseURL != "http://example.com" {
		t.Error("Sould not add 80 port for http:", c1.baseURL)
	}

	c2 := NewHTTPClient("https://example.com", &HTTPClientConfig{})
	if c2.baseURL != "https://example.com" {
		t.Error("Sould not add 443 port for https:", c2.baseURL)
	}

	c3 := NewHTTPClient("https://example.com:1", &HTTPClientConfig{})
	if c3.baseURL != "https://example.com:1" {
		t.Error("Sould use specified port:", c3.baseURL)
	}

	c4 := NewHTTPClient("example.com", &HTTPClientConfig{})
	if c4.baseURL != "http://example.com" {
		t.Error("Sould not add default protocol:", c4.baseURL)
	}
}

func TestHTTPClientSend(t *testing.T) {
	wg := new(sync.WaitGroup)

	payload := func(reqType string) []byte {
		switch reqType {
		case "GET":
			return []byte("GET / HTTP/1.1\r\n\r\n")
		case "POST":
			return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
		case "POST_CHUNKED":
			return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
		}

		return []byte("")
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		if r.Method == "POST" {
			defer r.Body.Close()
			body, _ := ioutil.ReadAll(r.Body)

			if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
				if string(body) != "Wikipedia in\r\n\r\nchunks." {
					t.Error("Wrong POST body:", body, string(body))
				}
			} else {
				if string(body) != "a=1&b=2" {
					buf, _ := httputil.DumpRequest(r, true)
					t.Error("Wrong POST body:", string(body), string(buf))
				}
			}
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})

	wg.Add(4)
	client.Send(payload("POST"))
	client.Send(payload("GET"))
	client.Send(payload("POST_CHUNKED"))
	client.Send(payload("POST"))

	wg.Wait()

	client = NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true, CompatibilityMode: true})

	wg.Add(4)

	if _, err := client.Send(payload("POST")); err != nil {
		t.Fatal(err.Error())
	}
	if _, err := client.Send(payload("GET")); err != nil {
		t.Fatal(err.Error())
	}
	if _, err := client.Send(payload("POST_CHUNKED")); err != nil {
		t.Fatal(err.Error())
	}
	if _, err := client.Send(payload("POST")); err != nil {
		t.Fatal(err.Error())
	}

	wg.Wait()
}

func TestHTTPClientResonseByClose(t *testing.T) {
	wg := new(sync.WaitGroup)

	payload := []byte("GET / HTTP/1.1\r\n\r\n")
	ln, _ := net.Listen("tcp", ":0")
	go func() {
		for {
			conn, _ := ln.Accept()
			buf := make([]byte, 4096)
			conn.Read(buf)

			conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
			conn.Write([]byte("ab"))
			conn.Close()

			wg.Done()
		}
	}()

	client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{Debug: true})

	wg.Add(1)
	resp, _ := client.Send(payload)

	if !bytes.Equal(resp, []byte("HTTP/1.1 200 OK\r\n\r\nab")) {
		t.Error("Should return valid response", string(resp))
	}

	wg.Wait()
}

// https://github.com/buger/gor/issues/184
func TestHTTPClientResponseBuffer(t *testing.T) {
	testCases := []struct {
		name         string
		responseSize int
		buffserSize  int
		expectedSize int
		timeout      time.Duration
	}{
		{"Chunked, buffer overflow", 10 * 1024, 1024, 1024, 50 * time.Millisecond},

		{"Chunked, fits buffer", 10 * 1024, 64 * 1024, 10*1024 + 145 /* headers length + chunked meta */, 50 * time.Millisecond},

		{"Content-Length, buffer overflow", 1024, 1000, 1000, 50 * time.Millisecond},

		{"Content-Length, fits buffer", 1024, 64 * 1024, 1024 + 118, 50 * time.Millisecond},
	}

	for _, tc := range testCases {
		wg := new(sync.WaitGroup)

		payload := []byte("GET / HTTP/1.1\r\n\r\n")

		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

			size := tc.responseSize // 1kb
			rb := make([]byte, size)
			rand.Read(rb)

			w.Write(rb[:size/2])
			w.Write(rb[size/2:])

			wg.Done()
		}))

		client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true, ResponseBufferSize: tc.buffserSize, Timeout: 100 * time.Millisecond})

		wg.Add(2)

		start := time.Now()
		client.Send(payload)
		resp, err := client.Send(payload)
		stop := time.Now()

		if err != nil {
			t.Error("Request error", err)
		}

		if stop.Sub(start) > tc.timeout {
			t.Error("Request took too long", stop.Sub(start), tc.timeout)
		}

		if len(resp) != tc.expectedSize {
			t.Error(tc.name, " - Wrong response size:", tc.expectedSize, len(resp))
		} else {
			if !bytes.Equal(resp[0:8], []byte("HTTP/1.1")) {
				t.Error(tc.name, " - Response buffer contains data from previous request", string(resp), len(resp))
			}
		}

		wg.Wait()
		server.Close()
	}
}

func TestHTTPClientHTTPSSend(t *testing.T) {
	wg := new(sync.WaitGroup)

	payload := func(reqType string) []byte {
		switch reqType {
		case "GET":
			return []byte("GET / HTTP/1.1\r\n\r\n")
		case "POST":
			return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
		case "POST_CHUNKED":
			return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
		}

		return []byte("")
	}

	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		if r.Method == "POST" {
			defer r.Body.Close()
			body, _ := ioutil.ReadAll(r.Body)

			if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
				if string(body) != "Wikipedia in\r\n\r\nchunks." {
					t.Error("Wrong POST body:", body, string(body))
				}
			} else {
				if string(body) != "a=1&b=2" {
					buf, _ := httputil.DumpRequest(r, true)
					t.Error("Wrong POST body:", string(body), string(buf))
				}
			}
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{})

	wg.Add(4)
	client.Send(payload("POST"))
	client.Send(payload("GET"))
	client.Send(payload("POST_CHUNKED"))
	client.Send(payload("POST"))

	wg.Wait()
}

func TestHTTPClientServerInstantDisconnect(t *testing.T) {
	wg := new(sync.WaitGroup)

	GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")

	ln, _ := net.Listen("tcp", "127.0.0.1:0")
	defer ln.Close()

	go func() {
		for {
			conn, err := ln.Accept()

			if err != nil {
				break
			}
			conn.Close()
			wg.Done()
		}
	}()

	client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})

	wg.Add(2)
	client.Send(GETPayload)
	client.Send(GETPayload)

	wg.Wait()
}

func TestHTTPClientServerNoKeepAlive(t *testing.T) {
	wg := new(sync.WaitGroup)

	GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")

	ln, _ := net.Listen("tcp", "127.0.0.1:0")
	defer ln.Close()

	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				break
			}

			buf := make([]byte, 4096)
			_, err = conn.Read(buf)
			if err != nil {
				t.Error("Error reading:", err.Error())
			}
			conn.Write([]byte("OK"))

			// No keep-alive connections
			conn.Close()

			wg.Done()
		}
	}()

	client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})

	wg.Add(2)
	client.Send(GETPayload)
	client.Send(GETPayload)

	wg.Wait()
}

func TestHTTPClientRedirect(t *testing.T) {
	wg := new(sync.WaitGroup)

	GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		if r.URL.Path == "/" {
			http.Redirect(w, r, "/new", 301)
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 1, Debug: false})

	// Should do 2 queries
	wg.Add(2)
	client.Send(GETPayload)

	wg.Wait()
}

func TestHTTPClientRedirectLimit(t *testing.T) {
	wg := new(sync.WaitGroup)

	GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		if r.URL.Path == "/" {
			http.Redirect(w, r, "/r1", 301)
		}

		if r.URL.Path == "/r1" {
			http.Redirect(w, r, "/r2", 301)
		}

		if r.URL.Path == "/r2" {
			http.Redirect(w, r, "/new", 301)
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 2, Debug: false})

	// Have 3 redirects + 1 GET, but should do only 2 redirects + GET
	wg.Add(3)
	client.Send(GETPayload)

	wg.Wait()
}

func TestHTTPClientKeepHeadersRedirect(t *testing.T) {
	wg := new(sync.WaitGroup)

	GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")

	GETPayload = proto.AddHeader(GETPayload, []byte("keep-header"), []byte("true"))

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		if r.URL.Path == "/" {
			http.Redirect(w, r, "/new", 301)
		}

		if r.Header.Get("keep-header") != "true" {
			t.Errorf("Header keep-header was incorrect, got: %s, want: %s.", r.Header.Get("keep-header"), "true")
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 1, Debug: false})

	wg.Add(2)
	client.Send(GETPayload)

	wg.Wait()
}

func TestHTTPClientBasicAuth(t *testing.T) {
	wg := new(sync.WaitGroup)
	wg.Add(2)

	GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, pass, _ := r.BasicAuth()

		if user != "user" || pass != "pass" {
			http.Error(w, "Unauthorized.", 401)
			wg.Done()
			return
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: false})
	resp, _ := client.Send(GETPayload)
	client.Disconnect()

	if !bytes.Equal(proto.Status(resp), []byte("401")) {
		t.Error("Should return unauthorized error", string(resp))
	}

	authUrl := strings.Replace(server.URL, "http://", "http://user:pass@", -1)
	client = NewHTTPClient(authUrl, &HTTPClientConfig{Debug: false})
	resp, _ = client.Send(GETPayload)
	client.Disconnect()

	if !bytes.Equal(proto.Status(resp), []byte("200")) {
		t.Error("Should return proper response", string(resp))
	}

	wg.Wait()
}

func TestHTTPClientHandleHTTP10(t *testing.T) {
	wg := new(sync.WaitGroup)

	GETPayload := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		if r.URL.Path != "/path" {
			t.Error("Path not match:", r.URL.Path)
		}

		wg.Done()
	}))
	defer server.Close()

	client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})

	wg.Add(1)
	client.Send(GETPayload)

	wg.Wait()
}

// func TestHTTPClientErrors(t *testing.T) {
// 	req := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")

// 	// Port not exists
// 	client := NewHTTPClient("http://127.0.0.1:1", &HTTPClientConfig{Debug: true})
// 	if resp, err := client.Send(req); err != nil {
// 		if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
// 			t.Error("Should return status 521 for connection refused, instead:", string(s), err)
// 		}
// 	} else {
// 		t.Error("Should throw error")
// 	}

// 	client = NewHTTPClient("http://not.existing", &HTTPClientConfig{Debug: true})
// 	if resp, err := client.Send(req); err != nil {
// 		if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
// 			t.Error("Should return status 521 for no such host, instead:", string(s))
// 		}
// 	} else {
// 		t.Error("Should throw error")
// 	}

// 	// Non routable IP address to simulate connection timeout
// 	client = NewHTTPClient("http://10.255.255.1", &HTTPClientConfig{Debug: true, ConnectionTimeout: 100 * time.Millisecond})

// 	if resp, err := client.Send(req); err != nil {
// 		if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
// 			t.Error("Should return status 521 for io/timeout:", string(s))
// 		}
// 	} else {
// 		t.Error("Should throw error")
// 	}

// 	// Connecting but io timeout on read
// 	ln, _ := net.Listen("tcp", "127.0.0.1:0")
// 	client = NewHTTPClient("http://"+ln.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
// 	defer ln.Close()

// 	if resp, err := client.Send(req); err != nil {
// 		if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
// 			t.Error("Should return status 524 for io read, instead:", string(s))
// 		}
// 	} else {
// 		t.Error("Should throw error")
// 	}

// 	// Response read error read tcp [::1]:51128: connection reset by peer &{{0xc20802a000}}
// 	ln1, _ := net.Listen("tcp", "127.0.0.1:0")
// 	go func() {
// 		ln1.Accept()
// 	}()
// 	defer ln1.Close()

// 	client = NewHTTPClient("http://"+ln1.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})

// 	if resp, err := client.Send(req); err != nil {
// 		if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
// 			t.Error("Should return status 524 for connection reset by peer, instead:", string(s))
// 		}
// 	} else {
// 		t.Error("Should throw error")
// 	}
// }
