package wtf.java9.xml_transformer;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringReader;
import java.io.StringWriter;

import static java.util.Arrays.stream;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;

class TransformTest {

	// note that the indentation is two spaces
	private static final String INITIAL_XML =
			"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n" +
			"  <node></node>\n" +
			"</root>";

	/*
	 * To experiment with `xml:space="preserve"` add it to the root element:
	 *
	 * On Java 8 it has no impact, so both tests suites should pass just the same.
	 * Apparently it is overridden by the transformer's configuration.
	 *
	 * On Java 9 it makes the configured case behave like the default one, so
	 * apparently it overrides the transformer's configuration.
	 */

	private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
	private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();

	private Transformer transformer;

	@BeforeEach
	void createTransformer() throws Exception {
		transformer = TRANSFORMER_FACTORY.newTransformer();
	}

	@Nested
	class DefaultSettings {

		// These tests are expected to pass on Java 9

		@Test
		void keepsRootOnFirstLine() throws Exception {
			Lines transformation = transform(parse(INITIAL_XML));

			assertThat(transformation.lineAt(0)).contains("<root");
		}

		@Test
		void doesNotAddEmptyLines(TestReporter reporter) throws Exception {
			Lines transformation = transform(parse(INITIAL_XML));

			reporter.publishEntry("Transformed XML", "\n" + transformation);

			assertThat(transformation.lineAt(2).trim()).isNotEmpty();
		}

		@Test
		void keepsIndentation() throws Exception {
			Lines transformation = transform(parse(INITIAL_XML));

			assertThat(transformation.lineAt(1)).startsWith("  <");
		}

		@Test
		void newNodesAreInline() throws Exception {
			Document document = parse(INITIAL_XML);
			setChildNode(document, "node", "inner", "inner node content");
			Lines transformation = transform(document);

			assertThat(transformation.lineAt(1)).endsWith("<node><inner>inner node content</inner></node>");
		}

		@Test
		void cDataIsInline() throws Exception {
			Document document = parse(INITIAL_XML);
			setCDataContent(document, "node", "cdata content");
			Lines transformation = transform(document);

			assertThat(transformation.lineAt(1)).endsWith("<node><![CDATA[cdata content]]></node>");
		}

	}

	@Nested
	class IndentationSettings {

		@BeforeEach
		void createTransformer() throws Exception {
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			// note that the indentation is set to be changed to four spaces
			transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "4");
		}

		@Test // expected to pass on Java 9
		void pushesRootNodeToUnindentedNewLine(TestReporter reporter) throws Exception {
			Lines transformation = transform(parse(INITIAL_XML));

			reporter.publishEntry("Transformed XML", "\n" + transformation);

			assertThat(transformation.lineAt(0)).doesNotContain("<root");
			assertThat(transformation.lineAt(1)).startsWith("<root");
		}

		@Test // expected to fail on Java 9 because it puts in new lines
		void doesNotAddEmptyLines(TestReporter reporter) throws Exception {
			Lines transformation = transform(parse(INITIAL_XML));

			reporter.publishEntry("Transformed XML", "\n" + transformation);

			assertThat(transformation.lineAt(2).trim()).isNotEmpty();
		}

		@Test // expected to fail on Java 9 because it reformats existing lines
		void keepsIndentationOfUnchangedNodes(TestReporter reporter) throws Exception {
			Lines transformation = transform(parse(INITIAL_XML));

			reporter.publishEntry("Transformed XML", "\n" + transformation);

			assertThat(transformation.lineWith("<node")).startsWith("  <node");
		}

		@Test // expected to pass on Java 9 because new nodes are always correctly indented
		void newNodesAreIndented(TestReporter reporter) throws Exception {
			Document document = parse(INITIAL_XML);
			setChildNode(document, "node", "inner", "inner node content");
			Lines transformation = transform(document);

			reporter.publishEntry("Transformed XML", "\n" + transformation);

			assertThat(transformation.lineWith("<inner>")).isEqualTo("        <inner>inner node content</inner>");
		}

		@Test // expected to fail on Java 9 because it puts CDATA on its own line
		void cDataIsInline(TestReporter reporter) throws Exception {
			Document document = parse(INITIAL_XML);
			setCDataContent(document, "node", "cdata content");
			Lines transformation = transform(document);

			reporter.publishEntry("Transformed XML", "\n" + transformation);

			assertThat(transformation.lineWith("CDATA")).endsWith("<node><![CDATA[cdata content]]></node>");
		}

	}

	/*
	 * HELPER
	 */

	private static Document parse(String xml) throws Exception {
		return DOCUMENT_BUILDER_FACTORY
				.newDocumentBuilder()
				.parse(new InputSource(new StringReader(xml)));
	}

	private static void setCDataContent(Document document, String nodeName, String nodeContent) {
		CDATASection cDATASection = document.createCDATASection(nodeContent);
		document.getElementsByTagName(nodeName).item(0)
				.appendChild(cDATASection);
	}

	private static void setChildNode(Document document, String parentName, String childName, String childContent) {
		Element inner = document.createElementNS(null, childName);
		inner.appendChild(document.createTextNode(childContent));
		document.getElementsByTagName(parentName).item(0)
				.appendChild(inner);
	}

	private Lines transform(Document document) throws Exception {
		Source source = new DOMSource(document);
		StringWriter outputWriter = new StringWriter();
		transformer.transform(source, new StreamResult(outputWriter));
		return Lines.of(outputWriter.toString());
	}

	static class Lines {

		private final String[] lines;

		private Lines(String[] lines) {
			this.lines = requireNonNull(lines);
		}

		public static Lines of(String string) {
			return new Lines(string.split(System.lineSeparator()));
		}

		public String lineAt(int line) {
			return lines[line];
		}

		public String lineWith(String content) {
			return stream(lines)
					.filter(line -> line.contains(content))
					.findFirst()
					.orElseThrow(() -> new IllegalArgumentException("No line contains \"" + content + "\"."));
		}

		@Override
		public String toString() {
			return String.join("\n", lines);
		}

	}

}