package fr.uge.code.camp;

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

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
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 java.util.Set;

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;

import fr.uge.code.camp.Session4.Box;

class Session4Test {

	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 Ex1FallingBricks {

		@Test
		@Order(24)
		void testFallingBricksFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/FallingBricks/Small");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).sorted().toList();

			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				int expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session4.bricks(dataPath));
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}
		
		@Test
		@Order(25)
		void testFallingBricksFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/FallingBricks/Medium");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).sorted().toList();

			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				int expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session4.bricks(dataPath));
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}
		
		@Test
		@Order(26)
		void testFallingBricksFileLarge(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/FallingBricks/Large");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).sorted().toList();

			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				int expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(5_000), () -> Session4.bricks(dataPath));
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}
	}
	


	@Nested
	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
	public final class Ex2JunctionBoxes {
		@Test
		@Order(23)
		void testJunctionBoxesFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/JunctionBoxes/Small");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).sorted().toList();

			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				try (var readerOut = Files.newBufferedReader(outPath)) {
					var result = Set.of(Box.ofString(readerOut.readLine()), Box.ofString(readerOut.readLine()));
					var boxes = assertTimeoutPreemptively(Duration.ofMillis(10),
							() -> Session4.junctionBoxes(dataPath));
					assertEquals(result, boxes, "problem with file " + dataPath);
				}
			}
		}
		
		@Test
		@Order(24)
		void testJunctionBoxesFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/JunctionBoxes/Medium");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).sorted().toList();

			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				try (var readerOut = Files.newBufferedReader(outPath)) {
					var result = Set.of(Box.ofString(readerOut.readLine()), Box.ofString(readerOut.readLine()));
					var boxes = assertTimeoutPreemptively(Duration.ofMillis(1_000),
							() -> Session4.junctionBoxes(dataPath));
					assertEquals(result, boxes, "problem with file " + dataPath);
				}
			}
		}
		
		@Test
		@Order(25)
		void testJunctionBoxesFileLarge(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/JunctionBoxes/Large");
			var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).sorted().toList();

			for (var dataPath : files) {
				var fileName = dataPath.getFileName().toString();
				if (fileName.startsWith("._")) {
					continue;
				}
				var outPath = dir.resolve(fileName.replace(".data", ".out"));

				try (var readerOut = Files.newBufferedReader(outPath)) {
					var result = Set.of(Box.ofString(readerOut.readLine()), Box.ofString(readerOut.readLine()));
					var boxes = assertTimeoutPreemptively(Duration.ofMillis(10_000),
							() -> Session4.junctionBoxes(dataPath));
					assertEquals(result, boxes, "problem with file " + dataPath);
				}
			}
		}
	}


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

		@Test
		@Order(30)
		void testNestingDepth(@TempDir Path tempDir) throws IOException {

			assertThrows(NullPointerException.class, () -> Session4.nestingDepth(null));

			assertEquals("0", Session4.nestingDepth("0"));
			assertEquals("(1)", Session4.nestingDepth("1"));
			assertEquals("0(1)", Session4.nestingDepth("01"));
			assertEquals("(1)0", Session4.nestingDepth("10"));
			assertEquals("((2))", Session4.nestingDepth("2"));
			assertEquals("(1((((5((7)))))))", Session4.nestingDepth("157"));
			assertEquals("(1(2)1((33)2)1)", Session4.nestingDepth("1213321"));
			assertEquals("(1((((5)4)3((5)4(5)))2(((5)4(5((7)))))))", Session4.nestingDepth("154354525457"));
			assertEquals("(1(2(3)))0(1)", Session4.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),
								() -> Session4.nestingDepth(input));
						assertEquals(output, swaps, "problem with file " + dataPath + " 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 Ex4SavingUniverse {

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

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

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

		}

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

			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
				return Session4.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 Session4.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 Session4.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),
								() -> Session4.saveTheUniverse(Integer.parseInt(tmp[0]), tmp[1]));
						assertEquals(value, swaps, "problem with file " + dataPath + " 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);

		}
	}
}
