/*
 * EliasDB
 *
 * Copyright 2016 Matthias Ladkau. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package v1

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"testing"

	"devt.de/krotik/common/errorutil"
	"devt.de/krotik/common/fileutil"
	"devt.de/krotik/common/httputil"
	"devt.de/krotik/eliasdb/api"
	"devt.de/krotik/eliasdb/config"
	"devt.de/krotik/eliasdb/ecal"
	"devt.de/krotik/eliasdb/eql"
	"devt.de/krotik/eliasdb/graph"
	"devt.de/krotik/eliasdb/graph/data"
	"devt.de/krotik/eliasdb/graph/graphstorage"
)

const TESTPORT = ":9090"

var gmMSM *graphstorage.MemoryGraphStorage

const testScriptDir = "testscripts"

// Main function for all tests in this package

func TestMain(m *testing.M) {
	flag.Parse()

	defer func() {
		if res, _ := fileutil.PathExists(testScriptDir); res {
			if err := os.RemoveAll(testScriptDir); err != nil {
				fmt.Print("Could not remove test directory:", err.Error())
			}
		}
	}()

	if res, _ := fileutil.PathExists(testScriptDir); res {
		if err := os.RemoveAll(testScriptDir); err != nil {
			fmt.Print("Could not remove test directory:", err.Error())
		}
	}

	ensurePath(testScriptDir)

	data := make(map[string]interface{})
	for k, v := range config.DefaultConfig {
		data[k] = v
	}

	config.Config = data

	config.Config[config.EnableECALScripts] = true
	config.Config[config.ECALScriptFolder] = testScriptDir
	config.Config[config.ECALLogFile] = filepath.Join(testScriptDir, "interpreter.log")

	gm, msm := filterGraph()
	api.GM = gm
	api.GS = msm
	gmMSM = msm

	resetSI()

	hs, wg := startServer()
	if hs == nil {
		return
	}

	// Register endpoints for version 1

	api.RegisterRestEndpoints(V1EndpointMap)
	api.RegisterRestEndpoints(V1PublicEndpointMap)

	// Run the tests

	m.Run()

	// Teardown

	stopServer(hs, wg)
}

func TestSwaggerDefs(t *testing.T) {

	// Test we can build swagger defs from the endpoint

	data := map[string]interface{}{
		"paths":       map[string]interface{}{},
		"definitions": map[string]interface{}{},
	}

	for _, inst := range V1EndpointMap {
		inst().SwaggerDefs(data)
	}
}

/*
Send a request to a HTTP test server
*/
func sendTestRequest(url string, method string, content []byte) (string, http.Header, string) {
	var req *http.Request
	var err error

	if content != nil {
		req, err = http.NewRequest(method, url, bytes.NewBuffer(content))
	} else {
		req, err = http.NewRequest(method, url, nil)
	}

	if err != nil {
		panic(err)
	}

	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	bodyStr := strings.Trim(string(body), " \n")

	// Try json decoding first

	out := bytes.Buffer{}
	err = json.Indent(&out, []byte(bodyStr), "", "  ")
	if err == nil {
		return resp.Status, resp.Header, out.String()
	}

	// Just return the body

	return resp.Status, resp.Header, bodyStr
}

/*
formatJSONString formats a given JSON string.
*/
func formatJSONString(str string) string {
	out := bytes.Buffer{}
	errorutil.AssertOk(json.Indent(&out, []byte(str), "", "  "))
	return out.String()
}

/*
formatData returns a given datastructure as JSON string.
*/
func formatData(data interface{}) string {
	actualResultBytes, _ := json.MarshalIndent(data, "", "  ")
	return string(actualResultBytes)
}

/*
Start a HTTP test server.
*/
func startServer() (*httputil.HTTPServer, *sync.WaitGroup) {
	hs := &httputil.HTTPServer{}

	var wg sync.WaitGroup
	wg.Add(1)

	go hs.RunHTTPServer(TESTPORT, &wg)

	wg.Wait()

	// Server is started

	if hs.LastError != nil {
		panic(hs.LastError)
	}

	return hs, &wg
}

/*
Stop a started HTTP test server.
*/
func stopServer(hs *httputil.HTTPServer, wg *sync.WaitGroup) {

	if hs.Running == true {

		wg.Add(1)

		// Server is shut down

		hs.Shutdown()

		wg.Wait()

	} else {

		panic("Server was not running as expected")
	}
}

func songGraph() (*graph.Manager, *graphstorage.MemoryGraphStorage) {

	mgs := graphstorage.NewMemoryGraphStorage("mystorage")
	gm := graph.NewGraphManager(mgs)

	constructEdge := func(key string, node1 data.Node, node2 data.Node, number int) data.Edge {
		edge := data.NewGraphEdge()

		edge.SetAttr("key", key)
		edge.SetAttr("kind", "Wrote")

		edge.SetAttr(data.EdgeEnd1Key, node1.Key())
		edge.SetAttr(data.EdgeEnd1Kind, node1.Kind())
		edge.SetAttr(data.EdgeEnd1Role, "Author")
		edge.SetAttr(data.EdgeEnd1Cascading, true)

		edge.SetAttr(data.EdgeEnd2Key, node2.Key())
		edge.SetAttr(data.EdgeEnd2Kind, node2.Kind())
		edge.SetAttr(data.EdgeEnd2Role, "Song")
		edge.SetAttr(data.EdgeEnd2Cascading, false)

		edge.SetAttr("number", number)

		return edge
	}

	storeSong := func(node data.Node, name string, ranking int, number int) {
		node3 := data.NewGraphNode()
		node3.SetAttr("key", name)
		node3.SetAttr("kind", "Song")
		node3.SetAttr("name", name)
		node3.SetAttr("ranking", ranking)
		gm.StoreNode("main", node3)
		gm.StoreEdge("main", constructEdge(name, node, node3, number))
	}

	node0 := data.NewGraphNode()
	node0.SetAttr("key", "000")
	node0.SetAttr("kind", "Author")
	node0.SetAttr("name", "John")
	node0.SetAttr("desc", "One of the most popular acoustic artists of the decade and one of its best-selling artists.")
	gm.StoreNode("main", node0)
	gm.StoreNode("test", node0)  // Same node but different partition
	gm.StoreNode("_test", node0) // Same node but different (hidden) partition

	storeSong(node0, "Aria1", 8, 1)
	storeSong(node0, "Aria2", 2, 2)
	storeSong(node0, "Aria3", 4, 3)
	storeSong(node0, "Aria4", 18, 4)

	node1 := data.NewGraphNode()
	node1.SetAttr("key", "123")
	node1.SetAttr("kind", "Author")
	node1.SetAttr("name", "Mike")
	gm.StoreNode("main", node1)

	storeSong(node1, "LoveSong3", 1, 3)
	storeSong(node1, "FightSong4", 3, 4)
	storeSong(node1, "DeadSong2", 6, 2)
	storeSong(node1, "StrangeSong1", 5, 1)

	node2 := data.NewGraphNode()
	node2.SetAttr("key", "456")
	node2.SetAttr("kind", "Author")
	node2.SetAttr("name", "Hans")
	gm.StoreNode("main", node2)

	storeSong(node2, "MyOnlySong3", 19, 3)

	// Create lots of spam nodes

	for i := 0; i < 21; i++ {
		nodespam := data.NewGraphNode()
		nodespam.SetAttr("key", "000"+strconv.Itoa(i))
		nodespam.SetAttr("kind", "Spam")
		nodespam.SetAttr("name", "Spam"+strconv.Itoa(i))
		gm.StoreNode("main", nodespam)
	}

	return gm, mgs.(*graphstorage.MemoryGraphStorage)
}

func songGraphGroups() (*graph.Manager, *graphstorage.MemoryGraphStorage) {
	gm, mgs := songGraph()

	node0 := data.NewGraphNode()
	node0.SetAttr("key", "Best")
	node0.SetAttr("kind", eql.GroupNodeKind)
	gm.StoreNode("main", node0)

	constructEdge := func(songkey string) data.Edge {
		edge := data.NewGraphEdge()

		edge.SetAttr("key", songkey)
		edge.SetAttr("kind", "Contains")

		edge.SetAttr(data.EdgeEnd1Key, node0.Key())
		edge.SetAttr(data.EdgeEnd1Kind, node0.Kind())
		edge.SetAttr(data.EdgeEnd1Role, "group")
		edge.SetAttr(data.EdgeEnd1Cascading, false)

		edge.SetAttr(data.EdgeEnd2Key, songkey)
		edge.SetAttr(data.EdgeEnd2Kind, "Song")
		edge.SetAttr(data.EdgeEnd2Role, "Song")
		edge.SetAttr(data.EdgeEnd2Cascading, false)

		return edge
	}

	gm.StoreEdge("main", constructEdge("LoveSong3"))
	gm.StoreEdge("main", constructEdge("Aria3"))
	gm.StoreEdge("main", constructEdge("MyOnlySong3"))
	gm.StoreEdge("main", constructEdge("StrangeSong1"))

	// Store additional groups

	node0 = data.NewGraphNode()
	node0.SetAttr("key", "foo")
	node0.SetAttr("kind", eql.GroupNodeKind)
	gm.StoreNode("main", node0)

	node0 = data.NewGraphNode()
	node0.SetAttr("key", "g1")
	node0.SetAttr("kind", eql.GroupNodeKind)
	gm.StoreNode("main", node0)

	node0 = data.NewGraphNode()
	node0.SetAttr("key", "g2")
	node0.SetAttr("kind", eql.GroupNodeKind)
	gm.StoreNode("main", node0)

	return gm, mgs
}

func filterGraph() (*graph.Manager, *graphstorage.MemoryGraphStorage) {
	gm, mgs := songGraphGroups()

	constructNode := func(key, val1, val2, val3 string) data.Node {
		node0 := data.NewGraphNode()
		node0.SetAttr("key", key)
		node0.SetAttr("kind", "filtertest")
		node0.SetAttr("val1", val1)
		node0.SetAttr("val2", val2)
		node0.SetAttr("val3", val3)

		return node0
	}

	gm.StoreNode("main", constructNode("1", "test", "Hans", "foo"))
	gm.StoreNode("main", constructNode("2", "test1", "Hans", "foo"))
	gm.StoreNode("main", constructNode("3", "test2", "Hans", "foo"))
	gm.StoreNode("main", constructNode("4", "test3", "Peter", "foo"))
	gm.StoreNode("main", constructNode("5", "test4", "Peter", "foo"))
	gm.StoreNode("main", constructNode("6", "test5", "Peter", "foo"))
	gm.StoreNode("main", constructNode("7", "test6", "Anna", "foo"))
	gm.StoreNode("main", constructNode("8", "test7", "Anna", "foo"))
	gm.StoreNode("main", constructNode("9", "test8", "Steve", "foo"))
	gm.StoreNode("main", constructNode("10", "test9", "Steve", "foo"))
	gm.StoreNode("main", constructNode("11", "test10", "Franz", "foo"))
	gm.StoreNode("main", constructNode("12", "test11", "Kevin", "foo"))
	gm.StoreNode("main", constructNode("13", "test12", "Kevin", "foo"))
	gm.StoreNode("main", constructNode("14", "test13", "Kevin", "foo"))
	gm.StoreNode("main", constructNode("15", "test14", "X1", "foo"))
	gm.StoreNode("main", constructNode("16", "test15", "X2", "foo"))
	gm.StoreNode("main", constructNode("17", "test16", "X3", "foo"))
	gm.StoreNode("main", constructNode("18", "test17", "X4", "foo"))
	gm.StoreNode("main", constructNode("19", "test18", "X5", "foo"))

	return gm, mgs
}

func ensurePath(path string) {
	if res, _ := fileutil.PathExists(path); !res {
		if err := os.Mkdir(path, 0770); err != nil {
			fmt.Print("Could not create directory:", err.Error())
			return
		}
	}
}

func resetSI() {
	api.SI = ecal.NewScriptingInterpreter(testScriptDir, api.GM)
}

func writeScript(content string) {
	filename := filepath.Join(testScriptDir, config.Str(config.ECALEntryScript))
	err := ioutil.WriteFile(
		filename,
		[]byte(content), 0600)
	errorutil.AssertOk(err)
	os.Remove(config.Str(config.ECALLogFile))
}

func checkLog(expected string) error {
	var err error

	content, err := ioutil.ReadFile(config.Str(config.ECALLogFile))

	if err == nil {
		logtext := string(content)

		if logtext != expected {
			err = fmt.Errorf("Unexpected log text:\n%v", logtext)
		}
	}
	return err
}
