package parse_test

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"os"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"

	"git.makaay.nl/mauricem/go-toml/parse"
)

func Test_Invalid(t *testing.T) {
	for _, suite := range getTestSuites("invalid") {
		success := 0
		fail := 0
		for name, testCase := range getTestCasesForSuite(suite) {
			reader, err := os.Open(testCase.toml)
			if err != nil {
				panic(fmt.Sprintf("Cannot read file (%s): %s", testCase.toml, err))
			}
			if _, err = parse.Run(reader); err == nil {
				t.Errorf("[%s] expected a parse error, but got none", name)
				fail++
			} else {
				success++
			}
		}
		t.Logf("Suite %s: successful = %d, failed = %d", suite.name, success, fail)
	}
}

func Test_Valid(t *testing.T) {
	for _, suite := range getTestSuites("valid") {
		success := 0
		fail := 0
		for name, testCase := range getTestCasesForSuite(suite) {
			input, err := os.Open(testCase.toml)
			if err != nil {
				panic(fmt.Sprintf("Cannot open toml file for test (%v): %s", testCase, err))
			}
			tomlTable, err := parse.RunWithoutSanityChecks(input)
			if err != nil {
				t.Errorf("[%s] parse failed unexpectedly: %s", name, err)
				fail++
				continue
			}
			jsonfh, err := os.Open(testCase.json)
			if err != nil {
				t.Errorf("[%s] Could not open JSON file (%s): %s", name, testCase.json, err)
				fail++
				continue
			}
			var expected interface{}
			if err := json.NewDecoder(jsonfh).Decode(&expected); err != nil {
				t.Errorf("[%s] Could not read JSON from (%s): %s", name, testCase.json, err)
				fail++
				continue
			}
			var actual interface{}
			if err := json.Unmarshal([]byte(tomlTable.MakeSushi()), &actual); err != nil {
				t.Errorf("[%s] Could not convert parser output to BurntSushi format: %s", name, err)
				fail++
				continue
			}
			if err := cmpJson("", expected, actual); err != nil {
				t.Errorf("[%s] Expected result does not match the actual result: %s", name, err)
				fail++
				continue
			}
			success++
		}
		t.Logf("Suite %s: successful = %d, failed = %d", suite.name, success, fail)
	}
}

type testSuite struct {
	tesactualType string
	name          string
	dir           string
}

func getTestSuites(tesactualType string) []testSuite {
	dir := getTestfilesDir() + "/" + tesactualType
	entries, err := ioutil.ReadDir(dir)
	if err != nil {
		log.Fatalf("Cannot read directory (%s): %s", dir, err)
	}
	suites := make([]testSuite, len(entries))
	for i, ent := range entries {
		suites[i] = testSuite{tesactualType, ent.Name(), dir + "/" + ent.Name()}
	}
	return suites
}

func getTestfilesDir() string {
	_, filename, _, _ := runtime.Caller(0)
	return strings.Replace(filename, "_test.go", "", 1)
}

type testCase struct {
	toml string
	json string
}

func getTestCasesForSuite(suite testSuite) map[string]*testCase {
	entries, err := ioutil.ReadDir(suite.dir)
	if err != nil {
		log.Fatalf("Cannot read directory (%s): %s", suite.dir, err)
	}
	testCases := make(map[string]*testCase)
	for _, ent := range entries {
		name := ent.Name()
		id := name[0 : len(name)-5]
		key := suite.tesactualType + "/" + suite.name + "/" + id
		c, ok := testCases[key]
		if !ok {
			c = &testCase{}
			testCases[key] = c
		}
		switch {
		case strings.HasSuffix(name, ".toml"):
			c.toml = suite.dir + "/" + name
		case strings.HasSuffix(name, ".json"):
			c.json = suite.dir + "/" + name
		default:
			panic("Invalid test suite file found: " + suite.dir + "/" + name)
		}
	}
	return testCases
}

// ---------------------------------------------------------------------------
// Inspired by https://github.com/BurntSushi/toml-test/blob/master/json.go
// I started out by using reflect.DeepEqual, but that solution does not
// take possibly different ordering of object keys into account.
// I do a lot less active checking on type assertions here, since this
// is only used for my local test sets and local parser code. I can live
// with a panic when I crap up my test data.
// ---------------------------------------------------------------------------

func cmpJson(key string, expected interface{}, actual interface{}) error {
	switch expected := expected.(type) {
	case map[string]interface{}:
		return cmpJsonObjects(key, expected, actual)
	case []interface{}:
		return cmpJsonArrays(key, expected, actual)
	default:
		return fmt.Errorf("Key '%s' in expected output should be a map or a list of maps, but it's a %T.", key, expected)
	}
}

func cmpJsonObjects(key string, expected map[string]interface{}, a interface{}) error {
	actual, _ := a.(map[string]interface{})

	// Check to make sure both or neither are values.
	eIsVal := isValue(expected)
	aIsVal := isValue(actual)
	if eIsVal && !aIsVal {
		return fmt.Errorf("Key '%s' is supposed to be a value, but the parser reports it as a table.", key)
	}
	if !eIsVal && aIsVal {
		return fmt.Errorf("Key '%s' is supposed to be a table, but the parser reports it as a value.", key)
	}
	if eIsVal && aIsVal {
		return cmpJsonValues(key, expected, actual)
	}

	// We've got two maps. Check if the keys are equivalent (order does not match).
	for k, _ := range expected {
		if _, ok := actual[k]; !ok {
			return fmt.Errorf("Could not find key '%s.%s' in parser output.", key, k)
		}
	}
	for k, _ := range actual {
		if _, ok := expected[k]; !ok {
			return fmt.Errorf("Could not find key '%s.%s' expected output.", key, k)
		}
	}

	// Check if the values are equivalent.
	for k, _ := range expected {
		subKey := k
		if key != "" {
			subKey = key + "." + k
		}
		if err := cmpJson(subKey, expected[k], actual[k]); err != nil {
			return err
		}
	}
	return nil
}

func cmpJsonArrays(key string, expected, actual interface{}) error {
	expectedArray, _ := expected.([]interface{})
	actualArray, _ := actual.([]interface{})
	if len(expectedArray) != len(actualArray) {
		return fmt.Errorf("Array lengths differ for key '%s'. Expected a length of %d but got %d.",
			key, len(expectedArray), len(actualArray))
	}
	for i := 0; i < len(expectedArray); i++ {
		if err := cmpJson(key, expectedArray[i], actualArray[i]); err != nil {
			return err
		}
	}
	return nil
}

func cmpJsonValues(key string, expected, actual map[string]interface{}) error {
	expectedType, _ := expected["type"].(string)
	actualType, _ := actual["type"].(string)
	if expectedType != actualType {
		return fmt.Errorf("Type mismatch for key '%s'. Expected %s but got %s.", key, expectedType, actualType)
	}

	if expectedType == "array" {
		return cmpJsonArrays(key, expected["value"], actual["value"])
	}

	// Except for floats and datetimes, other values can be compared as strings.
	expectedValue, _ := expected["value"].(string)
	actualValue, _ := actual["value"].(string)
	switch expectedType {
	case "float":
		return cmpFloats(key, expectedValue, actualValue)
	case "datetime":
		return cmpAsDatetimes(key, expectedValue, actualValue)
	default:
		return cmpAsStrings(key, expectedValue, actualValue)
	}
}

func cmpAsStrings(key, expected, actual string) error {
	if expected != actual {
		return fmt.Errorf("Values for key '%s' don't match. Expected a value of %q but got %q.", key, expected, actual)
	}
	return nil
}

func cmpFloats(key, expected, actual string) error {
	expectedFloat, err := strconv.ParseFloat(expected, 64)
	if err != nil {
		return fmt.Errorf("BUG in test case. Could not read '%s' as a float value for key '%s'.", expected, key)
	}

	actualFloat, err := strconv.ParseFloat(actual, 64)
	if err != nil {
		return fmt.Errorf("Malformed parser output. Could not read '%s' as a float value for key '%s'.", actual, key)
	}
	if expectedFloat != actualFloat && !(math.IsNaN(expectedFloat) && math.IsNaN(actualFloat)) {
		return fmt.Errorf("Values for key '%s' don't match. Expected a value of %v but got %v.", key, expectedFloat, actualFloat)
	}
	return nil
}

func cmpAsDatetimes(key, expected, actual string) error {
	expectedTime, err := time.Parse(time.RFC3339Nano, expected)
	if err != nil {
		return fmt.Errorf("BUG in test case. Could not read '%s' as a datetime value for key '%s'.", expected, key)
	}

	actualTime, err := time.Parse(time.RFC3339Nano, actual)
	if err != nil {
		return fmt.Errorf("Malformed parser output. Could not read '%s' as datetime value for key '%s'.", actual, key)
	}
	if !expectedTime.Equal(actualTime) {
		return fmt.Errorf("Values for key '%s' don't match. Expected a value of '%v' but got '%v'.", key, expectedTime, actualTime)
	}
	return nil
}

func isValue(m map[string]interface{}) bool {
	if len(m) != 2 {
		return false
	}
	if _, ok := m["type"]; !ok {
		return false
	}
	if _, ok := m["value"]; !ok {
		return false
	}
	return true
}
