package ast_test

import (
	"reflect"
	"testing"

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

func Test_ConstructSlightlyComplexStructure(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.SetKeyValuePair(ast.NewKey("ding"), ast.NewValue(ast.TypeInteger, 10))
		p.SetKeyValuePair(ast.NewKey("dong"), ast.NewValue(ast.TypeString, "not a song"))
		p.OpenTable(ast.NewKey("key1", "key2 a"))
		p.SetKeyValuePair(ast.NewKey("dooh"), ast.NewValue(ast.TypeBool, true))
		p.SetKeyValuePair(ast.NewKey("dah"), ast.NewValue(ast.TypeBool, false))
		p.OpenTable(ast.NewKey("key1", "key2 b"))
		p.SetKeyValuePair(ast.NewKey("dieh"), ast.NewValue(ast.TypeFloat, 1.111))
		p.SetKeyValuePair(ast.NewKey("duh"), ast.NewValue(ast.TypeFloat, 1.18e-12))
		arr := ast.NewArray()
		arr.Append(ast.NewValue(ast.TypeInteger, 1))
		arr.Append(ast.NewValue(ast.TypeInteger, 2))
		p.SetKeyValuePair(ast.NewKey("foo", "bar"), ast.NewValue(ast.TypeArray, arr))
		p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
		p.SetKeyValuePair(ast.NewKey("a"), ast.NewValue(ast.TypeFloat, 1.234))
		p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
		p.SetKeyValuePair(ast.NewKey("b"), ast.NewValue(ast.TypeFloat, 2.345))
		p.SetKeyValuePair(ast.NewKey("c"), ast.NewValue(ast.TypeString, "bingo!"))
		p.OpenArrayOfTables(ast.NewKey("aaah", "table array"))
		return nil, p
	},
		"",
		`{"aaah": {"table array": [{"a": 1.234}, {"b": 2.345, "c": "bingo!"}, {}]}, `+
			`"ding": 10, "dong": "not a song", `+
			`"key1": {"key2 a": {"dah": false, "dooh": true}, "key2 b": {"dieh": 1.111, "duh": 1.18e-12, "foo": {"bar": [1, 2]}}}}`)
}

// This document structure represents the actual structure of the example for nested
// arrays of tables from the TOML 0.5.0 specficiation.
func Test_ConstructNestedArraysOfTables(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenArrayOfTables(ast.NewKey("fruit"))
		p.SetKeyValuePair(ast.NewKey("name"), ast.NewValue(ast.TypeString, "apple"))
		p.OpenTable(ast.NewKey("fruit", "physical"))
		p.SetKeyValuePair(ast.NewKey("color"), ast.NewValue(ast.TypeString, "red"))
		p.SetKeyValuePair(ast.NewKey("shape"), ast.NewValue(ast.TypeString, "round"))
		p.OpenArrayOfTables(ast.NewKey("fruit", "variety"))
		p.SetKeyValuePair(ast.NewKey("name"), ast.NewValue(ast.TypeString, "red delicious"))
		p.OpenArrayOfTables(ast.NewKey("fruit", "variety"))
		p.SetKeyValuePair(ast.NewKey("name"), ast.NewValue(ast.TypeString, "granny smith"))
		p.OpenArrayOfTables(ast.NewKey("fruit"))
		p.SetKeyValuePair(ast.NewKey("name"), ast.NewValue(ast.TypeString, "banana"))
		p.OpenArrayOfTables(ast.NewKey("fruit", "variety"))
		p.SetKeyValuePair(ast.NewKey("name"), ast.NewValue(ast.TypeString, "plantain"))
		return nil, p
	},
		"",
		`{"fruit": [`+
			`{"name": "apple", "physical": {"color": "red", "shape": "round"}, "variety": [{"name": "red delicious"}, {"name": "granny smith"}]}, `+
			`{"name": "banana", "variety": [{"name": "plantain"}]}`+
			`]}`)
}

// This is a case from the BurntSushi test set which my parser did not correctly
// handle. From the specs, it was unclear to me that is was okay to handle things
// in this way. The actual TOML document that would lead to this looks like:
//
//     [a.b.c]
//     answer = 42
//
//     [a]
//     better = 43
func Test_ConstructExplicitTableAfterImplicitSubtable(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("a", "b", "c"))
		p.SetKeyValuePair(ast.NewKey("answer"), ast.NewValue(ast.TypeString, "42"))
		err := p.OpenTable(ast.NewKey("a"))
		p.SetKeyValuePair(ast.NewKey("better"), ast.NewValue(ast.TypeString, "43"))
		return err, p
	},
		"",
		`{"a": {"b": {"c": {"answer": "42"}}, "better": "43"}}`)
}

func Test_EmptyKeyForCreatingTablePath_Panics(t *testing.T) {
	defer func() {
		r := recover()
		if r.(string) != "makeTablePath(): empty key provided; a key must have at least one key part" {
			t.Fatalf("Did not get the expected panic message")
		}
	}()
	p := ast.NewDocument()
	p.OpenTable(ast.NewKey())
}

func Test_StoreValueInRooTypeTable(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.SetKeyValuePair(ast.NewKey("key1"), ast.NewValue(ast.TypeString, "value1"))
		return p.SetKeyValuePair(ast.NewKey("key2"), ast.NewValue(ast.TypeString, "value2")), p
	},
		"",
		`{"key1": "value1", "key2": "value2"}`)
}

func Test_StoreValueWithMultipartKey_CreatesTableHierarchy(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		return p.SetKeyValuePair(ast.NewKey("key1", "key2", "key3"), ast.NewValue(ast.TypeString, "value")), p
	},
		"",
		`{"key1": {"key2": {"key3": "value"}}}`)
}

func Test_StoreDuplicateKeyInRooTypeTable_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
		return p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeInteger, 321)), p
	},
		`invalid key/value pair: string value already exists at key [key]`,
		`{"key": "value"}`)
}

func Test_StoreValueWithMultipartKey_UnderSubtable_CreatesTableHierarchy(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("tablekey1", "tablekey2"))
		return p.SetKeyValuePair(ast.NewKey("valuekey1", "valuekey2", "valuekey3"), ast.NewValue(ast.TypeString, "value")), p
	},
		"",
		`{"tablekey1": {"tablekey2": {"valuekey1": {"valuekey2": {"valuekey3": "value"}}}}}`)
}

func Test_StoreKeyPathWherePathContainsNonTableAlready_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("key1"))
		p.SetKeyValuePair(ast.NewKey("key2"), ast.NewValue(ast.TypeInteger, 0))
		return p.SetKeyValuePair(ast.NewKey("key2", "key3"), ast.NewValue(ast.TypeString, "value")), p
	},
		`invalid key/value pair: integer value already exists at key [key1->key2]`,
		`{"key1": {"key2": 0}}`)
}

func Test_GivenExistingTableAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("key1", "key2"))
		return p.OpenTable(ast.NewKey("key1", "key2")), p
	},
		`invalid table: table value already exists at key [key1->key2]`,
		`{"key1": {"key2": {}}}`)
}

func Test_GivenExistingValueAtKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
		return p.OpenTable(ast.NewKey("key")), p
	},
		`invalid table: string value already exists at key [key]`,
		`{"key": "value"}`)
}

func Test_GivenExistingValueInKeyPath_CreatingTable_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeString, "value"))
		return p.OpenTable(ast.NewKey("key", "subkey")), p
	},
		`invalid table: string value already exists at key [key]`,
		`{"key": "value"}`)
}

func Test_GivenExistingValueAtDeepKey_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("deep", "table"))
		p.SetKeyValuePair(ast.NewKey("key"), ast.NewValue(ast.TypeInteger, 0))
		return p.OpenTable(ast.NewKey("deep", "table", "key")), p
	},
		`invalid table: integer value already exists at key [deep->table->key]`,
		`{"deep": {"table": {"key": 0}}}`)
}

func Test_GivenExistingValueAtDeepKeyFromSubTable_CreatingTableAtSameKey_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("deep", "table"))
		p.SetKeyValuePair(ast.NewKey("key1", "key2"), ast.NewValue(ast.TypeInteger, 0))
		return p.SetKeyValuePair(ast.NewKey("key1", "key2"), ast.NewValue(ast.TypeBool, true)), p
	},
		// This test mainly tests the formatting of [deep->table->key1->key2], being a concatenation
		// of the currently active table plus the multipart key for SetKeyValuePair().
		`invalid key/value pair: integer value already exists at key [deep->table->key1->key2]`,
		`{"deep": {"table": {"key1": {"key2": 0}}}}`)
}

func Test_FormattingOfQuotedPathPartInError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.OpenTable(ast.NewKey("must be quoted"))
		p.SetKeyValuePair(ast.NewKey("this one too"), ast.NewValue(ast.TypeInteger, 0))
		return p.SetKeyValuePair(ast.NewKey("this one too"), ast.NewValue(ast.TypeInteger, 0)), p
	},
		`invalid key/value pair: integer value already exists at key ["must be quoted"->"this one too"]`,
		`{"must be quoted": {"this one too": 0}}`)
}

func Test_GivenExistingValueAtKey_CreatingArrayOfTablesAtSameKey_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.Root["key"] = ast.NewValue(ast.TypeString, "value")
		return p.OpenArrayOfTables(ast.NewKey("key")), p
	},
		`invalid table array: string value already exists at key [key]`,
		`{"key": "value"}`)
}

func Test_GivenExistingValueInKeyPath_CreatingArrayOfTables_ReturnsError(t *testing.T) {
	testAST(t, func() (error, *ast.Document) {
		p := ast.NewDocument()
		p.Root["key"] = ast.NewValue(ast.TypeString, "value")
		return p.OpenArrayOfTables(ast.NewKey("key", "subkey")), p
	},
		`invalid table array: string value already exists at key [key]`,
		`{"key": "value"}`)
}

func testAST(t *testing.T, code func() (error, *ast.Document), expectedError string, expectedData string) {
	err, p := code()
	if expectedError == "" {
		if err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}
	} else {
		if err == nil {
			t.Fatalf("An error was expected, but no error was returned")
		} else if err.Error() != expectedError {
			t.Fatalf("Unexpected error:\nexpected: %s\nactual: %s\n", expectedError, err.Error())
		}
	}
	if expectedData == "" {
		return
	}
	if expectedData != p.Root.String() {
		t.Fatalf("Unexpected data after parsing:\nexpected: %s\nactual: %s\n", expectedData, p.Root.String())
	}
}

func Test_NewArray(t *testing.T) {
	a := ast.NewArray()
	if a.ItemType != "" {
		t.Fatalf("New array unexpectedly has a Type set to %q", a.ItemType)
	}
	if a.First != nil {
		t.Fatalf("New array unexpectedly has the First item set")
	}
	if a.Last != nil {
		t.Fatalf("New array unexpectedly has the Last item set")
	}
	if a.Length != 0 {
		t.Fatalf("New array unexpectedly has the Length set to %d", a.Length)
	}
}

func Test_AppendOneValueToArray(t *testing.T) {
	a := ast.NewArray()
	value := ast.NewValue(ast.TypeString, "Hi!")
	if err := a.Append(value); err != nil {
		t.Fatalf("Unexpected error while adding string value to array: %s", err)
	}
	if a.Length != 1 {
		t.Fatalf("Expected Array.Length 1, but got %d", a.Length)
	}
	if a.First != a.Last {
		t.Fatalf("Array.First and Array.Last do not point to the same ArrayItem")
	}
	if a.First.Value != value {
		t.Fatalf("Array.First.Value does not point to the appended string value")
	}
	if a.First.Next != nil {
		t.Fatalf("Array.First unexpectedly has Next set")
	}
}

func Test_AppendTwoValuesToArray(t *testing.T) {
	a := ast.NewArray()
	value1 := ast.NewValue(ast.TypeString, "Hi!")
	value2 := ast.NewValue(ast.TypeString, "Hello!")
	a.Append(value1)
	if err := a.Append(value2); err != nil {
		t.Fatalf("Unexpected error while adding second string value to Array: %s", err)
	}
	if a.Length != 2 {
		t.Fatalf("Array.Length unexpected not 2, but %d", a.Length)
	}
	if a.First == a.Last {
		t.Fatalf("Array.First and Array.Last unexpectedly point to the same ArrayItem")
	}
	if a.First.Next != a.Last {
		t.Fatalf("Array.First.Next unexpectedly does not point to Array.Last")
	}
	if a.Last.Value != value2 {
		t.Fatalf("Array.Last.Value does not point to the second appended string value")
	}
}

func Test_AppendMultipleValuesToArray(t *testing.T) {
	a := ast.NewArray()
	a.Append(ast.NewValue(ast.TypeInteger, 1))
	a.Append(ast.NewValue(ast.TypeInteger, 2))
	a.Append(ast.NewValue(ast.TypeInteger, 3))
	a.Append(ast.NewValue(ast.TypeInteger, 4))

	x := make([]int, 0, a.Length)
	for i := a.First; i != nil; i = i.Next {
		x = append(x, i.Value.Data[0].(int))
	}
	if !reflect.DeepEqual(x, []int{1, 2, 3, 4}) {
		t.Fatalf("Array contents do not match Array [1, 2, 3, 4]")
	}
}

func Test_GivenValuesOfDifferentTypes_WhenAppendingThemToArray_AnErrorIsReturned(t *testing.T) {
	a := ast.NewArray()
	a.Append(ast.NewValue(ast.TypeString, "first one is string"))
	err := a.Append(ast.NewValue(ast.TypeInteger, 2)) // second one an integer
	if err == nil {
		t.Fatalf("Unexpectedly, no error was returned while adding an integer to an array of strings")
	}
	expected := "type mismatch in array of strings: found an item of type integer"
	if err.Error() != expected {
		t.Fatalf("Unexpected error from invalid Append():\nexpected: %q\nactual: %q", expected, err.Error())
	}
}
