package http_response

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/testutil"

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

func setUpTestMux() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) {
		http.Redirect(w, req, "/good", http.StatusMovedPermanently)
	})
	mux.HandleFunc("/good", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "hit the good page!")
	})
	mux.HandleFunc("/jsonresponse", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "\"service_status\": \"up\", \"healthy\" : \"true\"")
	})
	mux.HandleFunc("/badredirect", func(w http.ResponseWriter, req *http.Request) {
		http.Redirect(w, req, "/badredirect", http.StatusMovedPermanently)
	})
	mux.HandleFunc("/mustbepostmethod", func(w http.ResponseWriter, req *http.Request) {
		if req.Method != "POST" {
			http.Error(w, "method wasn't post", http.StatusMethodNotAllowed)
			return
		}
		fmt.Fprintf(w, "used post correctly!")
	})
	mux.HandleFunc("/musthaveabody", func(w http.ResponseWriter, req *http.Request) {
		body, err := ioutil.ReadAll(req.Body)
		req.Body.Close()
		if err != nil {
			http.Error(w, "couldn't read request body", http.StatusBadRequest)
			return
		}
		if string(body) == "" {
			http.Error(w, "body was empty", http.StatusBadRequest)
			return
		}
		fmt.Fprintf(w, "sent a body!")
	})
	mux.HandleFunc("/twosecondnap", func(w http.ResponseWriter, req *http.Request) {
		time.Sleep(time.Second * 2)
		return
	})
	return mux
}

func TestHeaders(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cHeader := r.Header.Get("Content-Type")
		assert.Equal(t, "Hello", r.Host)
		assert.Equal(t, "application/json", cHeader)
		w.WriteHeader(http.StatusOK)
	}))
	defer ts.Close()

	h := &HTTPResponse{
		Address:         ts.URL,
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 2},
		Headers: map[string]string{
			"Content-Type": "application/json",
			"Host":         "Hello",
		},
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)
}

func TestFields(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:         ts.URL + "/good",
		Body:            "{ 'test': 'data'}",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}

	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)
	response_value, ok := acc.StringField("http_response", "result_type")
	require.True(t, ok)
	require.Equal(t, "success", response_value)
}

func TestRedirects(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:         ts.URL + "/redirect",
		Body:            "{ 'test': 'data'}",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)

	h = &HTTPResponse{
		Address:         ts.URL + "/badredirect",
		Body:            "{ 'test': 'data'}",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	acc = testutil.Accumulator{}
	err = h.Gather(&acc)
	require.NoError(t, err)

	value, ok = acc.IntField("http_response", "http_response_code")
	require.False(t, ok)
	response_value, ok := acc.StringField("http_response", "result_type")
	require.True(t, ok)
	require.Equal(t, "connection_failed", response_value)
}

func TestMethod(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:         ts.URL + "/mustbepostmethod",
		Body:            "{ 'test': 'data'}",
		Method:          "POST",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)

	h = &HTTPResponse{
		Address:         ts.URL + "/mustbepostmethod",
		Body:            "{ 'test': 'data'}",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	acc = testutil.Accumulator{}
	err = h.Gather(&acc)
	require.NoError(t, err)

	value, ok = acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusMethodNotAllowed, value)

	//check that lowercase methods work correctly
	h = &HTTPResponse{
		Address:         ts.URL + "/mustbepostmethod",
		Body:            "{ 'test': 'data'}",
		Method:          "head",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	acc = testutil.Accumulator{}
	err = h.Gather(&acc)
	require.NoError(t, err)

	value, ok = acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusMethodNotAllowed, value)
}

func TestBody(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:         ts.URL + "/musthaveabody",
		Body:            "{ 'test': 'data'}",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)

	h = &HTTPResponse{
		Address:         ts.URL + "/musthaveabody",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	acc = testutil.Accumulator{}
	err = h.Gather(&acc)
	require.NoError(t, err)

	value, ok = acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusBadRequest, value)
}

func TestStringMatch(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:             ts.URL + "/good",
		Body:                "{ 'test': 'data'}",
		Method:              "GET",
		ResponseStringMatch: "hit the good page",
		ResponseTimeout:     internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)
	value, ok = acc.IntField("http_response", "response_string_match")
	require.True(t, ok)
	require.Equal(t, 1, value)
	response_value, ok := acc.StringField("http_response", "result_type")
	require.True(t, ok)
	require.Equal(t, "success", response_value)
	_, ok = acc.FloatField("http_response", "response_time")
	require.True(t, ok)
}

func TestStringMatchJson(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:             ts.URL + "/jsonresponse",
		Body:                "{ 'test': 'data'}",
		Method:              "GET",
		ResponseStringMatch: "\"service_status\": \"up\"",
		ResponseTimeout:     internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)
	value, ok = acc.IntField("http_response", "response_string_match")
	require.True(t, ok)
	require.Equal(t, 1, value)
	response_value, ok := acc.StringField("http_response", "result_type")
	require.True(t, ok)
	require.Equal(t, "success", response_value)
	_, ok = acc.FloatField("http_response", "response_time")
	require.True(t, ok)
}

func TestStringMatchFail(t *testing.T) {
	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:             ts.URL + "/good",
		Body:                "{ 'test': 'data'}",
		Method:              "GET",
		ResponseStringMatch: "hit the bad page",
		ResponseTimeout:     internal.Duration{Duration: time.Second * 20},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}

	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	value, ok := acc.IntField("http_response", "http_response_code")
	require.True(t, ok)
	require.Equal(t, http.StatusOK, value)
	value, ok = acc.IntField("http_response", "response_string_match")
	require.True(t, ok)
	require.Equal(t, 0, value)
	response_value, ok := acc.StringField("http_response", "result_type")
	require.True(t, ok)
	require.Equal(t, "response_string_mismatch", response_value)
	_, ok = acc.FloatField("http_response", "response_time")
	require.True(t, ok)
}

func TestTimeout(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping test with sleep in short mode.")
	}

	mux := setUpTestMux()
	ts := httptest.NewServer(mux)
	defer ts.Close()

	h := &HTTPResponse{
		Address:         ts.URL + "/twosecondnap",
		Body:            "{ 'test': 'data'}",
		Method:          "GET",
		ResponseTimeout: internal.Duration{Duration: time.Second},
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		FollowRedirects: true,
	}
	var acc testutil.Accumulator
	err := h.Gather(&acc)
	require.NoError(t, err)

	_, ok := acc.IntField("http_response", "http_response_code")
	require.False(t, ok)
	response_value, ok := acc.StringField("http_response", "result_type")
	require.True(t, ok)
	require.Equal(t, "timeout", response_value)
	_, ok = acc.FloatField("http_response", "response_time")
	require.False(t, ok)
}
