package net.sourceforge.plantuml;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.parallel.Isolated;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import net.sourceforge.plantuml.cli.CliOptions;
import net.sourceforge.plantuml.cli.CliParser;
import net.sourceforge.plantuml.cli.CliParsingException;
import net.sourceforge.plantuml.cli.ExitStatus;
import net.sourceforge.plantuml.error.PSystemError;

@Isolated
@Execution(ExecutionMode.SAME_THREAD)
class PipeTest {

	ByteArrayOutputStream baos;
	ExitStatus errorStatus;
	CliOptions option;
	Pipe pipe;
	PrintStream ps;

	@BeforeEach
	void setup() {
		PSystemError.disableTimeBasedErrorDecorations();

		errorStatus = ExitStatus.init();

		baos = new ByteArrayOutputStream();
		option = new CliOptions();
		ps = new PrintStream(baos);

		pipe = new Pipe(option, ps, new ByteArrayInputStream(new byte[0]), UTF_8.toString());
	}

	@Test
	void should_managePipe_set_as_no_error_for_empty_input() throws IOException {
		pipe = new Pipe(option, ps, new ByteArrayInputStream(new byte[0]), UTF_8.name());

		pipe.managePipe(errorStatus);

		assertFalse(errorStatus.hasErrors());
		assertTrue(errorStatus.noDiagramFound());
		assertEquals(0, baos.toByteArray().length);
	}

	static List<TestCase> managePipeTestCases() {
		LinkedList<TestCase> l = new LinkedList<>();

		l.add(TestCase.of("", "", "", Verification.EXACT, false, true));

		// ok render
		l.add(TestCase.of("", "a->b", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, false,
				false));
		l.add(TestCase.of("", "@startuml\na->b\n@enduml", "(?s).*PNG.*Generated by https://plantuml.com.*",
				Verification.REGEX, false, false));
		l.add(TestCase.of("", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n",
				"(?s).*(PNG.*Generated by https://plantuml.com.*){2}", Verification.REGEX, false, false));

		// bad data
		l.add(TestCase.of("", "a", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, true, false));
		l.add(TestCase.of("", "@startuml\na\n@enduml\n", "(?s).*PNG.*Generated by https://plantuml.com.*",
				Verification.REGEX, true, false));
		l.add(TestCase.of("", "@startuml\na\n@enduml\n@startuml\na\n@enduml\n",
				"(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, true, false));

		// ignore garbage before; after; before&between&after diagrams
		l.add(TestCase.of("", "this-is-garbage\n@startuml\na->b\n@enduml",
				"(?s).*(PNG.*Generated by https://plantuml.com.*){1}", Verification.REGEX, false, false));
		l.add(TestCase.of("", "@startuml\na->b\n@enduml\nthis-is-garbage",
				"(?s).*(PNG.*Generated by https://plantuml.com.*){1}", Verification.REGEX, false, false));
		l.add(TestCase.of("",
				"this-is-garbage\n@startuml\na->b\n@enduml\nthis-is-garbage\n@startuml\na->b\n@enduml\nthis-is-garbage",
				"(?s).*(PNG.*Generated by https://plantuml.com.*){2}", Verification.REGEX, false, false));

		// ignore other diagram start tags when still not closed (but fails to generate)
		l.add(TestCase.of("", "@startuml\na->b\n@startgantt\n@enduml\n",
				"(?s).*(PNG.*Generated by https://plantuml.com.*){1}", Verification.REGEX, true, false));

		// manage @@@format svg
		l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml", "(?s).*<svg.*</svg>", Verification.REGEX,
				false, false));
		l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml\n@startuml\n@@@format svg\na->b\n@enduml",
				"(?s).*(<svg.*</svg>.*){2}", Verification.REGEX, false, false));

		// mixed formats
		l.add(TestCase.of("", "@startuml\n@@@format png\na->b\n@enduml\n@startuml\n@@@format svg\na->b\n@enduml",
				"(?s).*PNG.*Generated by https://plantuml.com.*<svg.*</svg>", Verification.REGEX, false, false));
		l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml\n@startuml\n@@@format png\na->b\n@enduml",
				"(?s).*<svg.*</svg>.*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, false, false));

		// pipe delimitor
		l.add(TestCase.of("-pipedelimitor PIPE-DELIMITOR", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n",
				"(?s).*(PNG.*Generated by https://plantuml.com.*PIPE-DELIMITOR.*){2}", Verification.REGEX, false,
				false));

		// if format is set in first diagram and not in the second, it will be used in
		// both (Possibly incorrect: preseved from old behaviour)
		l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml\n@startuml\na->b\n@enduml",
				"(?s).*(<svg.*</svg>.*){2}", Verification.REGEX, false, false));

		// ok computeurl
		l.add(TestCase.of("-computeurl", "@startuml\na->b\n@enduml", "IzIrIm80\n", Verification.EXACT, false, false));
		l.add(TestCase.of("-computeurl", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n",
				"IzIrIm80\nIzIrI-9AqhLB1W00\n", Verification.EXACT, false, false));

		// ok encodeurl
		l.add(TestCase.of("-encodeurl", "@startuml\na->b\n@enduml", "IzIrIm80\n", Verification.EXACT, false, false));
		l.add(TestCase.of("-encodeurl", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n",
				"IzIrIm80\nIzIrI-9AqhLB1W00\n", Verification.EXACT, false, false));

		// valid syntax
		l.add(TestCase.of("-syntax", "@startuml\na->b\n@enduml", "SEQUENCE\n(2 participants)\n", Verification.EXACT,
				false, false));
		l.add(TestCase.of("-syntax", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n",
				"SEQUENCE\n(2 participants)\nSEQUENCE\n(3 participants)\n", Verification.EXACT, false, false));
		l.add(TestCase.of("-syntax", "@startgantt\n[a] lasts 1 day\n@endgantt", "OTHER\n(Gantt)\n", Verification.EXACT,
				false, false));

		// invalid syntax
		l.add(TestCase.of("-syntax", "@startuml\na\n@enduml", "ERROR\n1\nSyntax Error?\n", Verification.EXACT, true,
				false));
		l.add(TestCase.of("-syntax", "@startuml\na\n@enduml\n@startuml\na\n@enduml",
				"ERROR\n1\nSyntax Error?\nERROR\n1\nSyntax Error?\n", Verification.EXACT, true, false));
		l.add(TestCase.of("-syntax", "@startuml\na->b\n@enduml\n@startuml\na\n@enduml",
				"SEQUENCE\n(2 participants)\nERROR\n1\nSyntax Error?\n", Verification.EXACT, true, false));
		l.add(TestCase.of("-syntax", "@startuml\na\n@enduml\n@startuml\na->b\n@enduml",
				"ERROR\n1\nSyntax Error?\nSEQUENCE\n(2 participants)\n", Verification.EXACT, true, false));

		// pipemap (using regexp to allow any coords so that it doesn't fail on
		// different systems)
		l.add(TestCase.of("-pipemap", "@startuml\na->b: [[http://a.com]] c\n@enduml",
				"<map id=\"plantuml_map\" name=\"plantuml_map\">\n"
						+ "<area shape=\"rect\" id=\"id1\" href=\"http://a.com\" title=\"http://a.com\" alt=\"\" coords=\"[0-9]+,[0-9]+,[0-9]+,[0-9]+\"/>\n"
						+ "</map>\n\n",
				Verification.REGEX, false, false));
		l.add(TestCase.of("-pipemap",
				"@startuml\na->b: [[http://a.com]] c\n@enduml\n@startuml\nc->d: [[http://c.com]] e\n@enduml",
				"<map id=\"plantuml_map\" name=\"plantuml_map\">\n"
						+ "<area shape=\"rect\" id=\"id1\" href=\"http://a.com\" title=\"http://a.com\" alt=\"\" coords=\"[0-9]+,[0-9]+,[0-9]+,[0-9]+\"/>\n"
						+ "</map>\n\n" + "<map id=\"plantuml_map\" name=\"plantuml_map\">\n"
						+ "<area shape=\"rect\" id=\"id1\" href=\"http://c.com\" title=\"http://c.com\" alt=\"\" coords=\"[0-9]+,[0-9]+,[0-9]+,[0-9]+\"/>\n"
						+ "</map>\n\n",
				Verification.REGEX, false, false));

		// no links/invalid input => no pipemap to output (no error as of
		// https://forum.plantuml.net/10049/2019-pipemap-diagrams-containing-links-give-zero-exit-code
		// )
		l.add(TestCase.of("-pipemap", "@startuml\na->b\n@enduml", "\n", Verification.EXACT, false, false));
		l.add(TestCase.of("-pipemap", "@startuml\na\n@enduml", "\n", Verification.EXACT, true, false));

		return l;
	}

	@ParameterizedTest
	@MethodSource("managePipeTestCases")
	void should_managePipe_manage_success_cases_correctly(TestCase testCase)
			throws IOException, InterruptedException, CliParsingException {
		option = CliParser.parse(testCase.getOptions().split(" "));
		pipe = new Pipe(option, ps, new ByteArrayInputStream(testCase.getInput().getBytes(UTF_8)), UTF_8.name());

		pipe.managePipe(errorStatus);

		assertEquals(testCase.isExpectedHasErrors(), errorStatus.hasErrors());
		assertEquals(testCase.isExpectedIsNoData(), errorStatus.noDiagramFound());
		testCase.getExpectedOutVerification().assertOk(baos, testCase.getExpectedOut());
	}

	@Test
	void should_readFirstDiagram_return_null_for_empty_input() throws IOException {
		pipe = new Pipe(option, null, new ByteArrayInputStream(new byte[0]), UTF_8.name());

		String actual = pipe.readFirstDiagram();

		assertNull(actual);
	}

	@Test
	void should_readFirstDiagram_decode_a_special_unicode_character_when_provided_charset_is_utf8() throws IOException {
		pipe = new Pipe(option, null, new ByteArrayInputStream("\u2620\n".getBytes(UTF_8)), UTF_8.name());

		String actual = pipe.readFirstDiagram();

		assertEquals("@startuml\n\u2620\n@enduml\n", actual);
	}

	// The testing is relevant only if the testing VM is configured in non-UTF-8
	// defaultCharset()
	@Test
	void should_readFirstDiagram_uses_current_charset_if_not_provided() throws IOException {
		String input = "\u00C1"; // A acute accented. (HTML &Aacute;). Multibyte in UTF-8.
		if (Charset.defaultCharset().newEncoder().canEncode(input)) {
			pipe = new Pipe(option, null, new ByteArrayInputStream(input.getBytes(Charset.defaultCharset())), null);

			String actual = pipe.readFirstDiagram();

			assertEquals("@startuml\n\u00C1\n@enduml\n", actual);

		} else {
			// default charset can't encode &Aacute;. Ignore the test.
			assertTrue(true);
		}
	}

	@ParameterizedTest
	@ValueSource(strings = { "ab\nc", // *nix, macOsX
			"ab\rc", // pre-macOsX macs
			"ab\r\nc", // Windows
			// the case \n\r is handled as 2 new lines, thus not added
			"ab\nc\n", "ab\nc\r", "ab\nc\r\n" })
	void should_readFirstDiagram_decode_correctly_different_line_endings(String input) throws IOException {
		pipe = new Pipe(option, null, new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.name());

		String first = pipe.readFirstDiagram();
		String second = pipe.readFirstDiagram();

		assertEquals("@startuml\nab\nc\n@enduml\n", first);
		assertNull(second); // no spurious diagram afterwards
	}

	static List<InputExpected> firstStartAndEndMarks() {
		List<InputExpected> l = new LinkedList<>();
		l.add(InputExpected.of("\nab\r\ncde", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("\nab\r\ncde\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("\nab\r\ncde\n@enduml", "@startuml\nab\ncde\n@enduml\n@enduml\n"));
		l.add(InputExpected.of("\nab\r\ncde\n@endwhatever", "@startuml\nab\ncde\n@endwhatever\n@enduml\n"));
		l.add(InputExpected.of("\nab\r\ncde\n@enduml\n", "@startuml\nab\ncde\n@enduml\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\r\n\r\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("this-is-garbage\n@startuml\nab\rcde\n@enduml\nthis-is-garbage\n",
				"@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startwhatever\nab\rcde\n@endwhatever", "@startwhatever\nab\ncde\n@endwhatever\n"));
		l.add(InputExpected.of("@start\nab\rcde\n@end", "@start\nab\ncde\n@end\n"));
		l.add(InputExpected.of("@startX\nab\rcde\n@endX", "@startX\nab\ncde\n@endX\n"));
		l.add(InputExpected.of("@startXY\nab\rcde\n@endXY", "@startXY\nab\ncde\n@endXY\n"));
		l.add(InputExpected.of("@startXYZ\nab\rcde\n@endXYZ", "@startXYZ\nab\ncde\n@endXYZ\n"));
		l.add(InputExpected.of("@start1\nab\rcde\n@end1", "@start1\nab\ncde\n@end1\n"));
		l.add(InputExpected.of("@start12\nab\rcde\n@end12", "@start12\nab\ncde\n@end12\n"));
		l.add(InputExpected.of("@start123\nab\rcde\n@end123", "@start123\nab\ncde\n@end123\n"));
		return l;
	}

	@ParameterizedTest
	@MethodSource("firstStartAndEndMarks")
	void should_readFirstDiagram_handle_correctly_start_and_end_marks(InputExpected inputExpected) throws IOException {
		pipe = new Pipe(option, null, new ByteArrayInputStream(inputExpected.getInput().getBytes(UTF_8)), UTF_8.name());

		String actual = pipe.readFirstDiagram();

		assertEquals(inputExpected.getExpected(), actual);
	}

	static List<InputExpected> subsequentStartAndEndMarks() {
		List<InputExpected> l = new LinkedList<>();
		l.add(InputExpected.of("\nab\r\ncde", null));
		l.add(InputExpected.of("\nab\r\ncde\n", null));
		l.add(InputExpected.of("\nab\r\ncde\n@enduml", null));
		l.add(InputExpected.of("\nab\r\ncde\n@endwhatever", null));
		l.add(InputExpected.of("\nab\r\ncde\n@enduml\n", null));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\r\n\r\n", "@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("this-is-garbage\n@startuml\nab\rcde\n@enduml\nthis-is-garbage\n",
				"@startuml\nab\ncde\n@enduml\n"));
		l.add(InputExpected.of("@startwhatever\nab\rcde\n@endwhatever", "@startwhatever\nab\ncde\n@endwhatever\n"));
		l.add(InputExpected.of("@start\nab\rcde\n@end", "@start\nab\ncde\n@end\n"));

		l.add(InputExpected.of("@startX\nab\rcde\n@endX", "@startX\nab\ncde\n@endX\n"));
		l.add(InputExpected.of("@startXY\nab\rcde\n@endXY", "@startXY\nab\ncde\n@endXY\n"));
		l.add(InputExpected.of("@startXYZ\nab\rcde\n@endXYZ", "@startXYZ\nab\ncde\n@endXYZ\n"));
		l.add(InputExpected.of("@start1\nab\rcde\n@end1", "@start1\nab\ncde\n@end1\n"));
		l.add(InputExpected.of("@start12\nab\rcde\n@end12", "@start12\nab\ncde\n@end12\n"));
		l.add(InputExpected.of("@start123\nab\rcde\n@end123", "@start123\nab\ncde\n@end123\n"));
		return l;
	}

	@ParameterizedTest
	@MethodSource("subsequentStartAndEndMarks")
	void should_readSubsequentDiagram_handle_correctly_start_and_end_marks(InputExpected inputExpected)
			throws IOException {
		pipe = new Pipe(option, null, new ByteArrayInputStream(inputExpected.getInput().getBytes(UTF_8)), UTF_8.name());

		String actual = pipe.readSubsequentDiagram();

		assertEquals(inputExpected.getExpected(), actual);
	}

	@ParameterizedTest
	@ValueSource(strings = { "@@@format png", "png", "@@@format png <deadbeef>" })
	void should_manageFormat_handle_png(String valid) {
		pipe.manageFormat(valid);

		assertEquals(FileFormat.PNG, option.getFileFormatOption().getFileFormat());
	}

	@ParameterizedTest
	@ValueSource(strings = { "@@@format svg", "svg", "@@@format svg <deadbeef>" })
	void should_manageFormat_handle_svg(String valid) {
		pipe.manageFormat(valid);

		assertEquals(FileFormat.SVG, option.getFileFormatOption().getFileFormat());
	}

	@ParameterizedTest
	@ValueSource(strings = { "@@@format invalid", "invalid", "@@@format invalid <deadbeef>" })
	void should_manageFormat_ignore_any_other_value_and_keep_default_value_as_file_format_option(String valid) {
		pipe.manageFormat(valid);

		assertEquals(FileFormat.PNG, option.getFileFormatOption().getFileFormat());
	}

	@ParameterizedTest
	@ValueSource(strings = { "@@@format atxt", "atxt", "@@@format atxt <deadbeef>", "@@@format ATXT", "ATXT",
			"@@@format ATXT <deadbeef>" })
	void should_manageFormat_handle_atxt(String valid) {
		pipe.manageFormat(valid);

		assertEquals(FileFormat.ATXT, option.getFileFormatOption().getFileFormat());
	}

	@ParameterizedTest
	@ValueSource(strings = { "@@@format utxt", "utxt", "@@@format utxt <deadbeef>", "@@@format UTXT", "UTXT",
			"@@@format UTXT <deadbeef>" })
	void should_manageFormat_handle_utxt(String valid) {
		pipe.manageFormat(valid);

		assertEquals(FileFormat.UTXT, option.getFileFormatOption().getFileFormat());
	}

	static class TestCase {

		private final String options;
		private final String input;
		private final String expectedOut;
		private final Verification expectedOutVerification;
		private final boolean expectedHasErrors;
		private final boolean expectedIsNoData;

		public TestCase(String options, String input, String expectedOut, Verification expectedOutVerification,
				boolean expectedHasErrors, boolean expectedIsNoData) {
			this.options = options;
			this.input = input;
			this.expectedOut = expectedOut;
			this.expectedOutVerification = expectedOutVerification;
			this.expectedHasErrors = expectedHasErrors;
			this.expectedIsNoData = expectedIsNoData;
		}

		public static TestCase of(String option, String input, String expectedOut, Verification expectedOutVerification,
				boolean expectedHasErrors, boolean expectedIsNoData) {
			return new TestCase(option, input, expectedOut, expectedOutVerification, expectedHasErrors,
					expectedIsNoData);
		}

		public String getOptions() {
			return options;
		}

		public String getInput() {
			return input;
		}

		public String getExpectedOut() {
			return expectedOut;
		}

		public Verification getExpectedOutVerification() {
			return expectedOutVerification;
		}

		public boolean isExpectedHasErrors() {
			return expectedHasErrors;
		}

		public boolean isExpectedIsNoData() {
			return expectedIsNoData;
		}

		@Override
		public String toString() {
			return "o:'" + options + "', i:'" + input + "', e-out='" + expectedOut + "', e-err='" + expectedHasErrors
					+ "', e-nodata='" + expectedIsNoData + "'";
		}

	}

	static class InputExpected {
		private final String input;
		private final String expected;

		public InputExpected(String input, String expected) {
			this.input = input;
			this.expected = expected;
		}

		public static InputExpected of(String input, String expected) {
			return new InputExpected(input, expected);
		}

		public String getInput() {
			return input;
		}

		public String getExpected() {
			return expected;
		}

		@Override
		public String toString() {
			return "i:'" + input + "', e='" + expected + "'";
		}
	}

	enum Verification {
		EXACT, REGEX;

		void assertOk(ByteArrayOutputStream baos, String expectedOut) {
			final String result = new String(baos.toByteArray(), UTF_8).replaceAll("\r", "");
			switch (this) {
			case EXACT:
				assertEquals(expectedOut, result);
				break;

			case REGEX:
				assertTrue(result.matches(expectedOut), "Result does not match pattern: " + expectedOut);
				break;
			}
		}
	}
}
