#region Copyright & License Information
/*
 * Copyright (c) The OpenRA Developers and Contributors
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System;
using System.Linq;
using NUnit.Framework;

namespace OpenRA.Test
{
	[TestFixture]
	public class MiniYamlTest
	{
		[TestCase(TestName = "Parse tree roundtrips")]
		public void TestParseRoundtrip()
		{
			const string Yaml =
@"1:
2: Test
3: # Test
4:
	4.1:
5: Test
	5.1:
6: # Test
	6.1:
7:
	7.1.1:
	7.1.2: Test
	7.1.3: # Test
8: Test
	8.1.1:
	8.1.2: Test
	8.1.3: # Test
9: # Test
	9.1.1:
	9.1.2: Test
	9.1.3: # Test
";
			var serialized = MiniYaml.FromString(Yaml, "", discardCommentsAndWhitespace: false).WriteToString();
			Console.WriteLine();
			Assert.That(serialized, Is.EqualTo(Yaml));
		}

		[TestCase(TestName = "Parse tree can handle empty lines")]
		public void TestParseEmptyLines()
		{
			const string Yaml =
@"1:

2: Test

3: # Test

4:

	4.1:

5: Test

	5.1:

6: # Test

	6.1:

7:

	7.1.1:

	7.1.2: Test

	7.1.3: # Test

8: Test

	8.1.1:

	8.1.2: Test

	8.1.3: # Test

9: # Test

	9.1.1:

	9.1.2: Test

	9.1.3: # Test

";

			const string ExpectedYaml =
@"1:
2: Test
3:
4:
	4.1:
5: Test
	5.1:
6:
	6.1:
7:
	7.1.1:
	7.1.2: Test
	7.1.3:
8: Test
	8.1.1:
	8.1.2: Test
	8.1.3:
9:
	9.1.1:
	9.1.2: Test
	9.1.3:
";
			var serialized = MiniYaml.FromString(Yaml, "").WriteToString();
			Assert.That(serialized, Is.EqualTo(ExpectedYaml));
		}

		[TestCase(TestName = "Mixed tabs & spaces indents")]
		public void TestIndents()
		{
			const string YamlTabStyle = @"
Root1:
	Child1:
		Attribute1: Test
		Attribute2: Test
	Child2:
		Attribute1: Test
		Attribute2: Test
Root2:
	Child1:
		Attribute1: Test
";

			const string YamlMixedStyle = @"
Root1:
    Child1:
        Attribute1: Test
        Attribute2: Test
	Child2:
		Attribute1: Test
	    Attribute2: Test
Root2:
    Child1:
		Attribute1: Test
";
			var tabs = MiniYaml.FromString(YamlTabStyle, "").WriteToString();
			Console.WriteLine(tabs);
			var mixed = MiniYaml.FromString(YamlMixedStyle, "").WriteToString();
			Console.WriteLine(mixed);
			Assert.That(tabs, Is.EqualTo(mixed));
		}

		[TestCase(TestName = "Yaml files should be able to remove nodes")]
		public void NodeRemoval()
		{
			const string BaseString = @"
Parent:
	Child:
		Key: value
		-Key:
";

			const string ResultString = "Parent:\n\tChild:\n";
			var baseYaml = MiniYaml.FromString(BaseString, "");

			var resultYaml = MiniYaml.Merge(new[] { baseYaml });
			Assert.AreEqual(ResultString, resultYaml.WriteToString());
		}

		[TestCase(TestName = "Merged yaml files should be able to remove nodes")]
		public void MergedNodeRemoval()
		{
			const string BaseString = @"
Parent:
	Child:
		Key: value
";

			const string MergeString = @"
Parent:
	Child:
		-Key:
";

			const string ResultString = "Parent:\n\tChild:\n";
			var baseYaml = MiniYaml.FromString(BaseString, "");
			var mergeYaml = MiniYaml.FromString(MergeString, "");

			var resultYaml = MiniYaml.Merge(new[] { baseYaml, mergeYaml });
			Assert.AreEqual(ResultString, resultYaml.WriteToString());
		}

		[TestCase(TestName = "Merged yaml files should be able to remove nodes from inherited parents")]
		public void MergedInheritedNodeRemoval()
		{
			const string BaseString = @"
^Base:
	Child:
		Key: value
Parent:
	Inherits: ^Base
";

			const string MergeString = @"
Parent:
	Child:
		-Key:
";

			const string ResultString = "^Base:\n\tChild:\n\t\tKey: value\nParent:\n\tChild:\n";
			var baseYaml = MiniYaml.FromString(BaseString, "");
			var mergeYaml = MiniYaml.FromString(MergeString, "");

			var resultYaml = MiniYaml.Merge(new[] { baseYaml, mergeYaml });
			Assert.AreEqual(ResultString, resultYaml.WriteToString());
		}

		[TestCase(TestName = "Inheritance and removal can be composed")]
		public void InheritanceAndRemovalCanBeComposed()
		{
			const string BaseYaml = @"
^BaseA:
	MockA2:
^BaseB:
	Inherits@a: ^BaseA
	MockB2:
";
			const string ExtendedYaml = @"
Test:
	Inherits@b: ^BaseB
	-MockA2:
";
			const string MapYaml = @"
^BaseC:
	MockC2:
Test:
	Inherits@c: ^BaseC
";
			var result = MiniYaml.Merge(new[] { BaseYaml, ExtendedYaml, MapYaml }.Select(s => MiniYaml.FromString(s, "")))
				.First(n => n.Key == "Test").Value.Nodes;

			Assert.IsFalse(result.Any(n => n.Key == "MockA2"), "Node should not have the MockA2 child, but does.");
			Assert.IsTrue(result.Any(n => n.Key == "MockB2"), "Node should have the MockB2 child, but does not.");
			Assert.IsTrue(result.Any(n => n.Key == "MockC2"), "Node should have the MockC2 child, but does not.");
		}

		[TestCase(TestName = "Child can be removed after multiple inheritance")]
		public void ChildCanBeRemovedAfterMultipleInheritance()
		{
			const string BaseYaml = @"
^BaseA:
	MockA2:
Test:
	Inherits: ^BaseA
	MockA2:
";
			const string OverrideYaml = @"
Test:
	-MockA2
";

			var result = MiniYaml.Merge(new[] { BaseYaml, OverrideYaml }.Select(s => MiniYaml.FromString(s, "")))
				.First(n => n.Key == "Test").Value.Nodes;

			Assert.IsFalse(result.Any(n => n.Key == "MockA2"), "Node should not have the MockA2 child, but does.");
		}

		[TestCase(TestName = "Child can be immediately removed")]
		public void ChildCanBeImmediatelyRemoved()
		{
			const string BaseYaml = @"
^BaseA:
    MockString:
        AString: Base
Test:
    Inherits: ^BaseA
    MockString:
        AString: Override
    -MockString:
";

			var result = MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "")))
				 .First(n => n.Key == "Test").Value.Nodes;

			Assert.IsFalse(result.Any(n => n.Key == "MockString"), "Node should not have the MockString child, but does.");
		}

		[TestCase(TestName = "Child can be removed and immediately overridden")]
		public void ChildCanBeRemovedAndImmediatelyOverridden()
		{
			const string BaseYaml = @"
^BaseA:
    MockString:
        AString: Base
Test:
    Inherits: ^BaseA
    -MockString:
    MockString:
        AString: Override
";

			var result = MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "")))
				 .First(n => n.Key == "Test").Value.Nodes;

			Assert.IsTrue(result.Any(n => n.Key == "MockString"), "Node should have the MockString child, but does not.");
			Assert.IsTrue(result.First(n => n.Key == "MockString").Value.NodeWithKey("AString").Value.Value == "Override",
				"MockString value has not been set with the correct override value for AString.");
		}

		[TestCase(TestName = "Child can be removed and later overridden")]
		public void ChildCanBeRemovedAndLaterOverridden()
		{
			const string BaseYaml = @"
^BaseA:
    MockString:
        AString: Base
Test:
    Inherits: ^BaseA
    -MockString:
";
			const string OverrideYaml = @"
Test:
    MockString:
        AString: Override
";

			var result = MiniYaml.Merge(new[] { BaseYaml, OverrideYaml }.Select(s => MiniYaml.FromString(s, "")))
				.First(n => n.Key == "Test").Value.Nodes;

			Assert.IsTrue(result.Any(n => n.Key == "MockString"), "Node should have the MockString child, but does not.");
			Assert.IsTrue(result.First(n => n.Key == "MockString").Value.NodeWithKey("AString").Value.Value == "Override",
				"MockString value has not been set with the correct override value for AString.");
		}

		[TestCase(TestName = "Child can be removed from intermediate parent")]
		public void ChildCanBeOverriddenThenRemoved()
		{
			const string BaseYaml = @"
^BaseA:
    MockString:
        AString: Base
^BaseB:
    Inherits: ^BaseA
    MockString:
        AString: Override
";
			const string OverrideYaml = @"
Test:
    Inherits: ^BaseB
    MockString:
    	-AString:
";

			var result = MiniYaml.Merge(new[] { BaseYaml, OverrideYaml }.Select(s => MiniYaml.FromString(s, "")))
				.First(n => n.Key == "Test").Value.Nodes;
			Assert.IsTrue(result.Any(n => n.Key == "MockString"), "Node should have the MockString child, but does not.");
			Assert.IsFalse(result.First(n => n.Key == "MockString").Value.Nodes.Any(n => n.Key == "AString"),
				"MockString value should have been removed, but was not.");
		}

		[TestCase(TestName = "Child subnode can be removed and immediately overridden")]
		public void ChildSubNodeCanBeRemovedAndImmediatelyOverridden()
		{
			const string BaseYaml = @"
^BaseA:
	MockString:
		CollectionOfStrings:
			StringA: A
			StringB: B
Test:
    Inherits: ^BaseA
    MockString:
		-CollectionOfStrings:
		CollectionOfStrings:
			StringC: C
";

			var merged = MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "")))
				.First(n => n.Key == "Test");

			var traitNode = merged.Value.Nodes.Single();
			var fieldNodes = traitNode.Value.Nodes;
			var fieldSubNodes = fieldNodes.Single().Value.Nodes;

			Assert.IsTrue(fieldSubNodes.Length == 1, "Collection of strings should only contain the overriding subnode.");
			Assert.IsTrue(fieldSubNodes.Single(n => n.Key == "StringC").Value.Value == "C",
				"CollectionOfStrings value has not been set with the correct override value for StringC.");
		}

		[TestCase(TestName = "Child subnode can be removed and later overridden")]
		public void ChildSubNodeCanBeRemovedAndLaterOverridden()
		{
			const string BaseYaml = @"
^BaseA:
	MockString:
		CollectionOfStrings:
			StringA: A
			StringB: B
Test:
    Inherits: ^BaseA
    MockString:
		-CollectionOfStrings:
";

			const string OverrideYaml = @"
Test:
    MockString:
		CollectionOfStrings:
			StringC: C
";

			var merged = MiniYaml.Merge(new[] { BaseYaml, OverrideYaml }.Select(s => MiniYaml.FromString(s, "")))
				.First(n => n.Key == "Test");

			var traitNode = merged.Value.Nodes.Single();
			var fieldNodes = traitNode.Value.Nodes;
			var fieldSubNodes = fieldNodes.Single().Value.Nodes;

			Assert.IsTrue(fieldSubNodes.Length == 1, "Collection of strings should only contain the overriding subnode.");
			Assert.IsTrue(fieldSubNodes.Single(n => n.Key == "StringC").Value.Value == "C",
				"CollectionOfStrings value has not been set with the correct override value for StringC.");
		}

		[TestCase(TestName = "Inheritance works for nested nodes")]
		public void InheritanceWorksForNestedNodes()
		{
			const string BaseYaml = @"
^DefaultKey:
	Key: value
";
			const string ExtendedYaml = @"
Parent:
	Child:
		Inherits: ^DefaultKey
";

			const string ResultString =
@"^DefaultKey:
	Key: value
Parent:
	Child:
		Key: value
";
			var baseYaml = MiniYaml.FromString(BaseYaml, "");
			var mergeYaml = MiniYaml.FromString(ExtendedYaml, "");

			var resultYaml = MiniYaml.Merge(new[] { baseYaml, mergeYaml });
			Assert.AreEqual(ResultString, resultYaml.WriteToString());
		}

		[TestCase(TestName = "Empty lines should count toward line numbers")]
		public void EmptyLinesShouldCountTowardLineNumbers()
		{
			const string Yaml = @"
TestA:
	Nothing:

TestB:
	Nothing:
";

			var result = MiniYaml.FromString(Yaml, "").First(n => n.Key == "TestB");
			Assert.AreEqual(5, result.Location.Line);
		}

		[TestCase(TestName = "Duplicated nodes are correctly merged")]
		public void TestSelfMerging()
		{
			const string BaseYaml = @"
Test:
	Merge: original
		Child: original
	Original:
Test:
	Merge: override
		Child: override
	Override:
";

			var result = MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "")));
			Assert.That(result.Count(n => n.Key == "Test"), Is.EqualTo(1), "Result should have exactly one Test node.");

			var testNodes = result.First(n => n.Key == "Test").Value.Nodes;
			Assert.That(testNodes.Select(n => n.Key), Is.EqualTo(new[] { "Merge", "Original", "Override" }), "Merged Test node has incorrect child nodes.");

			var mergeNode = testNodes.First(n => n.Key == "Merge").Value;
			Assert.That(mergeNode.Value, Is.EqualTo("override"), "Merge node has incorrect value.");
			Assert.That(mergeNode.Nodes[0].Value.Value, Is.EqualTo("override"), "Merge node Child value should be 'override', but is not");
		}

		[TestCase(TestName = "Duplicated nodes across multiple sources are correctly merged")]
		public void TestSelfMergingMultiSource()
		{
			const string FirstYaml = @"
Test:
	Merge: original
		Child: original
	Original:
";
			const string SecondYaml = @"
Test:
	Merge: original
		Child: original
	Original:
Test:
	Merge: override
		Child: override
	Override:
";

			var result = MiniYaml.Merge(new[] { FirstYaml, SecondYaml }.Select(s => MiniYaml.FromString(s, "")));
			Assert.That(result.Count(n => n.Key == "Test"), Is.EqualTo(1), "Result should have exactly one Test node.");

			var testNodes = result.First(n => n.Key == "Test").Value.Nodes;
			Assert.That(testNodes.Select(n => n.Key), Is.EqualTo(new[] { "Merge", "Original", "Override" }), "Merged Test node has incorrect child nodes.");

			var mergeNode = testNodes.First(n => n.Key == "Merge").Value;
			Assert.That(mergeNode.Value, Is.EqualTo("override"), "Merge node has incorrect value.");
			Assert.That(mergeNode.Nodes[0].Value.Value, Is.EqualTo("override"), "Merge node Child value should be 'override', but is not");
		}

		[TestCase(TestName = "Duplicated child nodes throw merge error if parent does not require merging")]
		public void TestMergeConflictsNoMerge()
		{
			const string BaseYaml = @"
Test:
	Merge:
		Child:
		Child:
";

			static void Merge() => MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "test-filename")));

			Assert.That(Merge, Throws.Exception.TypeOf<ArgumentException>().And.Message.EqualTo(
				"MiniYaml.Merge, duplicate values found for the following keys: Child: [Child (at test-filename:4),Child (at test-filename:5)]"));
		}

		[TestCase(TestName = "Duplicated child nodes throw merge error if first parent requires merging")]
		public void TestMergeConflictsFirstParent()
		{
			const string BaseYaml = @"
Test:
	Merge:
		Child1:
		Child1:
	Merge:
";

			static void Merge() => MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "test-filename")));

			Assert.That(Merge, Throws.Exception.TypeOf<ArgumentException>().And.Message.EqualTo(
				"MiniYaml.Merge, duplicate values found for the following keys: Child1: [Child1 (at test-filename:4),Child1 (at test-filename:5)]"));
		}

		[TestCase(TestName = "Duplicated child nodes throw merge error if second parent requires merging")]
		public void TestMergeConflictsSecondParent()
		{
			const string BaseYaml = @"
Test:
	Merge:
	Merge:
		Child2:
		Child2:
";

			static void Merge() => MiniYaml.Merge(new[] { BaseYaml }.Select(s => MiniYaml.FromString(s, "test-filename")));

			Assert.That(Merge, Throws.Exception.TypeOf<ArgumentException>().And.Message.EqualTo(
				"MiniYaml.Merge, duplicate values found for the following keys: Child2: [Child2 (at test-filename:5),Child2 (at test-filename:6)]"));
		}

		[TestCase(TestName = "Duplicated child nodes across multiple sources do not throw")]
		public void TestMergeConflictsMultiSourceMerge()
		{
			const string FirstYaml = @"
Test:
	Merge:
		Child:
";
			const string SecondYaml = @"
Test:
	Merge:
		Child:
";

			var result = MiniYaml.Merge(new[] { FirstYaml, SecondYaml }.Select(s => MiniYaml.FromString(s, "")));
			var testNodes = result.First(n => n.Key == "Test").Value.Nodes;
			var mergeNode = testNodes.First(n => n.Key == "Merge").Value;
			Assert.That(mergeNode.Nodes.Count, Is.EqualTo(1));
		}

		[TestCase(TestName = "Duplicated child nodes across multiple sources throw merge error if first parent requires merging")]
		public void TestMergeConflictsMultiSourceFirstParent()
		{
			const string FirstYaml = @"
Test:
	Merge:
		Child1:
		Child1:
";
			const string SecondYaml = @"
Test:
	Merge:
";

			static void Merge() => MiniYaml.Merge(new[] { FirstYaml, SecondYaml }.Select(s => MiniYaml.FromString(s, "test-filename")));

			Assert.That(Merge, Throws.Exception.TypeOf<ArgumentException>().And.Message.EqualTo(
				"MiniYaml.Merge, duplicate values found for the following keys: Child1: [Child1 (at test-filename:4),Child1 (at test-filename:5)]"));
		}

		[TestCase(TestName = "Duplicated child nodes across multiple sources throw merge error if second parent requires merging")]
		public void TestMergeConflictsMultiSourceSecondParent()
		{
			const string FirstYaml = @"
Test:
	Merge:
";
			const string SecondYaml = @"
Test:
	Merge:
		Child2:
		Child2:
";

			static void Merge() => MiniYaml.Merge(new[] { FirstYaml, SecondYaml }.Select(s => MiniYaml.FromString(s, "test-filename")));

			Assert.That(Merge, Throws.Exception.TypeOf<ArgumentException>().And.Message.EqualTo(
				"MiniYaml.Merge, duplicate values found for the following keys: Child2: [Child2 (at test-filename:4),Child2 (at test-filename:5)]"));
		}

		[TestCase(TestName = "Comments are correctly separated from values")]
		public void TestEscapedHashInValues()
		{
			var trailingWhitespace = MiniYaml.FromString("key: value # comment", "", discardCommentsAndWhitespace: false)[0];
			Assert.AreEqual("value", trailingWhitespace.Value.Value);
			Assert.AreEqual(" comment", trailingWhitespace.Comment);

			var noWhitespace = MiniYaml.FromString("key:value# comment", "", discardCommentsAndWhitespace: false)[0];
			Assert.AreEqual("value", noWhitespace.Value.Value);
			Assert.AreEqual(" comment", noWhitespace.Comment);

			var escapedHashInValue = MiniYaml.FromString(@"key: before \# after # comment", "", discardCommentsAndWhitespace: false)[0];
			Assert.AreEqual("before # after", escapedHashInValue.Value.Value);
			Assert.AreEqual(" comment", escapedHashInValue.Comment);

			var emptyValueAndComment = MiniYaml.FromString("key:#", "", discardCommentsAndWhitespace: false)[0];
			Assert.AreEqual(null, emptyValueAndComment.Value.Value);
			Assert.AreEqual("", emptyValueAndComment.Comment);

			var noValue = MiniYaml.FromString("key:", "", discardCommentsAndWhitespace: false)[0];
			Assert.AreEqual(null, noValue.Value.Value);
			Assert.AreEqual(null, noValue.Comment);

			var emptyKey = MiniYaml.FromString(" : value", "", discardCommentsAndWhitespace: false)[0];
			Assert.AreEqual(null, emptyKey.Key);
			Assert.AreEqual("value", emptyKey.Value.Value);
			Assert.AreEqual(null, emptyKey.Comment);
		}

		[TestCase(TestName = "Leading and trailing whitespace can be guarded using a backslash")]
		public void TestGuardedWhitespace()
		{
			const string TestYaml = @"key:   \      test value    \   ";
			var nodes = MiniYaml.FromString(TestYaml, "");
			Assert.AreEqual("      test value    ", nodes[0].Value.Value);
		}

		[TestCase(TestName = "Comments should count toward line numbers")]
		public void CommentsShouldCountTowardLineNumbers()
		{
			const string Yaml = @"
TestA:
	Nothing:

# Comment
TestB:
	Nothing:
";
			var resultDiscard = MiniYaml.FromString(Yaml, "");
			var resultDiscardLine = resultDiscard.First(n => n.Key == "TestB").Location.Line;
			Assert.That(resultDiscardLine, Is.EqualTo(6), "Node TestB should report its location as line 6, but is not (discarding comments)");
			Assert.That(resultDiscard[1].Key, Is.EqualTo("TestB"), "Node TestB should be the second child of the root node, but is not (discarding comments)");

			var resultKeep = MiniYaml.FromString(Yaml, "", discardCommentsAndWhitespace: false);
			var resultKeepLine = resultKeep.First(n => n.Key == "TestB").Location.Line;
			Assert.That(resultKeepLine, Is.EqualTo(6), "Node TestB should report its location as line 6, but is not (parsing comments)");
			Assert.That(resultKeep[4].Key, Is.EqualTo("TestB"), "Node TestB should be the fifth child of the root node, but is not (parsing comments)");
		}

		[TestCase(TestName = "Comments should survive a round trip intact")]
		public void CommentsSurviveRoundTrip()
		{
			var yaml = @"
# Top level comment node
#
Parent: # comment without value
	# Indented comment node
	#
		# Double Indented comment node
		#
			# Triple Indented comment node
			#
	First: value containing a \# character
	Second: value # node with inline comment
	Third: value #
	Fourth: #
	Fifth# embedded comment:
	Sixth# embedded comment: still a comment
	Seventh# embedded comment: still a comment # more comment
".Replace("\r\n", "\n");

			var canonicalYaml = @"
# Top level comment node
#
Parent: # comment without value
	# Indented comment node
	#
		# Double Indented comment node
		#
			# Triple Indented comment node
			#
	First: value containing a \# character
	Second: value # node with inline comment
	Third: value #
	Fourth: #
	Fifth: # embedded comment:
	Sixth: # embedded comment: still a comment
	Seventh: # embedded comment: still a comment # more comment
".Replace("\r\n", "\n");

			var result = MiniYaml.FromString(yaml, "", discardCommentsAndWhitespace: false).WriteToString();
			Assert.AreEqual(canonicalYaml, result);
		}

		[TestCase(TestName = "Comments should be removed when discardCommentsAndWhitespace is false")]
		public void CommentsShouldntSurviveRoundTrip()
		{
			const string Yaml = @"
# Top level comment node
#
Parent: # comment without value
	# Indented comment node
	#
		# Double Indented comment node
		#
			# Triple Indented comment node
			#
	First: value containing a \# character
	Second: value # node with inline comment
	Third: value #
	Fourth: #
	Fifth# embedded comment:
	Sixth# embedded comment: still a comment
	Seventh# embedded comment: still a comment # more comment
";

			var strippedYaml = @"Parent:
	First: value containing a \# character
	Second: value
	Third: value
	Fourth:
	Fifth:
	Sixth:
	Seventh:
".Replace("\r\n", "\n");

			var result = MiniYaml.FromString(Yaml, "").WriteToString();
			Assert.AreEqual(strippedYaml, result);
		}
	}
}
