// Copyright (c) 2019 The BFE Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Copyright 2010 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 bfe_http

import (
	"bytes"
	"fmt"
	"io"
	"net/url"
	"reflect"
	"strings"
	"testing"
)

import (
	"github.com/bfenetworks/bfe/bfe_bufio"
)

type reqTest struct {
	Raw     string
	Req     *Request
	Body    string
	Trailer Header
	Error   string
}

var noError = ""
var noBody = ""
var noTrailer Header = nil

var reqTests = []reqTest{
	// Baseline test; All Request fields included for template use
	{
		"GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
			"Host: www.techcrunch.com\r\n" +
			"User-Agent: Fake\r\n" +
			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
			"Accept-Language: en-us,en;q=0.5\r\n" +
			"Accept-Encoding: gzip,deflate\r\n" +
			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
			"Keep-Alive: 300\r\n" +
			"Content-Length: 7\r\n" +
			"Proxy-Connection: keep-alive\r\n\r\n" +
			"abcdef\n???",

		&Request{
			Method: MethodGet,
			URL: &url.URL{
				Scheme: "http",
				Host:   "www.techcrunch.com",
				Path:   "/",
			},
			Proto:      "HTTP/1.1",
			ProtoMajor: 1,
			ProtoMinor: 1,
			Header: Header{
				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
				"Accept-Language":  {"en-us,en;q=0.5"},
				"Accept-Encoding":  {"gzip,deflate"},
				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
				"Keep-Alive":       {"300"},
				"Proxy-Connection": {"keep-alive"},
				"Content-Length":   {"7"},
				"User-Agent":       {"Fake"},
			},
			HeaderKeys: []string{
				"Host",
				"User-Agent",
				"Accept",
				"Accept-Language",
				"Accept-Encoding",
				"Accept-Charset",
				"Keep-Alive",
				"Content-Length",
				"Proxy-Connection",
			},
			Close:         false,
			ContentLength: 7,
			Host:          "www.techcrunch.com",
			RequestURI:    "http://www.techcrunch.com/",
		},

		"abcdef\n",

		noTrailer,
		noError,
	},

	// GET request with no body (the normal case)
	{
		"GET / HTTP/1.1\r\n" +
			"Host: foo.com\r\n\r\n",

		&Request{
			Method: MethodGet,
			URL: &url.URL{
				Path: "/",
			},
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			Header:        Header{},
			HeaderKeys:    []string{"Host"},
			Close:         false,
			ContentLength: 0,
			Host:          "foo.com",
			RequestURI:    "/",
		},

		noBody,
		noTrailer,
		noError,
	},

	// Tests that we don't parse a path that looks like a
	// scheme-relative URI as a scheme-relative URI.
	{
		"GET //user@host/is/actually/a/path/ HTTP/1.1\r\n" +
			"Host: test\r\n\r\n",

		&Request{
			Method: MethodGet,
			URL: &url.URL{
				Path: "//user@host/is/actually/a/path/",
			},
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			Header:        Header{},
			HeaderKeys:    []string{"Host"},
			Close:         false,
			ContentLength: 0,
			Host:          "test",
			RequestURI:    "//user@host/is/actually/a/path/",
		},

		noBody,
		noTrailer,
		noError,
	},

	// Tests a bogus abs_path on the Request-Line (RFC 2616 section 5.1.2)
	{
		"GET ../../../../etc/passwd HTTP/1.1\r\n" +
			"Host: test\r\n\r\n",
		nil,
		noBody,
		noTrailer,
		"invalid URI for request",
	},

	// Tests missing URL:
	{
		"GET  HTTP/1.1\r\n" +
			"Host: test\r\n\r\n",
		nil,
		noBody,
		noTrailer,
		"empty url",
	},

	// Tests chunked body with trailer:
	{
		"POST / HTTP/1.1\r\n" +
			"Host: foo.com\r\n" +
			"Transfer-Encoding: chunked\r\n\r\n" +
			"3\r\nfoo\r\n" +
			"3\r\nbar\r\n" +
			"0\r\n" +
			"Trailer-Key: Trailer-Value\r\n" +
			"\r\n",
		&Request{
			Method: MethodPost,
			URL: &url.URL{
				Path: "/",
			},
			TransferEncoding: []string{"chunked"},
			Proto:            "HTTP/1.1",
			ProtoMajor:       1,
			ProtoMinor:       1,
			Header:           Header{},
			HeaderKeys:       []string{"Host", "Transfer-Encoding"},
			ContentLength:    -1,
			Host:             "foo.com",
			RequestURI:       "/",
		},

		"foobar",
		Header{
			"Trailer-Key": {"Trailer-Value"},
		},
		noError,
	},

	// CONNECT request with domain name:
	{
		"CONNECT www.google.com:443 HTTP/1.1\r\n\r\n",

		&Request{
			Method: MethodConnect,
			URL: &url.URL{
				Host: "www.google.com:443",
			},
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			Header:        Header{},
			HeaderKeys:    []string{},
			Close:         false,
			ContentLength: 0,
			Host:          "www.google.com:443",
			RequestURI:    "www.google.com:443",
		},

		noBody,
		noTrailer,
		noError,
	},

	// CONNECT request with IP address:
	{
		"CONNECT 127.0.0.1:6060 HTTP/1.1\r\n\r\n",

		&Request{
			Method: MethodConnect,
			URL: &url.URL{
				Host: "127.0.0.1:6060",
			},
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			Header:        Header{},
			HeaderKeys:    []string{},
			Close:         false,
			ContentLength: 0,
			Host:          "127.0.0.1:6060",
			RequestURI:    "127.0.0.1:6060",
		},

		noBody,
		noTrailer,
		noError,
	},

	// CONNECT request for RPC:
	{
		"CONNECT /_goRPC_ HTTP/1.1\r\n\r\n",

		&Request{
			Method: MethodConnect,
			URL: &url.URL{
				Path: "/_goRPC_",
			},
			Proto:         "HTTP/1.1",
			ProtoMajor:    1,
			ProtoMinor:    1,
			Header:        Header{},
			HeaderKeys:    []string{},
			Close:         false,
			ContentLength: 0,
			Host:          "",
			RequestURI:    "/_goRPC_",
		},

		noBody,
		noTrailer,
		noError,
	},

	// SSDP Notify request. golang.org/issue/3692
	{
		"NOTIFY * HTTP/1.1\r\nServer: foo\r\n\r\n",
		&Request{
			Method: "NOTIFY",
			URL: &url.URL{
				Path: "*",
			},
			Proto:      "HTTP/1.1",
			ProtoMajor: 1,
			ProtoMinor: 1,
			Header: Header{
				"Server": []string{"foo"},
			},
			HeaderKeys:    []string{"Server"},
			Close:         false,
			ContentLength: 0,
			RequestURI:    "*",
		},

		noBody,
		noTrailer,
		noError,
	},

	// OPTIONS request. Similar to golang.org/issue/3692
	{
		"OPTIONS * HTTP/1.1\r\nServer: foo\r\n\r\n",
		&Request{
			Method: MethodOptions,
			URL: &url.URL{
				Path: "*",
			},
			Proto:      "HTTP/1.1",
			ProtoMajor: 1,
			ProtoMinor: 1,
			Header: Header{
				"Server": []string{"foo"},
			},
			HeaderKeys:    []string{"Server"},
			Close:         false,
			ContentLength: 0,
			RequestURI:    "*",
		},

		noBody,
		noTrailer,
		noError,
	},
}

func TestReadRequest(t *testing.T) {
	for i := range reqTests {
		tt := &reqTests[i]
		var braw bytes.Buffer
		braw.WriteString(tt.Raw)
		req, err := ReadRequest(bfe_bufio.NewReader(&braw), 8000)
		if err != nil {
			if !strings.Contains(err.Error(), tt.Error) {
				t.Errorf("#%d: error %q, want include error %q", i, err.Error(), tt.Error)
			}
			continue
		}
		rbody := req.Body
		req.Body = nil
		req.State = nil // ignore request state
		diff(t, fmt.Sprintf("#%d Request", i), req, tt.Req)
		var bout bytes.Buffer
		if rbody != nil {
			_, err := io.Copy(&bout, rbody)
			if err != nil {
				t.Fatalf("#%d. copying body: %v", i, err)
			}
			rbody.Close()
		}
		body := bout.String()
		if body != tt.Body {
			t.Errorf("#%d: Body = %q want %q", i, body, tt.Body)
		}
		if !reflect.DeepEqual(tt.Trailer, req.Trailer) {
			t.Errorf("#%d. Trailers differ.\n got: %v\nwant: %v", i, req.Trailer, tt.Trailer)
		}
	}
}
