package twitchclient_test

import (
	"context"
	"errors"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"code.justin.tv/foundation/twitchclient"

	"github.com/stretchr/testify/require"
)

func TestNewJSONClient(t *testing.T) {
	fc := fakeHTTPClient{}
	c := twitchclient.NewJSONClient("http://fake.com", &fc)
	require.NotNil(t, c)
}

func TestPathf(t *testing.T) {
	require.Equal(t, "/foo/123/bar", twitchclient.Pathf("/foo/%d/bar", 123))
	require.Equal(t, "user/an%20id", twitchclient.Pathf("user/%s", "an id"))
	require.Equal(t, "with%20space/99/with%2Fslash",
		twitchclient.Pathf("%s/%d/%s", "with space", 99, "with/slash"), "escapes spaces and slashes in url params")
}

func TestJSONClient_Get_ValidJSON(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"foo": "bar"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	ctx := context.WithValue(context.Background(), "foo", "bar")
	path := twitchclient.Pathf("foo/bar")
	q := url.Values{"q": {"la la la"}}
	type FooBar struct {
		Foo string `json:"foo"`
	}

	resp := FooBar{}
	err := c.Get(ctx, path, q, &resp)

	require.NoError(t, err)
	require.Equal(t, resp.Foo, "bar") // resp properly serialized

	require.Equal(t, "GET", fc.req.Method)              // is a GET request
	require.Equal(t, "fake.com", fc.req.URL.Host)       // host properly parsed
	require.Equal(t, "/api/foo/bar", fc.req.URL.Path)   // path properly parsed
	require.Equal(t, "q=la+la+la", fc.req.URL.RawQuery) // query string properly escaped
	require.Equal(t, "application/json", fc.req.Header.Get("Content-Type"))
	require.Equal(t, "bar", fc.req.Context().Value("foo").(string)) // passed in the right context
	require.Nil(t, fc.req.Body)
}

func TestJSONClient_Get_WithEmptyQueryString(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"foo": "bar"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	type FooBar struct {
		Foo string `json:"foo"`
	}
	resp := FooBar{}
	err := c.Get(context.Background(), "/", nil, &resp)

	require.NoError(t, err)
	require.Equal(t, "bar", resp.Foo)
	require.Equal(t, "", fc.req.URL.RawQuery)
}

func TestJSONClient_Get_IgnoringResponseBody(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"foo": "bar"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	err := c.Get(context.Background(), "/", nil, nil)
	require.NoError(t, err)
}

func TestJSONClient_Get_InvalidJSONResponse(t *testing.T) {
	fc := fakeHTTPClient{respBody: `invalidjson`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	err := c.Get(context.Background(), "/", nil, &struct{}{})
	require.EqualError(t, err, "Unable to read response body: invalid character 'i' looking for beginning of value")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

func TestJSONClient_Get_NetworkError(t *testing.T) {
	fc := fakeHTTPClient{err: errors.New("broken pipe")}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	err := c.Get(context.Background(), "/", nil, &struct{}{})
	require.EqualError(t, err, "broken pipe")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

func TestJSONClient_Get_4xxResponse(t *testing.T) {
	fc := fakeHTTPClient{status: 400, respBody: `{"message": "you did something wrong"}`}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	resp := struct{}{}
	err := c.Get(context.Background(), "/failhere", nil, &resp)
	require.EqualError(t, err, "400: you did something wrong")
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "you did something wrong", twithErr.Message)
	require.Equal(t, 400, twithErr.StatusCode)
}

func TestJSONClient_Get_5xxResponse(t *testing.T) {
	fc := fakeHTTPClient{status: 500, respBody: `{"message": "we did something wrong"}`}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	resp := struct{}{}
	err := c.Get(context.Background(), "/failhere", nil, &resp)
	require.EqualError(t, err, "500: we did something wrong")
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "we did something wrong", twithErr.Message)
	require.Equal(t, 500, twithErr.StatusCode)
}

func TestJSONClient_Get_5xxResponseWithTextPlain(t *testing.T) {
	fc := fakeHTTPClient{status: 500, respBody: `yolo`, respContentType: "text/plain"}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	resp := struct{}{}
	err := c.Get(context.Background(), "/failhere", nil, &resp)
	require.EqualError(t, err, "500: yolo")
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "yolo", twithErr.Message)
	require.Equal(t, 500, twithErr.StatusCode)
}

func TestJSONClient_Post_ValidJSON(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"number": 999}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	ctx := context.WithValue(context.Background(), "foo", "bar")
	type ReqType struct {
		Str string `json:"str"`
	}
	type RespType struct {
		Number int `json:"number"`
	}

	resp := RespType{}
	err := c.Post(ctx, "mystuff", ReqType{Str: "hello world"}, &resp)

	require.NoError(t, err)
	require.Equal(t, 999, resp.Number) // resp properly serialized

	reqBodyBytes, err := ioutil.ReadAll(fc.req.Body)
	require.NoError(t, err)

	require.Equal(t, "POST", fc.req.Method)                         // is a POST request
	require.Equal(t, "fake.com", fc.req.URL.Host)                   // host properly parsed
	require.Equal(t, "/api/mystuff", fc.req.URL.Path)               // path properly parsed
	require.Equal(t, "", fc.req.URL.RawQuery)                       // no query string
	require.Equal(t, `{"str":"hello world"}`, string(reqBodyBytes)) // request body is JSON
	require.Equal(t, "application/json", fc.req.Header.Get("Content-Type"))
	require.Equal(t, "bar", fc.req.Context().Value("foo").(string)) // passed in the right context
}

func TestJSONClient_Post_WithEmptyRequestBody(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"foo": "bar"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	type FooBar struct {
		Foo string `json:"foo"`
	}
	resp := FooBar{}
	err := c.Post(context.Background(), "/", nil, &resp)

	require.NoError(t, err)
	require.Equal(t, "bar", resp.Foo)
	require.Nil(t, fc.req.Body, "request body is nil")
}

func TestJSONClient_Post_IgnoringResponseBody(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"foo": "bar"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	err := c.Post(context.Background(), "/", nil, nil)
	require.NoError(t, err)
}

func TestJSONClient_Post_InvalidJSONResponse(t *testing.T) {
	fc := fakeHTTPClient{respBody: `invalidjson`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	err := c.Post(context.Background(), "/", nil, &struct{}{})
	require.EqualError(t, err, "Unable to read response body: invalid character 'i' looking for beginning of value")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

func TestJSONClient_Post_NetworkError(t *testing.T) {
	fc := fakeHTTPClient{err: errors.New("broken pipe")}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	err := c.Post(context.Background(), "/", nil, &struct{}{})
	require.EqualError(t, err, "broken pipe")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

func TestJSONClient_Post_4xxResponse(t *testing.T) {
	fc := fakeHTTPClient{status: 400, respBody: `{"message": "you did something wrong"}`}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	resp := struct{}{}
	err := c.Post(context.Background(), "/failhere", nil, &resp)
	require.EqualError(t, err, "400: you did something wrong")
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "you did something wrong", twithErr.Message)
	require.Equal(t, 400, twithErr.StatusCode)
}

func TestJSONClient_Post_5xxResponse(t *testing.T) {
	fc := fakeHTTPClient{status: 500, respBody: `{"message": "we did something wrong"}`}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	resp := struct{}{}
	err := c.Post(context.Background(), "/failhere", nil, &resp)
	require.EqualError(t, err, "500: we did something wrong")
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "we did something wrong", twithErr.Message)
	require.Equal(t, 500, twithErr.StatusCode)
}

func TestJSONClient_Post_5xxResponseWithTextPlain(t *testing.T) {
	fc := fakeHTTPClient{status: 500, respBody: `yolo`, respContentType: "text/plain"}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	resp := struct{}{}
	err := c.Post(context.Background(), "/failhere", nil, &resp)
	require.EqualError(t, err, "500: yolo")
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "yolo", twithErr.Message)
	require.Equal(t, 500, twithErr.StatusCode)
}

func TestJSONClient_Put_ValidJSON(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"number": 999}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	ctx := context.WithValue(context.Background(), "foo", "bar")
	type ReqType struct {
		Str string `json:"str"`
	}
	type RespType struct {
		Number int `json:"number"`
	}

	resp := RespType{}
	err := c.Put(ctx, "mystuff", ReqType{Str: "hello world"}, &resp)

	require.NoError(t, err)
	require.Equal(t, 999, resp.Number) // resp properly serialized

	reqBodyBytes, err := ioutil.ReadAll(fc.req.Body)
	require.NoError(t, err)

	require.Equal(t, "PUT", fc.req.Method)                          // is a PUT request
	require.Equal(t, "fake.com", fc.req.URL.Host)                   // host properly parsed
	require.Equal(t, "/api/mystuff", fc.req.URL.Path)               // path properly parsed
	require.Equal(t, "", fc.req.URL.RawQuery)                       // no query string
	require.Equal(t, `{"str":"hello world"}`, string(reqBodyBytes)) // request body is JSON
	require.Equal(t, "application/json", fc.req.Header.Get("Content-Type"))
	require.Equal(t, "bar", fc.req.Context().Value("foo").(string)) // passed in the right context
}

func TestJSONClient_Put_IgnoringResponseBody(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"foo": "bar"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	err := c.Put(context.Background(), "/", nil, nil)
	require.NoError(t, err)
}

func TestJSONClient_Put_NetworkError(t *testing.T) {
	fc := fakeHTTPClient{err: errors.New("broken pipe")}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	err := c.Put(context.Background(), "/", nil, &struct{}{})
	require.EqualError(t, err, "broken pipe")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

func TestJSONClient_Delete(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"number": 999}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	ctx := context.WithValue(context.Background(), "foo", "bar")

	err := c.Delete(ctx, "mystuff")
	require.NoError(t, err)

	require.Equal(t, "DELETE", fc.req.Method)         // is a DELETE request
	require.Equal(t, "fake.com", fc.req.URL.Host)     // host properly parsed
	require.Equal(t, "/api/mystuff", fc.req.URL.Path) // path properly parsed
	require.Equal(t, "", fc.req.URL.RawQuery)         // no query string
	require.Nil(t, fc.req.Body)                       // request body is nil
	require.Equal(t, "application/json", fc.req.Header.Get("Content-Type"))
	require.Equal(t, "bar", fc.req.Context().Value("foo").(string)) // passed in the right context
}

func TestJSONClient_Delete_NetworkError(t *testing.T) {
	fc := fakeHTTPClient{err: errors.New("broken pipe")}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	err := c.Delete(context.Background(), "/")
	require.EqualError(t, err, "broken pipe")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

func TestJSONClient_Do_GetRequest(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{"id": 123, "name": "lirik"}`}
	c := twitchclient.NewJSONClient("http://fake.com/api/", &fc)

	ctx := context.WithValue(context.Background(), "foo", "bar")

	type User struct {
		ID   int
		Name string
	}
	resp := User{}
	httpResp, err := c.Do(ctx, twitchclient.JSONRequest{
		Method: "GET",
		Path:   twitchclient.Pathf("users/%s", "123"),
		Query:  url.Values{"fields": {"name"}},
		Header: http.Header{"X-My-Header": {"lul"}},
	}, &resp)

	require.NoError(t, err)
	require.Equal(t, 123, resp.ID) // resp properly serialized
	require.Equal(t, "lirik", resp.Name)

	require.NotNil(t, httpResp)
	require.Equal(t, 200, httpResp.StatusCode)

	require.Equal(t, "GET", fc.req.Method)                                  // is a GET request
	require.Equal(t, "fake.com", fc.req.URL.Host)                           // host properly parsed
	require.Equal(t, "/api/users/123", fc.req.URL.Path)                     // path properly parsed
	require.Equal(t, "fields=name", fc.req.URL.RawQuery)                    // query string
	require.Equal(t, "application/json", fc.req.Header.Get("Content-Type")) // json by default
	require.Equal(t, "lul", fc.req.Header.Get("X-My-Header"))               // custom header
	require.Equal(t, "bar", fc.req.Context().Value("foo").(string))         // passed in the right context
	require.Nil(t, fc.req.Body)
}

func TestJSONClient_Do_PostWithContentTypeQueryAndBody(t *testing.T) {
	fc := fakeHTTPClient{respBody: `{}`}
	c := twitchclient.NewJSONClient("http://fake.com", &fc)

	type Thing struct {
		Name string `json:"name"`
	}

	resp := struct{}{}
	httpResp, err := c.Do(context.Background(), twitchclient.JSONRequest{
		Method: "POST",
		Path:   twitchclient.Pathf("/things"),
		Query:  url.Values{"foo": {"bar", "baz"}},
		Header: http.Header{"X-My-Header": {"lul"}, "Content-Type": {"weird/stuff"}},
		Body:   Thing{Name: "Flux Capacitor"},
	}, &resp)

	require.NoError(t, err)
	require.NotNil(t, httpResp)
	require.Equal(t, 200, httpResp.StatusCode)

	reqBodyBytes, err := ioutil.ReadAll(fc.req.Body)
	require.NoError(t, err)

	require.Equal(t, "POST", fc.req.Method)                            // is a POST request
	require.Equal(t, "fake.com", fc.req.URL.Host)                      // host properly parsed
	require.Equal(t, "/things", fc.req.URL.Path)                       // path properly parsed
	require.Equal(t, "foo=bar&foo=baz", fc.req.URL.RawQuery)           // query string can be added to POST requests
	require.Equal(t, "weird/stuff", fc.req.Header.Get("Content-Type")) // custom Content-Type if explicitly passed
	require.Equal(t, "lul", fc.req.Header.Get("X-My-Header"))          // custom header
	require.Equal(t, `{"name":"Flux Capacitor"}`, string(reqBodyBytes))
}

func TestJSONClient_Do_PatchReturning4xxError(t *testing.T) {
	fc := fakeHTTPClient{status: 404, respBody: `{"message": "Stuff Not Found"}`}
	c := twitchclient.NewJSONClient("http://fake.com", &fc)

	resp := struct{}{}
	httpResp, err := c.Do(context.Background(), twitchclient.JSONRequest{
		Method: "PATCH",
		Path:   twitchclient.Pathf("/thing/%s", "123"),
		Body:   struct{ Name string }{Name: "foo"},
	}, &resp)

	require.EqualError(t, err, "404: Stuff Not Found")
	require.NotNil(t, httpResp)
	require.Equal(t, 404, httpResp.StatusCode)
	twithErr, ok := err.(*twitchclient.Error)
	require.True(t, ok, "error is a *twitchclient.Error")
	require.Equal(t, "Stuff Not Found", twithErr.Message)
	require.Equal(t, 404, twithErr.StatusCode)
}

func TestJSONClient_Do_NetworkError(t *testing.T) {
	fc := fakeHTTPClient{err: errors.New("broken pipe")}
	c := twitchclient.NewJSONClient("http://fail.me", &fc)

	httpResp, err := c.Do(context.Background(), twitchclient.JSONRequest{
		Method: "GET",
		Path:   "stuff",
	}, nil)
	require.Nil(t, httpResp)
	require.EqualError(t, err, "broken pipe")
	_, ok := err.(*twitchclient.Error)
	require.False(t, ok, "error is NOT a *twitchclient.Error")
}

// ----------
// Test Utils
// ----------

type fakeHTTPClient struct {
	status          int    // response status, default 200.
	respBody        string // response body as string.
	respContentType string // response Content-Type header, default "application/json".
	err             error  // response HTTP error, default nil.

	req *http.Request // request used in the call to Do that can be used for inspection.
}

// Do makes a fake request that depends on the configured fake attrs.
func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) {
	c.req = req

	if c.err != nil {
		return nil, c.err
	}

	if c.status == 0 {
		c.status = 200
	}
	if c.respContentType == "" {
		c.respContentType = "application/json"
	}

	header := http.Header{"Content-Type": {c.respContentType}}
	body := ioutil.NopCloser(strings.NewReader(c.respBody))
	return &http.Response{StatusCode: c.status, Body: body, Header: header}, nil
}
