package httpexpect

import (
	"bufio"
	"crypto/tls"
	"encoding/json"
	"errors"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/valyala/fasthttp"
	"github.com/valyala/fasthttp/fasthttpadaptor"
)

func TestExpectMethods(t *testing.T) {
	client := &mockClient{}

	reporter := NewAssertReporter(t)

	config := Config{
		BaseURL:  "http://example.com",
		Client:   client,
		Reporter: reporter,
	}

	var reqs [8]*Request

	e := WithConfig(config)

	reqs[0] = e.Request("METHOD", "/url")
	reqs[1] = e.OPTIONS("/url")
	reqs[2] = e.HEAD("/url")
	reqs[3] = e.GET("/url")
	reqs[4] = e.POST("/url")
	reqs[5] = e.PUT("/url")
	reqs[6] = e.PATCH("/url")
	reqs[7] = e.DELETE("/url")

	assert.Equal(t, "METHOD", reqs[0].http.Method)
	assert.Equal(t, "OPTIONS", reqs[1].http.Method)
	assert.Equal(t, "HEAD", reqs[2].http.Method)
	assert.Equal(t, "GET", reqs[3].http.Method)
	assert.Equal(t, "POST", reqs[4].http.Method)
	assert.Equal(t, "PUT", reqs[5].http.Method)
	assert.Equal(t, "PATCH", reqs[6].http.Method)
	assert.Equal(t, "DELETE", reqs[7].http.Method)
}

func TestExpectBuilders(t *testing.T) {
	client := &mockClient{}

	reporter := NewAssertReporter(t)

	config := Config{
		Client:   client,
		Reporter: reporter,
	}

	e := WithConfig(config)

	var reqs1 []*Request

	e1 := e.Builder(func(r *Request) {
		reqs1 = append(reqs1, r)
	})

	var reqs2 []*Request

	e2 := e1.Builder(func(r *Request) {
		reqs2 = append(reqs2, r)
	})

	e.Request("METHOD", "/url")

	r1 := e1.Request("METHOD", "/url")
	r2 := e2.Request("METHOD", "/url")

	assert.Equal(t, 2, int(len(reqs1)))
	assert.Equal(t, 1, int(len(reqs2)))

	assert.Equal(t, r1, reqs1[0])
	assert.Equal(t, r2, reqs1[1])
	assert.Equal(t, r1, reqs2[0])
}

func TestExpectValues(t *testing.T) {
	client := &mockClient{}

	r := NewAssertReporter(t)

	config := Config{
		Client:   client,
		Reporter: r,
	}

	e := WithConfig(config)

	m := map[string]interface{}{}
	a := []interface{}{}
	s := ""
	n := 0.0
	b := false

	assert.Equal(t, NewValue(r, m), e.Value(m))
	assert.Equal(t, NewObject(r, m), e.Object(m))
	assert.Equal(t, NewArray(r, a), e.Array(a))
	assert.Equal(t, NewString(r, s), e.String(s))
	assert.Equal(t, NewNumber(r, n), e.Number(n))
	assert.Equal(t, NewBoolean(r, b), e.Boolean(b))
}

func TestExpectTraverse(t *testing.T) {
	client := &mockClient{}

	reporter := NewAssertReporter(t)

	config := Config{
		BaseURL:  "http://example.com",
		Client:   client,
		Reporter: reporter,
	}

	data := map[string]interface{}{
		"foo": []interface{}{"bar", 123, false, nil},
		"bar": "hello",
		"baz": 456,
	}

	resp := WithConfig(config).GET("/url").WithJSON(data).Expect()

	m := resp.JSON().Object()

	m.Equal(data)

	m.ContainsKey("foo")
	m.ContainsKey("bar")
	m.ContainsKey("foo")

	m.ValueEqual("foo", data["foo"])
	m.ValueEqual("bar", data["bar"])
	m.ValueEqual("baz", data["baz"])

	m.Keys().ContainsOnly("foo", "bar", "baz")
	m.Values().ContainsOnly(data["foo"], data["bar"], data["baz"])

	m.Value("foo").Array().Elements("bar", 123, false, nil)
	m.Value("bar").String().Equal("hello")
	m.Value("baz").Number().Equal(456)

	m.Value("foo").Array().Element(2).Boolean().False()
	m.Value("foo").Array().Element(3).Null()
}

func TestExpectBranches(t *testing.T) {
	client := &mockClient{}

	config := Config{
		BaseURL:  "http://example.com",
		Client:   client,
		Reporter: newMockReporter(t),
	}

	data := map[string]interface{}{
		"foo": []interface{}{"bar", 123, false, nil},
		"bar": "hello",
		"baz": 456,
	}

	resp := WithConfig(config).GET("/url").WithJSON(data).Expect()

	m1 := resp.JSON().Array()
	m2 := resp.JSON().Object()

	e1 := m2.Value("foo").Object()
	e2 := m2.Value("foo").Array().Element(999).String()
	e3 := m2.Value("foo").Array().Element(0).Number()
	e4 := m2.Value("foo").Array().Element(0).String()

	e4.Equal("bar")

	m1.chain.assertFailed(t)
	m2.chain.assertOK(t)

	e1.chain.assertFailed(t)
	e2.chain.assertFailed(t)
	e3.chain.assertFailed(t)
	e4.chain.assertOK(t)
}

func TestExpectStdCompat(_ *testing.T) {
	New(&testing.T{}, "")
	New(&testing.B{}, "")
	New(testing.TB(&testing.T{}), "")
}

type testRequestFactory struct {
	lastreq *http.Request
	fail    bool
}

func (f *testRequestFactory) NewRequest(
	method, urlStr string, body io.Reader) (*http.Request, error) {
	if f.fail {
		return nil, errors.New("testRequestFactory")
	}
	f.lastreq = httptest.NewRequest(method, urlStr, body)
	return f.lastreq, nil
}

func TestExpectRequestFactory(t *testing.T) {
	e1 := WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
	})
	r1 := e1.Request("GET", "/")
	r1.chain.assertOK(t)
	assert.NotNil(t, r1.http)

	f2 := &testRequestFactory{}
	e2 := WithConfig(Config{
		BaseURL:        "http://example.com",
		Reporter:       NewAssertReporter(t),
		RequestFactory: f2,
	})
	r2 := e2.Request("GET", "/")
	r2.chain.assertOK(t)
	assert.NotNil(t, f2.lastreq)
	assert.True(t, f2.lastreq == r2.http)

	f3 := &testRequestFactory{
		fail: true,
	}
	e3 := WithConfig(Config{
		BaseURL:        "http://example.com",
		Reporter:       newMockReporter(t),
		RequestFactory: f3,
	})
	r3 := e3.Request("GET", "/")
	r3.chain.assertFailed(t)
	assert.Nil(t, f3.lastreq)
}

func createBasicHandler() http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/foo", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"foo":123}`))
	})

	mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
		w.Write([]byte(`field1=` + r.FormValue("field1")))
		w.Write([]byte(`&field2=` + r.PostFormValue("field2")))
	})

	mux.HandleFunc("/baz/qux", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case "GET":
			w.Header().Set("Content-Type", "application/json")
			w.Write([]byte(`[true, false]`))

		case "PUT":
			decoder := json.NewDecoder(r.Body)
			var m map[string]interface{}
			if err := decoder.Decode(&m); err != nil {
				w.WriteHeader(http.StatusBadRequest)
			} else if m["test"] != "ok" {
				w.WriteHeader(http.StatusBadRequest)
			} else {
				w.WriteHeader(http.StatusOK)
				w.Write([]byte(`ok`))
			}
		}
	})

	mux.HandleFunc("/wee", func(w http.ResponseWriter, r *http.Request) {
		if u, p, ok := r.BasicAuth(); ok {
			w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
			w.Write([]byte(`username=` + u))
			w.Write([]byte(`&password=` + p))
		} else {
			w.WriteHeader(http.StatusBadRequest)
		}
	})

	return mux
}

func testBasicHandler(e *Expect) {
	e.GET("/foo")
	e.GET("/foo").Expect()
	e.GET("/foo").Expect().Status(http.StatusOK)

	e.GET("/foo").Expect().
		Status(http.StatusOK).JSON().Object().ValueEqual("foo", 123)

	e.PUT("/bar").WithQuery("field1", "hello").WithFormField("field2", "world").
		Expect().
		Status(http.StatusOK).
		Form().ValueEqual("field1", "hello").ValueEqual("field2", "world")

	e.GET("/{a}/{b}", "baz", "qux").
		Expect().
		Status(http.StatusOK).JSON().Array().Elements(true, false)

	e.PUT("/{a}/{b}").
		WithPath("a", "baz").
		WithPath("b", "qux").
		WithJSON(map[string]string{"test": "ok"}).
		Expect().
		Status(http.StatusOK).Body().Equal("ok")

	auth := e.Builder(func(req *Request) {
		req.WithBasicAuth("john", "secret")
	})

	auth.PUT("/wee").
		Expect().
		Status(http.StatusOK).
		Form().ValueEqual("username", "john").ValueEqual("password", "secret")
}

func TestExpectBasicHandlerLiveDefault(t *testing.T) {
	handler := createBasicHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	testBasicHandler(New(t, server.URL))
}

func TestExpectBasicHandlerLiveConfig(t *testing.T) {
	handler := createBasicHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	testBasicHandler(WithConfig(Config{
		BaseURL:  server.URL,
		Reporter: NewAssertReporter(t),
		Printers: []Printer{
			NewCurlPrinter(t),
			NewDebugPrinter(t, true),
		},
	}))
}

func TestExpectBasicHandlerLiveTLS(t *testing.T) {
	handler := createBasicHandler()

	server := httptest.NewTLSServer(handler)
	defer server.Close()

	testBasicHandler(WithConfig(Config{
		BaseURL:  server.URL,
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{
					InsecureSkipVerify: true,
				},
			},
		},
	}))
}

func TestExpectBasicHandlerLiveLongRun(t *testing.T) {
	if testing.Short() {
		return
	}

	handler := createBasicHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	e := New(t, server.URL)

	for i := 0; i < 2; i++ {
		testBasicHandler(e)
	}
}

func TestExpectBasicHandlerBinderStandard(t *testing.T) {
	handler := createBasicHandler()

	testBasicHandler(WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewBinder(handler),
		},
	}))
}

func TestExpectBasicHandlerBinderFast(t *testing.T) {
	handler := fasthttpadaptor.NewFastHTTPHandler(createBasicHandler())

	testBasicHandler(WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewFastBinder(handler),
		},
	}))
}

func createRedirectHandler() http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/foo", func(w http.ResponseWriter, _ *http.Request) {
		w.Write([]byte(`hello`))
	})

	mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/foo", http.StatusFound)
	})

	return mux
}

func createRedirectFastHandler() fasthttp.RequestHandler {
	return func(ctx *fasthttp.RequestCtx) {
		switch string(ctx.Path()) {
		case "/foo":
			ctx.SetBody([]byte(`hello`))

		case "/bar":
			ctx.Redirect("/foo", http.StatusFound)
		}
	}
}

func testRedirectHandler(e *Expect) {
	e.POST("/bar").
		Expect().
		Status(http.StatusOK).Body().Equal(`hello`)
}

func TestExpectRedirectHandlerLive(t *testing.T) {
	handler := createRedirectHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	testRedirectHandler(New(t, server.URL))
}

func TestExpectRedirectHandlerBinderStandard(t *testing.T) {
	handler := createRedirectHandler()

	testRedirectHandler(WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewBinder(handler),
		},
	}))
}

func TestExpectRedirectHandlerBinderFast(t *testing.T) {
	handler := createRedirectFastHandler()

	testRedirectHandler(WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewFastBinder(handler),
		},
	}))
}

func createAutoTLSHandler(https string) http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/tls", func(w http.ResponseWriter, r *http.Request) {
		if r.TLS == nil {
			w.Write([]byte(`no`))
		} else {
			w.Write([]byte(`yes`))
		}
	})

	mux.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
		if r.TLS == nil {
			http.Redirect(w, r, https+r.RequestURI, http.StatusFound)
		} else {
			w.Write([]byte(`hello`))
		}
	})

	return mux
}

func createAutoTLSFastHandler(https string) fasthttp.RequestHandler {
	return func(ctx *fasthttp.RequestCtx) {
		switch string(ctx.Path()) {
		case "/tls":
			if !ctx.IsTLS() {
				ctx.SetBody([]byte(`no`))
			} else {
				ctx.SetBody([]byte(`yes`))
			}

		case "/protected":
			if !ctx.IsTLS() {
				ctx.Redirect(https+string(ctx.Request.RequestURI()), http.StatusFound)
			} else {
				ctx.SetBody([]byte(`hello`))
			}
		}
	}
}

func testAutoTLSHandler(config Config) {
	e := WithConfig(config)

	tls := e.POST("/tls").
		Expect().
		Status(http.StatusOK).Body()

	if strings.HasPrefix(config.BaseURL, "https://") {
		tls.Equal(`yes`)
	} else {
		tls.Equal(`no`)
	}

	e.POST("/protected").
		Expect().
		Status(http.StatusOK).Body().Equal(`hello`)
}

func TestExpectAutoTLSHandlerLive(t *testing.T) {
	httpsServ := httptest.NewTLSServer(createAutoTLSHandler(""))
	defer httpsServ.Close()

	httpServ := httptest.NewServer(createAutoTLSHandler(httpsServ.URL))
	defer httpServ.Close()

	assert.True(t, strings.HasPrefix(httpsServ.URL, "https://"))
	assert.True(t, strings.HasPrefix(httpServ.URL, "http://"))

	for _, url := range []string{httpsServ.URL, httpServ.URL} {
		testAutoTLSHandler(Config{
			BaseURL:  url,
			Reporter: NewRequireReporter(t),
			Printers: []Printer{
				NewDebugPrinter(t, true),
			},
			Client: &http.Client{
				Transport: &http.Transport{
					TLSClientConfig: &tls.Config{
						InsecureSkipVerify: true,
					},
				},
			},
		})
	}
}

func TestExpectAutoTLSHandlerBinderStandard(t *testing.T) {
	handler := createAutoTLSHandler("https://example.com")

	for _, url := range []string{"https://example.com", "http://example.com"} {
		testAutoTLSHandler(Config{
			BaseURL:  url,
			Reporter: NewRequireReporter(t),
			Printers: []Printer{
				NewDebugPrinter(t, true),
			},
			Client: &http.Client{
				Transport: &Binder{
					Handler: handler,
					TLS:     &tls.ConnectionState{},
				},
			},
		})
	}
}

func TestExpectAutoTLSHandlerBinderFast(t *testing.T) {
	handler := createAutoTLSFastHandler("https://example.com")

	for _, url := range []string{"https://example.com", "http://example.com"} {
		testAutoTLSHandler(Config{
			BaseURL:  url,
			Reporter: NewRequireReporter(t),
			Printers: []Printer{
				NewDebugPrinter(t, true),
			},
			Client: &http.Client{
				Transport: &FastBinder{
					Handler: handler,
					TLS:     &tls.ConnectionState{},
				},
			},
		})
	}
}

func createChunkedHandler() http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.Proto != "HTTP/1.1" {
			w.WriteHeader(http.StatusBadRequest)
		} else if len(r.TransferEncoding) != 1 || r.TransferEncoding[0] != "chunked" {
			w.WriteHeader(http.StatusBadRequest)
		} else if r.PostFormValue("key") != "value" {
			w.WriteHeader(http.StatusBadRequest)
		} else {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(`[1, `))
			w.(http.Flusher).Flush()
			w.Write([]byte(`2]`))
		}
	})

	return mux
}

func createChunkedFastHandler(t *testing.T) fasthttp.RequestHandler {
	return func(ctx *fasthttp.RequestCtx) {
		headers := map[string][]string{}

		ctx.Request.Header.VisitAll(func(k, v []byte) {
			headers[string(k)] = append(headers[string(k)], string(v))
		})

		assert.Equal(t, []string{"chunked"}, headers["Transfer-Encoding"])
		assert.Equal(t, "value", string(ctx.FormValue("key")))
		assert.Equal(t, "key=value", string(ctx.Request.Body()))

		ctx.Response.Header.Set("Content-Type", "application/json")
		ctx.Response.SetBodyStreamWriter(func(w *bufio.Writer) {
			w.WriteString(`[1, `)
			w.Flush()
			w.WriteString(`2]`)
		})
	}
}

func testChunkedHandler(e *Expect) {
	e.PUT("/").
		WithHeader("Content-Type", "application/x-www-form-urlencoded").
		WithChunked(strings.NewReader("key=value")).
		Expect().
		Status(http.StatusOK).
		ContentType("application/json").
		TransferEncoding("chunked").
		JSON().Array().Elements(1, 2)
}

func TestExpectChunkedHandlerLive(t *testing.T) {
	handler := createChunkedHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	testChunkedHandler(New(t, server.URL))
}

func TestExpectChunkedHandlerBinderStandard(t *testing.T) {
	handler := createChunkedHandler()

	testChunkedHandler(WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewBinder(handler),
		},
	}))
}

func TestExpectChunkedHandlerBinderFast(t *testing.T) {
	handler := createChunkedFastHandler(t)

	testChunkedHandler(WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewFastBinder(handler),
		},
	}))
}

func createCookieHandler() http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
		http.SetCookie(w, &http.Cookie{
			Name:    "myname",
			Value:   "myvalue",
			Path:    "/",
			Expires: time.Date(3000, 0, 0, 0, 0, 0, 0, time.UTC),
		})
		w.WriteHeader(http.StatusNoContent)
	})

	mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
		cookie, err := r.Cookie("myname")
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
		} else {
			w.Header().Set("Content-Type", "text/plain")
			w.Write([]byte(cookie.Value))
		}
	})

	return mux
}

func testCookieHandler(e *Expect, enabled bool) {
	r := e.PUT("/set").Expect().Status(http.StatusNoContent)

	r.Cookies().ContainsOnly("myname")
	c := r.Cookie("myname")
	c.Value().Equal("myvalue")
	c.Path().Equal("/")
	c.Expires().Equal(time.Date(3000, 0, 0, 0, 0, 0, 0, time.UTC))

	if enabled {
		e.GET("/get").Expect().Status(http.StatusOK).Text().Equal("myvalue")
	} else {
		e.GET("/get").Expect().Status(http.StatusBadRequest)
	}
}

func TestExpectCookieHandlerLiveDisabled(t *testing.T) {
	handler := createCookieHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	e := WithConfig(Config{
		BaseURL:  server.URL,
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Jar: nil,
		},
	})

	testCookieHandler(e, false)
}

func TestExpecCookieHandlerLiveEnabled(t *testing.T) {
	handler := createCookieHandler()

	server := httptest.NewServer(handler)
	defer server.Close()

	e := WithConfig(Config{
		BaseURL:  server.URL,
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Jar: NewJar(),
		},
	})

	testCookieHandler(e, true)
}

func TestExpectCookieHandlerBinderStandardDisabled(t *testing.T) {
	handler := createCookieHandler()

	e := WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewBinder(handler),
			Jar:       nil,
		},
	})

	testCookieHandler(e, false)
}

func TestExpectCookieHandlerBinderStandardEnabled(t *testing.T) {
	handler := createCookieHandler()

	e := WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewBinder(handler),
			Jar:       NewJar(),
		},
	})

	testCookieHandler(e, true)
}

func TestExpectCookieHandlerBinderFastDisabled(t *testing.T) {
	handler := fasthttpadaptor.NewFastHTTPHandler(createCookieHandler())

	e := WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewFastBinder(handler),
			Jar:       nil,
		},
	})

	testCookieHandler(e, false)
}

func TestExpectCookieHandlerBinderFastEnabled(t *testing.T) {
	handler := fasthttpadaptor.NewFastHTTPHandler(createCookieHandler())

	e := WithConfig(Config{
		BaseURL:  "http://example.com",
		Reporter: NewAssertReporter(t),
		Client: &http.Client{
			Transport: NewFastBinder(handler),
			Jar:       NewJar(),
		},
	})

	testCookieHandler(e, true)
}

func TestExpectStaticFastBinder(t *testing.T) {
	tempdir, err := ioutil.TempDir("", "httpexpect")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempdir)

	if err := ioutil.WriteFile(
		path.Join(tempdir, "hello"), []byte("hello, world!"), 0666); err != nil {
		t.Fatal(err)
	}

	fs := &fasthttp.FS{
		Root: tempdir,
	}

	handler := fs.NewRequestHandler()

	e := WithConfig(Config{
		Client: &http.Client{
			Transport: NewFastBinder(handler),
			Jar:       NewJar(),
		},
		Reporter: NewAssertReporter(t),
		Printers: []Printer{
			NewDebugPrinter(t, true),
		},
	})

	e.GET("/hello").
		Expect().
		Status(http.StatusOK).
		Text().Equal("hello, world!")
}
