package fr.uge.code.camp;

import static org.junit.jupiter.api.Assertions.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.io.TempDir;

class SessionBTest {

	private static Path ensureInProjectOrShares(String file) throws IOException {
		var path = Paths.get(file);
		if (Files.exists(path)) {
			return path;
		}
		// Same relative path, but under folder "/mnt/shares/..." instead of "data"
		var relative = Paths.get("").relativize(path);
		var fallback = Paths.get("/mnt/shares/igm/prof/pivoteau/JCC/Session4").resolve(relative);

		if (!Files.exists(fallback)) {
			throw new NoSuchFileException("Missing file in both locations: " + path + " and " + fallback);
		}
		return fallback;
	}

	@Nested
	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
	public final class Ex1DoubleOrOneThing {

		@Test
		@Order(20)
		void testDoubleOrOneThing(@TempDir Path tempDir) throws IOException {
			assertThrows(NullPointerException.class, () -> SessionB.saveTheUniverse(0, null));
			assertThrows(IllegalArgumentException.class, () -> SessionB.saveTheUniverse(-1, ""));

			assertEquals("A", SessionB.doubleOrOneThing("A"));
			assertEquals("AAB", SessionB.doubleOrOneThing("AB"));
			assertEquals("AA", SessionB.doubleOrOneThing("AA"));
			assertEquals("AABA", SessionB.doubleOrOneThing("ABA"));
			assertEquals("BAAC", SessionB.doubleOrOneThing("BAC"));
			assertEquals("BBAAAAC", SessionB.doubleOrOneThing("BBAAC"));
		}

		@Test
		@Order(21)
		void testDoubleOrOneThingEfficiency(@TempDir Path tempDir) throws IOException {

			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
				return SessionB.doubleOrOneThing("A".repeat(1_000_000));
			}, "La méthode saveTheUniverse est trop lente");
			assertEquals(1_000_000, result.length());

			result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
				return SessionB.doubleOrOneThing("A".repeat(1_000_000) + "B");
			}, "La méthode saveTheUniverse est trop lente");
			assertEquals(2_000_001, result.length());

			result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
				return SessionB.doubleOrOneThing("A".repeat(1_000_000) + "B".repeat(1_000_000));
			}, "La méthode saveTheUniverse est trop lente");
			assertEquals(3_000_000, result.length());

		}

		private static void testDoubleOrOneThing(Path dir, List<Path> files) throws IOException {
			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				try (var readerData = Files.newBufferedReader(dataPath);
						var readerOut = Files.newBufferedReader(outPath)) {
					var line = readerData.readLine();
					var count = 1;
					while ((line = readerData.readLine()) != null) {
						var input = line;
						var output = readerOut.readLine().split(": ")[1];
						var result = assertTimeoutPreemptively(Duration.ofMillis(10),
								() -> SessionB.doubleOrOneThing(input));
						assertEquals(output, result,
								"problem with file " + dataPath.getFileName() + " with case #" + count);
						count++;
					}
				}
			}
		}

		@Test
		@Order(22)
		void testSavingUniverseFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/DoubleOrOneThing/sample_test_set_1/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testDoubleOrOneThing(dir, files);
		}

		@Test
		@Order(23)
		void testSavingUniverseFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/DoubleOrOneThing/test_set_1/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testDoubleOrOneThing(dir, files);
		}

		@Test
		@Order(24)
		void testSavingUniverseFileMedium2(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/DoubleOrOneThing/test_set_2/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testDoubleOrOneThing(dir, files);
		}
	}

	@Nested
	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
	public final class Ex2NestingDepth {

		@Test
		@Order(30)
		void testNestingDepth(@TempDir Path tempDir) throws IOException {
			assertThrows(NullPointerException.class, () -> SessionB.nestingDepth(null));

			assertEquals("0", SessionB.nestingDepth("0"));
			assertEquals("(1)", SessionB.nestingDepth("1"));
			assertEquals("0(1)", SessionB.nestingDepth("01"));
			assertEquals("(1)0", SessionB.nestingDepth("10"));
			assertEquals("((2))", SessionB.nestingDepth("2"));
			assertEquals("(1((((5((7)))))))", SessionB.nestingDepth("157"));
			assertEquals("(1(2)1((33)2)1)", SessionB.nestingDepth("1213321"));
			assertEquals("(1((((5)4)3((5)4(5)))2(((5)4(5((7)))))))", SessionB.nestingDepth("154354525457"));
			assertEquals("(1(2(3)))0(1)", SessionB.nestingDepth("12301"));
		}

		@Test
		@Order(31)
		void testNestingDepthEfficiency(@TempDir Path tempDir) throws IOException {

			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
				return Session4.nestingDepth("9".repeat(10_000_000));
			}, "La méthode nestingDepth est trop lente");
			assertEquals("(((((((((" + "9".repeat(10_000_000) + ")))))))))", result);

			result = assertTimeoutPreemptively(Duration.ofMillis(3_000), () -> {
				return Session4.nestingDepth("9".repeat(100_000_000));
			}, "La méthode nestingDepth est trop lente");
			assertEquals("(((((((((" + "9".repeat(100_000_000) + ")))))))))", result);

			result = assertTimeoutPreemptively(Duration.ofMillis(10_000), () -> {
				return Session4.nestingDepth("9".repeat(1_000_000_000));
			}, "La méthode nestingDepth est trop lente");
			assertEquals("(((((((((" + "9".repeat(1_000_000_000) + ")))))))))", result);
		}

		private static void testNestingDepth(Path dir, List<Path> files) throws IOException {
			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				try (var readerData = Files.newBufferedReader(dataPath);
						var readerOut = Files.newBufferedReader(outPath)) {
					String line = readerData.readLine();
					var count = 1;
					while ((line = readerData.readLine()) != null) {
						var input = line;
						var output = readerOut.readLine().split(": ")[1];
						var swaps = assertTimeoutPreemptively(Duration.ofMillis(10),
								() -> SessionB.nestingDepth(input));
						assertEquals(output, swaps,
								"problem with file " + dataPath.getFileName() + " with case #" + count);
						count++;
					}
				}
			}
		}

		@Test
		@Order(32)
		void testNestingDepthFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/NestingDepth/sample_test_set_1/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testNestingDepth(dir, files);
		}

		@Test
		@Order(33)
		void testNestingDepthFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/NestingDepth/test_set_1/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testNestingDepth(dir, files);
		}

		@Test
		@Order(34)
		void testNestingDepthFileMedium2(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/NestingDepth/test_set_2/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testNestingDepth(dir, files);
		}
	}

	@Nested
	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
	public final class Ex3SavingUniverse {

		@Test
		@Order(40)
		void testSavingUniverse(@TempDir Path tempDir) throws IOException {

			assertThrows(NullPointerException.class, () -> SessionB.saveTheUniverse(0, null));
			assertThrows(IllegalArgumentException.class, () -> SessionB.saveTheUniverse(-1, ""));

			assertEquals(0, SessionB.saveTheUniverse(0, ""));
			assertEquals(0, SessionB.saveTheUniverse(0, "C"));
			assertEquals(0, SessionB.saveTheUniverse(1, "S"));
			assertEquals(0, SessionB.saveTheUniverse(1, "SC"));
			assertEquals(0, SessionB.saveTheUniverse(2, "CS"));
			assertEquals(1, SessionB.saveTheUniverse(1, "CS"));
			assertEquals(2, SessionB.saveTheUniverse(7, "SCSCSS"));
			assertEquals(3, SessionB.saveTheUniverse(6, "SCSCSS"));
			assertEquals(4, SessionB.saveTheUniverse(5, "SCSCSS"));
			assertEquals(6, SessionB.saveTheUniverse(7, "SCSCSSCS"));
			assertEquals(7, SessionB.saveTheUniverse(6, "SCSCSSCS"));
			assertEquals(8, SessionB.saveTheUniverse(5, "SCSCSSCS"));
		}

		@Test
		@Order(41)
		void testSavingUniverseEfficiency(@TempDir Path tempDir) throws IOException {

			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
				return SessionB.saveTheUniverse(1_000_000, "CCCC" + "S".repeat(1_000_000));
			}, "La méthode saveTheUniverse est trop lente");
			assertEquals(4_000_000, result);

			result = assertTimeoutPreemptively(Duration.ofMillis(3_000), () -> {
				return SessionB.saveTheUniverse(100_000_000, "CCCC" + "S".repeat(100_000_000));
			}, "La méthode saveTheUniverse est trop lente");
			assertEquals(400_000_000, result);

			result = assertTimeoutPreemptively(Duration.ofMillis(5_000), () -> {
				return SessionB.saveTheUniverse(100_000_000, "C".repeat(20) + "S".repeat(100_000_000));
			}, "La méthode saveTheUniverse est trop lente");
			assertEquals(2_000_000_000, result);
		}

		private static void testSavingUniverse(Path dir, List<Path> files) throws IOException {
			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				try (var readerData = Files.newBufferedReader(dataPath);
						var readerOut = Files.newBufferedReader(outPath)) {
					var input = readerData.readLine();
					var count = 1;
					while ((input = readerData.readLine()) != null) {
						var output = readerOut.readLine().split(": ")[1];
						var value = "IMPOSSIBLE".equals(output) ? -1 : Integer.parseInt(output);
						var tmp = input.split(" ");
						var swaps = assertTimeoutPreemptively(Duration.ofMillis(10),
								() -> SessionB.saveTheUniverse(Integer.parseInt(tmp[0]), tmp[1]));
						assertEquals(value, swaps,
								"problem with file " + dataPath.getFileName() + " with case #" + count);
						count++;
					}
				}

			}
		}

		@Test
		@Order(42)
		void testSavingUniverseFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/SaveTheUniverse/sample_test_set_1/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testSavingUniverse(dir, files);
		}

		@Test
		@Order(43)
		void testSavingUniverseFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/SaveTheUniverse/test_set_1/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testSavingUniverse(dir, files);
		}

		@Test
		@Order(44)
		void testSavingUniverseFileMedium2(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/SaveTheUniverse/test_set_2/");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();
			testSavingUniverse(dir, files);
		}
	}
}
