package query

import (
	"fmt"
	"io/ioutil"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/pelletier/go-toml"
)

type queryTestNode struct {
	value    interface{}
	position toml.Position
}

func valueString(root interface{}) string {
	result := "" //fmt.Sprintf("%T:", root)
	switch node := root.(type) {
	case *Result:
		items := []string{}
		for i, v := range node.Values() {
			items = append(items, fmt.Sprintf("%s:%s",
				node.Positions()[i].String(), valueString(v)))
		}
		sort.Strings(items)
		result = "[" + strings.Join(items, ", ") + "]"
	case queryTestNode:
		result = fmt.Sprintf("%s:%s",
			node.position.String(), valueString(node.value))
	case []interface{}:
		items := []string{}
		for _, v := range node {
			items = append(items, valueString(v))
		}
		sort.Strings(items)
		result = "[" + strings.Join(items, ", ") + "]"
	case *toml.Tree:
		// workaround for unreliable map key ordering
		items := []string{}
		for _, k := range node.Keys() {
			v := node.GetPath([]string{k})
			items = append(items, k+":"+valueString(v))
		}
		sort.Strings(items)
		result = "{" + strings.Join(items, ", ") + "}"
	case map[string]interface{}:
		// workaround for unreliable map key ordering
		items := []string{}
		for k, v := range node {
			items = append(items, k+":"+valueString(v))
		}
		sort.Strings(items)
		result = "{" + strings.Join(items, ", ") + "}"
	case int64:
		result += fmt.Sprintf("%d", node)
	case string:
		result += "'" + node + "'"
	case float64:
		result += fmt.Sprintf("%f", node)
	case bool:
		result += fmt.Sprintf("%t", node)
	case time.Time:
		result += fmt.Sprintf("'%v'", node)
	}
	return result
}

func assertValue(t *testing.T, result, ref interface{}) {
	pathStr := valueString(result)
	refStr := valueString(ref)
	if pathStr != refStr {
		t.Errorf("values do not match")
		t.Log("test:", pathStr)
		t.Log("ref: ", refStr)
	}
}

func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) {
	tree, err := toml.Load(tomlDoc)
	if err != nil {
		t.Errorf("Non-nil toml parse error: %v", err)
		return
	}
	q, err := Compile(query)
	if err != nil {
		t.Error(err)
		return
	}
	results := q.Execute(tree)
	assertValue(t, results, ref)
}

func TestQueryRoot(t *testing.T) {
	assertQueryPositions(t,
		"a = 42",
		"$",
		[]interface{}{
			queryTestNode{
				map[string]interface{}{
					"a": int64(42),
				}, toml.Position{1, 1},
			},
		})
}

func TestQueryKey(t *testing.T) {
	assertQueryPositions(t,
		"[foo]\na = 42",
		"$.foo.a",
		[]interface{}{
			queryTestNode{
				int64(42), toml.Position{2, 1},
			},
		})
}

func TestQueryKeyString(t *testing.T) {
	assertQueryPositions(t,
		"[foo]\na = 42",
		"$.foo['a']",
		[]interface{}{
			queryTestNode{
				int64(42), toml.Position{2, 1},
			},
		})
}

func TestQueryIndex(t *testing.T) {
	assertQueryPositions(t,
		"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
		"$.foo.a[5]",
		[]interface{}{
			queryTestNode{
				int64(6), toml.Position{2, 1},
			},
		})
}

func TestQuerySliceRange(t *testing.T) {
	assertQueryPositions(t,
		"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
		"$.foo.a[0:5]",
		[]interface{}{
			queryTestNode{
				int64(1), toml.Position{2, 1},
			},
			queryTestNode{
				int64(2), toml.Position{2, 1},
			},
			queryTestNode{
				int64(3), toml.Position{2, 1},
			},
			queryTestNode{
				int64(4), toml.Position{2, 1},
			},
			queryTestNode{
				int64(5), toml.Position{2, 1},
			},
		})
}

func TestQuerySliceStep(t *testing.T) {
	assertQueryPositions(t,
		"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
		"$.foo.a[0:5:2]",
		[]interface{}{
			queryTestNode{
				int64(1), toml.Position{2, 1},
			},
			queryTestNode{
				int64(3), toml.Position{2, 1},
			},
			queryTestNode{
				int64(5), toml.Position{2, 1},
			},
		})
}

func TestQueryAny(t *testing.T) {
	assertQueryPositions(t,
		"[foo.bar]\na=1\nb=2\n[foo.baz]\na=3\nb=4",
		"$.foo.*",
		[]interface{}{
			queryTestNode{
				map[string]interface{}{
					"a": int64(1),
					"b": int64(2),
				}, toml.Position{1, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(3),
					"b": int64(4),
				}, toml.Position{4, 1},
			},
		})
}
func TestQueryUnionSimple(t *testing.T) {
	assertQueryPositions(t,
		"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
		"$.*[bar,foo]",
		[]interface{}{
			queryTestNode{
				map[string]interface{}{
					"a": int64(1),
					"b": int64(2),
				}, toml.Position{1, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(3),
					"b": int64(4),
				}, toml.Position{4, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(5),
					"b": int64(6),
				}, toml.Position{7, 1},
			},
		})
}

func TestQueryRecursionAll(t *testing.T) {
	assertQueryPositions(t,
		"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
		"$..*",
		[]interface{}{
			queryTestNode{
				map[string]interface{}{
					"foo": map[string]interface{}{
						"bar": map[string]interface{}{
							"a": int64(1),
							"b": int64(2),
						},
					},
					"baz": map[string]interface{}{
						"foo": map[string]interface{}{
							"a": int64(3),
							"b": int64(4),
						},
					},
					"gorf": map[string]interface{}{
						"foo": map[string]interface{}{
							"a": int64(5),
							"b": int64(6),
						},
					},
				}, toml.Position{1, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"bar": map[string]interface{}{
						"a": int64(1),
						"b": int64(2),
					},
				}, toml.Position{1, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(1),
					"b": int64(2),
				}, toml.Position{1, 1},
			},
			queryTestNode{
				int64(1), toml.Position{2, 1},
			},
			queryTestNode{
				int64(2), toml.Position{3, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"foo": map[string]interface{}{
						"a": int64(3),
						"b": int64(4),
					},
				}, toml.Position{4, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(3),
					"b": int64(4),
				}, toml.Position{4, 1},
			},
			queryTestNode{
				int64(3), toml.Position{5, 1},
			},
			queryTestNode{
				int64(4), toml.Position{6, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"foo": map[string]interface{}{
						"a": int64(5),
						"b": int64(6),
					},
				}, toml.Position{7, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(5),
					"b": int64(6),
				}, toml.Position{7, 1},
			},
			queryTestNode{
				int64(5), toml.Position{8, 1},
			},
			queryTestNode{
				int64(6), toml.Position{9, 1},
			},
		})
}

func TestQueryRecursionUnionSimple(t *testing.T) {
	assertQueryPositions(t,
		"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
		"$..['foo','bar']",
		[]interface{}{
			queryTestNode{
				map[string]interface{}{
					"bar": map[string]interface{}{
						"a": int64(1),
						"b": int64(2),
					},
				}, toml.Position{1, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(3),
					"b": int64(4),
				}, toml.Position{4, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(1),
					"b": int64(2),
				}, toml.Position{1, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"a": int64(5),
					"b": int64(6),
				}, toml.Position{7, 1},
			},
		})
}

func TestQueryFilterFn(t *testing.T) {
	buff, err := ioutil.ReadFile("../example.toml")
	if err != nil {
		t.Error(err)
		return
	}

	assertQueryPositions(t, string(buff),
		"$..[?(int)]",
		[]interface{}{
			queryTestNode{
				int64(8001), toml.Position{13, 1},
			},
			queryTestNode{
				int64(8001), toml.Position{13, 1},
			},
			queryTestNode{
				int64(8002), toml.Position{13, 1},
			},
			queryTestNode{
				int64(5000), toml.Position{14, 1},
			},
		})

	assertQueryPositions(t, string(buff),
		"$..[?(string)]",
		[]interface{}{
			queryTestNode{
				"TOML Example", toml.Position{3, 1},
			},
			queryTestNode{
				"Tom Preston-Werner", toml.Position{6, 1},
			},
			queryTestNode{
				"GitHub", toml.Position{7, 1},
			},
			queryTestNode{
				"GitHub Cofounder & CEO\nLikes tater tots and beer.",
				toml.Position{8, 1},
			},
			queryTestNode{
				"192.168.1.1", toml.Position{12, 1},
			},
			queryTestNode{
				"10.0.0.1", toml.Position{21, 3},
			},
			queryTestNode{
				"eqdc10", toml.Position{22, 3},
			},
			queryTestNode{
				"10.0.0.2", toml.Position{25, 3},
			},
			queryTestNode{
				"eqdc10", toml.Position{26, 3},
			},
		})

	assertQueryPositions(t, string(buff),
		"$..[?(float)]",
		[]interface{}{ // no float values in document
		})

	tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
	assertQueryPositions(t, string(buff),
		"$..[?(tree)]",
		[]interface{}{
			queryTestNode{
				map[string]interface{}{
					"name":         "Tom Preston-Werner",
					"organization": "GitHub",
					"bio":          "GitHub Cofounder & CEO\nLikes tater tots and beer.",
					"dob":          tv,
				}, toml.Position{5, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"server":         "192.168.1.1",
					"ports":          []interface{}{int64(8001), int64(8001), int64(8002)},
					"connection_max": int64(5000),
					"enabled":        true,
				}, toml.Position{11, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"alpha": map[string]interface{}{
						"ip": "10.0.0.1",
						"dc": "eqdc10",
					},
					"beta": map[string]interface{}{
						"ip": "10.0.0.2",
						"dc": "eqdc10",
					},
				}, toml.Position{17, 1},
			},
			queryTestNode{
				map[string]interface{}{
					"ip": "10.0.0.1",
					"dc": "eqdc10",
				}, toml.Position{20, 3},
			},
			queryTestNode{
				map[string]interface{}{
					"ip": "10.0.0.2",
					"dc": "eqdc10",
				}, toml.Position{24, 3},
			},
			queryTestNode{
				map[string]interface{}{
					"data": []interface{}{
						[]interface{}{"gamma", "delta"},
						[]interface{}{int64(1), int64(2)},
					},
				}, toml.Position{28, 1},
			},
		})

	assertQueryPositions(t, string(buff),
		"$..[?(time)]",
		[]interface{}{
			queryTestNode{
				tv, toml.Position{9, 1},
			},
		})

	assertQueryPositions(t, string(buff),
		"$..[?(bool)]",
		[]interface{}{
			queryTestNode{
				true, toml.Position{15, 1},
			},
		})
}
