/*
 * Copyright 2018 American Express
 *
 * 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.
 */

package main

import (
	"encoding/hex"
	"fmt"
	"github.com/valyala/fasthttp"
	"io/ioutil"
	"os"
	"sync/atomic"
	"testing"
	"time"
)

type HTTPTestHandler struct {
	noRequestsReceived  uint32
	lastBodyReceived    string
	lastMethodReceived  string
	lastURIReceived     string
	lastHeadersReceived *fasthttp.RequestHeader
	lastTimestamp       int64
}

func (h *HTTPTestHandler) HandleRequest(ctx *fasthttp.RequestCtx) {
	atomic.AddUint32(&h.noRequestsReceived, 1)
	atomic.StoreInt64(&h.lastTimestamp, time.Now().Unix())
	h.lastBodyReceived = hex.EncodeToString(ctx.Request.Body())
	h.lastMethodReceived = string(ctx.Request.Header.Method())
	h.lastURIReceived = ctx.Request.URI().String()
	newHeader := fasthttp.RequestHeader{}
	ctx.Request.Header.CopyTo(&newHeader)
	h.lastHeadersReceived = &newHeader

}

func (h *HTTPTestHandler) reset() {
	h.noRequestsReceived = 0
	h.lastBodyReceived = ""
	h.lastMethodReceived = ""
	h.lastURIReceived = ""
	h.lastTimestamp = 0
}

var serverRunning = false
var internalHandlerRef *HTTPTestHandler
var port = "8888"

func startServer() *HTTPTestHandler {
	if !serverRunning {
		internalHandlerRef = &HTTPTestHandler{0, "", "", "", &fasthttp.RequestHeader{}, 0}
		serverRunning = true
		go func() {
			err := fasthttp.ListenAndServe(":"+port, internalHandlerRef.HandleRequest)
			if err != nil {
				fmt.Printf("Failed to bind on port %s. Make sure no other service is running on that port and restart the test\n", port)
				os.Exit(1)
			}
		}()
		return internalHandlerRef
	}
	internalHandlerRef.reset()
	return internalHandlerRef
}

func defaultConfig() Configuration {
	return Configuration{
		"",
		1,
		"",
		0,
		false,
		"GET",
		1,
		"",
		true,
		"http://localhost:" + port,
		0,
	}
}

func setupAndListen(config Configuration) *HTTPTestHandler {
	testHandler := startServer()
	// Give server enough time to spin
	time.Sleep(time.Duration(500) * time.Millisecond)
	// Create a baton instance with given config
	baton := &Baton{configuration: config, result: Result{}}
	baton.run()
	// Give server enough time to receive requests
	time.Sleep(time.Duration(500) * time.Millisecond)
	// Collect results from handler
	return testHandler
}

func TestRequestCount(t *testing.T) {
	noRequestsToSend := 10000

	config := defaultConfig()
	config.numberOfRequests = noRequestsToSend
	testHandler := setupAndListen(config)

	reqsReceived := int(testHandler.noRequestsReceived)
	if reqsReceived != noRequestsToSend {
		t.Errorf("Wrong number of requests sent. Expected %d, got %d", noRequestsToSend, reqsReceived)
	}
}

func TestRequestCountWithMoreWorkers(t *testing.T) {
	noRequestsToSend := 100000

	config := defaultConfig()
	config.concurrency = 10
	config.numberOfRequests = noRequestsToSend
	testHandler := setupAndListen(config)

	reqsReceived := int(testHandler.noRequestsReceived)
	if reqsReceived != noRequestsToSend {
		t.Errorf("Wrong number of requests sent. Expected %d, got %d", noRequestsToSend, reqsReceived)
	}
}

func TestThatBodyHasCorrectValue(t *testing.T) {
	body := "Hello World"
	bodyBytes := hex.EncodeToString([]byte(body))

	config := defaultConfig()
	config.body = body
	config.method = "POST"
	testHandler := setupAndListen(config)

	bytesReceived := testHandler.lastBodyReceived
	if bytesReceived != bodyBytes {
		t.Errorf("The body received by the server didn't match. Expected %s, got %s", bodyBytes, bytesReceived)
	}
}

func TestThatTheCorrectHTTPMethodIsUsed(t *testing.T) {
	method := "DELETE"

	config := defaultConfig()
	config.method = method
	testHandler := setupAndListen(config)

	methodReceived := testHandler.lastMethodReceived
	if methodReceived != method {
		t.Errorf("The HTTP method received by the server didn't match. Expected %s, got %s", method, methodReceived)
	}
}

func TestThatServerReceivesCorrectURI(t *testing.T) {
	uri := "http://localhost:" + port + "/path/to/complex?uri&with=html&entities=&#ef"

	config := defaultConfig()
	config.url = uri
	testHandler := setupAndListen(config)

	uriReceived := testHandler.lastURIReceived
	if uriReceived != uri {
		t.Errorf("The URI received by the server does not match. Expected %s, got %s.", uri, uriReceived)
	}
}

func TestLoadPostFromTextFile(t *testing.T) {
	body := "Hello World"
	bodyBytes := hex.EncodeToString([]byte(body))

	config := defaultConfig()
	config.dataFilePath = "test-resources/post-body.txt"
	config.method = "POST"
	testHandler := setupAndListen(config)

	bytesReceived := testHandler.lastBodyReceived
	if bytesReceived != bodyBytes {
		t.Errorf("The body received by the server didn't match. Expected %s, got %s", bodyBytes, bytesReceived)
	}
}

func TestPostRequestLoadedFromFile(t *testing.T) {
	uri := "http://localhost:" + port
	method := "POST"
	fileContents := method + "," + uri + "," + "Data"
	fileInBytes := []byte(fileContents)

	fileDir := "test-resources/requests-from-file.txt"
	if ioutil.WriteFile(fileDir, fileInBytes, 0644) != nil {
		t.Errorf("Failed to write a required test case file. Check the directory permissions.")
	}
	defer os.Remove(fileDir)

	config := defaultConfig()
	config.requestsFromFile = fileDir
	config.numberOfRequests = 2
	testHandler := setupAndListen(config)

	postBodyInBytes := hex.EncodeToString([]byte("Data"))
	methodReceived := testHandler.lastMethodReceived
	bodyReceived := testHandler.lastBodyReceived

	if methodReceived != method {
		t.Errorf("The HTTP method received by the server didn't match. Expected %s, got %s", method, methodReceived)
	}

	if bodyReceived != postBodyInBytes {
		t.Errorf("The body received by the server didn't match. Expected %s, got %s", postBodyInBytes, bodyReceived)
	}
}

func TestThatHeadersAreSetWhenSendingFromFile(t *testing.T) {
	uri := "http://localhost:" + port
	method := "GET"
	fileContents := method + "," + uri + "," + "" + "," + "Content-Type: Hello, Secret: World"
	fileInBytes := []byte(fileContents)

	fileDir := "test-resources/requests-from-file.txt"
	if ioutil.WriteFile(fileDir, fileInBytes, 0644) != nil {
		t.Errorf("Failed to write a required test case file. Check the directory permissions.")
	}
	defer os.Remove(fileDir)

	config := defaultConfig()
	config.requestsFromFile = fileDir
	config.numberOfRequests = 1
	testHandler := setupAndListen(config)

	headerActual := hex.EncodeToString(testHandler.lastHeadersReceived.Peek("Content-Type"))
	headerExpected := hex.EncodeToString([]byte("Hello"))
	if headerExpected != headerActual {
		t.Errorf("Header not found or improperly set, Expected %s, got %s", headerExpected, headerActual)
	}

	headerActual2 := hex.EncodeToString(testHandler.lastHeadersReceived.Peek("Secret"))
	headerExpected2 := hex.EncodeToString([]byte("World"))
	if headerExpected != headerActual {
		t.Errorf("Header not found or improperly set, Expected %s, got %s", headerExpected2, headerActual2)
	}
}

func TestThatTimeOptionRunsForCorrectAmountOfTime(t *testing.T) {
	duration := 10
	testHandler := startServer()
	timeNow := time.Now().Unix()

	config := defaultConfig()
	config.duration = duration

	baton := &Baton{configuration: config, result: Result{}}
	baton.run()

	time.Sleep(time.Duration(15) * time.Second)
	lastTimeStamp := testHandler.lastTimestamp
	epsilonLow := int64(duration - 1)
	epsilonHigh := int64(duration + 1)
	diff := lastTimeStamp - timeNow
	if diff < epsilonLow || diff > epsilonHigh {
		t.Errorf("Requests sent for longer/shorter than expected. Expected %d, got %d)", duration, diff)
	}
}
