package statuscake

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

func TestTest_Validate(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	test := &Test{
		Timeout:      200,
		Confirmation: 100,
		Public:       200,
		Virus:        200,
		TestType:     "FTP",
		RealBrowser:  100,
		TriggerRate:  100,
		CheckRate:    100000,
		CustomHeader: "here be dragons",
		WebsiteName:  "",
		WebsiteURL:   "",
	}

	err := test.Validate()
	require.NotNil(err)

	message := err.Error()
	assert.Contains(message, "WebsiteName is required")
	assert.Contains(message, "WebsiteURL is required")
	assert.Contains(message, "Timeout must be 0 or between 6 and 99")
	assert.Contains(message, "Confirmation must be between 0 and 9")
	assert.Contains(message, "CheckRate must be between 0 and 23999")
	assert.Contains(message, "Public must be 0 or 1")
	assert.Contains(message, "Virus must be 0 or 1")
	assert.Contains(message, "TestType must be HTTP, TCP, or PING")
	assert.Contains(message, "RealBrowser must be 0 or 1")
	assert.Contains(message, "TriggerRate must be between 0 and 59")
	assert.Contains(message, "CustomHeader must be provided as json string")

	test.Timeout = 10
	test.Confirmation = 2
	test.Public = 1
	test.Virus = 1
	test.TestType = "HTTP"
	test.RealBrowser = 1
	test.TriggerRate = 50
	test.CheckRate = 10
	test.WebsiteName = "Foo"
	test.WebsiteURL = "http://example.com"
	test.CustomHeader = `{"test": 15}`
	test.NodeLocations = []string{"foo", "bar"}

	err = test.Validate()
	assert.Nil(err)
}

func TestTest_ToURLValues(t *testing.T) {
	assert := assert.New(t)

	test := &Test{
		TestID:         123,
		Paused:         true,
		WebsiteName:    "Foo Bar",
		CustomHeader:   `{"some":{"json": ["value"]}}`,
		WebsiteURL:     "http://example.com",
		Port:           3000,
		NodeLocations:  []string{"foo", "bar"},
		Timeout:        11,
		PingURL:        "http://example.com/ping",
		Confirmation:   1,
		CheckRate:      500,
		BasicUser:      "myuser",
		BasicPass:      "mypass",
		Public:         1,
		LogoImage:      "http://example.com/logo.jpg",
		Branding:       1,
		WebsiteHost:    "hoster",
		Virus:          1,
		FindString:     "hello",
		DoNotFind:      true,
		TestType:       "HTTP",
		RealBrowser:    1,
		TriggerRate:    50,
		TestTags:       []string{"tag1", "tag2"},
		StatusCodes:    "500",
		EnableSSLAlert: false,
		FollowRedirect: false,
	}

	expected := url.Values{
		"TestID":         {"123"},
		"Paused":         {"1"},
		"WebsiteName":    {"Foo Bar"},
		"WebsiteURL":     {"http://example.com"},
		"CustomHeader":   {`{"some":{"json": ["value"]}}`},
		"Port":           {"3000"},
		"NodeLocations":  {"foo,bar"},
		"Timeout":        {"11"},
		"PingURL":        {"http://example.com/ping"},
		"ContactGroup":   {""},
		"Confirmation":   {"1"},
		"CheckRate":      {"500"},
		"BasicUser":      {"myuser"},
		"BasicPass":      {"mypass"},
		"Public":         {"1"},
		"LogoImage":      {"http://example.com/logo.jpg"},
		"Branding":       {"1"},
		"WebsiteHost":    {"hoster"},
		"Virus":          {"1"},
		"FindString":     {"hello"},
		"DoNotFind":      {"1"},
		"TestType":       {"HTTP"},
		"RealBrowser":    {"1"},
		"TriggerRate":    {"50"},
		"TestTags":       {"tag1,tag2"},
		"StatusCodes":    {"500"},
		"UseJar":         {"0"},
		"PostRaw":        {""},
		"FinalEndpoint":  {""},
		"EnableSSLAlert": {"0"},
		"FollowRedirect": {"0"},
	}

	assert.Equal(expected, test.ToURLValues())

	test.TestID = 0
	delete(expected, "TestID")

	assert.Equal(expected.Encode(), test.ToURLValues().Encode())

	test.StatusCodes = ""
	delete(expected, "StatusCodes")

	assert.Equal(expected.Encode(), test.ToURLValues().Encode())
}

func TestTests_All(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_all_ok.json",
	}
	tt := newTests(c)
	tests, err := tt.All()
	require.Nil(err)

	assert.Equal("/Tests", c.sentRequestPath)
	assert.Equal("GET", c.sentRequestMethod)
	assert.Nil(c.sentRequestValues)
	assert.Len(tests, 2)

	expectedTest := &Test{
		TestID:        100,
		Paused:        false,
		TestType:      "HTTP",
		WebsiteName:   "www 1",
		ContactGroup:  []string{"1"},
		Status:        "Up",
		Uptime:        100,
		NodeLocations: []string{"foo", "bar"},
	}
	assert.Equal(expectedTest, tests[0])

	expectedTest = &Test{
		TestID:        101,
		Paused:        true,
		TestType:      "HTTP",
		WebsiteName:   "www 2",
		ContactGroup:  []string{"2"},
		Status:        "Down",
		Uptime:        0,
		NodeLocations: []string{"foo"},
		TestTags:      []string{"test1", "test2"},
	}
	assert.Equal(expectedTest, tests[1])
}

func TestTests_AllWithFilter(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_all_ok.json",
	}

	v := url.Values{}
	v.Set("tags", "test1,test2")
	tt := newTests(c)
	tests, err := tt.AllWithFilter(v)
	require.Nil(err)

	assert.Equal("/Tests", c.sentRequestPath)
	assert.Equal("GET", c.sentRequestMethod)
	assert.NotNil(c.sentRequestValues)
	assert.Len(tests, 1)

	expectedTest := &Test{
		TestID:        101,
		Paused:        true,
		TestType:      "HTTP",
		WebsiteName:   "www 2",
		ContactGroup:  []string{"2"},
		Status:        "Down",
		Uptime:        0,
		NodeLocations: []string{"foo"},
		TestTags:      []string{"test1", "test2"},
	}
	assert.Equal(expectedTest, tests[0])
}

func TestTests_Update_OK(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_update_ok.json",
	}
	tt := newTests(c)

	test1 := &Test{
		WebsiteName: "foo",
	}

	test2, err := tt.Update(test1)
	require.Nil(err)

	assert.Equal("/Tests/Update", c.sentRequestPath)
	assert.Equal("PUT", c.sentRequestMethod)
	assert.NotNil(c.sentRequestValues)
	assert.NotNil(test2)

	assert.Equal(1234, test2.TestID)
}

func TestTests_Update_Error(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_update_error.json",
	}
	tt := newTests(c)

	test1 := &Test{
		WebsiteName: "foo",
	}

	test2, err := tt.Update(test1)
	assert.Nil(test2)

	require.NotNil(err)
	assert.IsType(&updateError{}, err)
	assert.Contains(err.Error(), "issue a")
}

func TestTests_Update_ErrorWithSliceOfIssues(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_update_error_slice_of_issues.json",
	}
	tt := newTests(c)

	test1 := &Test{
		WebsiteName: "foo",
	}

	test2, err := tt.Update(test1)
	assert.Nil(test2)

	require.NotNil(err)
	assert.IsType(&updateError{}, err)
	assert.Equal("Required Data is Missing., hello, world", err.Error())
}

func TestTests_Delete_OK(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_delete_ok.json",
	}
	tt := newTests(c)

	err := tt.Delete(1234)
	require.Nil(err)

	assert.Equal("/Tests/Details", c.sentRequestPath)
	assert.Equal("DELETE", c.sentRequestMethod)
	assert.Equal(url.Values{"TestID": {"1234"}}, c.sentRequestValues)
}

func TestTests_Delete_Error(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_delete_error.json",
	}
	tt := newTests(c)

	err := tt.Delete(1234)
	require.NotNil(err)
	assert.Equal("this is an error", err.Error())
}

func TestTests_Detail_OK(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	c := &fakeAPIClient{
		fixture: "tests_detail_ok.json",
	}
	tt := newTests(c)

	test, err := tt.Detail(1234)
	require.Nil(err)

	assert.Equal("/Tests/Details", c.sentRequestPath)
	assert.Equal("GET", c.sentRequestMethod)
	assert.Equal(url.Values{"TestID": {"1234"}}, c.sentRequestValues)

	assert.Equal(test.TestID, 6735)
	assert.Equal(test.TestType, "HTTP")
	assert.Equal(test.Paused, false)
	assert.Equal(test.WebsiteName, "NL")
	assert.Equal(test.CustomHeader, `{"some":{"json": ["value"]}}`)
	assert.Equal(test.UserAgent, "product/version (comment)")
	assert.Equal(test.ContactGroup, []string{"536"})
	assert.Equal(test.Status, "Up")
	assert.Equal(test.Uptime, 0.0)
	assert.Equal(test.CheckRate, 60)
	assert.Equal(test.Timeout, 40)
	assert.Equal(test.LogoImage, "")
	assert.Equal(test.WebsiteHost, "Various")
	assert.Equal(test.FindString, "")
	assert.Equal(test.DoNotFind, false)
	assert.Equal(test.NodeLocations, []string{"foo", "bar"})
}

type fakeAPIClient struct {
	sentRequestPath   string
	sentRequestMethod string
	sentRequestValues url.Values
	fixture           string
}

func (c *fakeAPIClient) put(path string, v url.Values) (*http.Response, error) {
	return c.all("PUT", path, v)
}

func (c *fakeAPIClient) delete(path string, v url.Values) (*http.Response, error) {
	return c.all("DELETE", path, v)
}

func (c *fakeAPIClient) get(path string, v url.Values) (*http.Response, error) {
	return c.all("GET", path, v)
}

func (c *fakeAPIClient) all(method string, path string, v url.Values) (*http.Response, error) {
	c.sentRequestMethod = method
	c.sentRequestPath = path
	c.sentRequestValues = v

	p := filepath.Join("fixtures", c.fixture)
	f, err := os.Open(p)
	if err != nil {
		log.Fatal(err)
	}

	var resp *http.Response
	if len(c.sentRequestValues.Get("tags")) > 0 {
		var storedResponses []Test
		var returnResponses []Test
		byteValue, _ := ioutil.ReadAll(f)
		json.Unmarshal(byteValue, &storedResponses)
		requestedTags := strings.Split(c.sentRequestValues.Get("tags"), ",")

		for _, storedResponse := range storedResponses {
			if len(requestedTags) > len(storedResponse.TestTags) { // if we are requesting more tags than whats stored then there are no matches
				continue
			}

			match := true
			for i, tag := range requestedTags {
				if tag != storedResponse.TestTags[i] {
					match = false
				}
			}

			if match { // we can add it to the response now
				returnResponses = append(returnResponses, storedResponse)
			}
		}

		if len(returnResponses) == 0 {
			return nil, nil
		}

		newByteValue, _ := json.Marshal(&returnResponses)
		resp = &http.Response{
			Body: ioutil.NopCloser(bytes.NewBuffer(newByteValue)),
		}

	} else {
		resp = &http.Response{
			Body: f,
		}
	}

	return resp, nil
}
