/*
 * 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 ac

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

	"devt.de/krotik/common/datautil"
	"devt.de/krotik/common/errorutil"
	"devt.de/krotik/common/httputil"
	"devt.de/krotik/common/httputil/access"
	"devt.de/krotik/common/httputil/auth"
	"devt.de/krotik/common/stringutil"
	"devt.de/krotik/eliasdb/api"
)

const TESTPORT = ":9090"

// Main function for all tests in this package

func TestMain(m *testing.M) {
	var err error

	flag.Parse()

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

	// Disable access logging

	LogAccess = func(v ...interface{}) {}

	// Register public endpoints

	api.RegisterRestEndpoints(PublicAccessControlEndpointMap)

	// Initialise auth handler

	AuthHandler = auth.NewCookieAuthHandleFuncWrapper(http.HandleFunc)

	// Important statement! - all registered endpoints afterwards
	// are subject to access control

	api.HandleFunc = AuthHandler.HandleFunc

	// Register management endpoints

	api.RegisterRestEndpoints(AccessManagementEndpointMap)

	// Register dummy page

	api.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("foobar!"))
	})

	// Initialise user DB

	UserDB, err = datautil.NewEnforcedUserDB("test_user.db", "")
	errorutil.AssertOk(err)

	// Put the UserDB in charge of verifying passwords

	AuthHandler.SetAuthFunc(UserDB.CheckUserPassword)

	// Initialise ACL's

	var conf map[string]interface{}

	errorutil.AssertOk(json.Unmarshal(stringutil.StripCStyleComments(DefaultAccessDB), &conf))
	at, err := access.NewMemoryACLTableFromConfig(conf)
	errorutil.AssertOk(err)
	InitACLs(at)

	// Connect the ACL object to the AuthHandler - this provides authorization for users

	AuthHandler.SetAccessFunc(ACL.CheckHTTPRequest)

	// Adding special handlers which redirect to the login page

	AuthHandler.CallbackSessionExpired = CallbackSessionExpired
	AuthHandler.CallbackUnauthorized = CallbackUnauthorized

	// Add users

	UserDB.UserDB.AddUserEntry("elias", "elias", nil)
	UserDB.UserDB.AddUserEntry("johndoe", "doe", nil)
	UserDB.UserDB.AddUserEntry("guest", "g", nil)

	// Disable debounce time for unit tests

	DebounceTime = 0

	// Run the tests

	res := m.Run()

	// Teardown

	stopServer(hs, wg)

	// Stop ACL monitoring

	ACL.Close()

	// Remove files

	os.Remove("test_user.db")

	os.Exit(res)
}

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 PublicAccessControlEndpointMap {
		inst().SwaggerDefs(data)
	}
	for _, inst := range AccessManagementEndpointMap {
		inst().SwaggerDefs(data)
	}
}

/*
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")
	}
}

/*
Send a request to a HTTP test server
*/
func sendTestRequest(contentType string, url string, method string, content []byte,
	reqMod func(*http.Request)) string {

	body, _ := sendTestRequestResponse(contentType, url, method, content, reqMod)

	return body
}

/*
Send a request to a HTTP test server
*/
func sendTestRequestResponse(contentType string, url string, method string,
	content []byte, reqMod func(*http.Request)) (string, *http.Response) {

	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", contentType)

	if reqMod != nil {
		reqMod(req)
	}

	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 out.String(), resp
	}

	// Just return the body

	return bodyStr, resp
}

/*
Perform authentication and retrieve an auth cookie
*/
func doAuth(user, pass string) *http.Cookie {
	queryURL := "http://localhost" + TESTPORT

	// Send authentication request with correct credentials

	res, resp := sendTestRequestResponse("application/json", queryURL+EndpointLogin, "POST", []byte(`
{
	"user" : "`+user+`",
	"pass" : "`+pass+`"
}
`), nil)

	errorutil.AssertTrue(len(resp.Cookies()) > 0, res)

	// Right after authentication we only have the authentication cookie - after
	// the first visit to a non-public page we will also have a session cookie

	authCookie := resp.Cookies()[0]

	errorutil.AssertTrue(authCookie.Name == "~aid",
		fmt.Sprint("Unexpected name for cookie:", authCookie))

	return authCookie
}
