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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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

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

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

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

		private static void assertAbsoluteDifferenceMatchesExpected(Path dataPath, Duration timeout)
				throws IOException {
			var fileName = dataPath.getFileName().toString();
			var outPath = dataPath.resolveSibling(fileName.replace(".data", ".out"));
			var values = Files.lines(dataPath).mapToInt(Integer::parseInt).boxed().toList();
			var expected = Integer.parseInt(Files.readString(outPath).trim());
			var result = assertTimeoutPreemptively(timeout, () -> SessionR26.minimumAbsoluteDifference(values),
					"timeout with file " + dataPath);
			assertEquals(expected, result, "problem with file " + fileName);
		}

		private static void testAbsoluteDifferenceFiles(Path dir, Duration timeout) throws IOException {
			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;
				}
				assertAbsoluteDifferenceMatchesExpected(dataPath, timeout);
			}
		}

		@Test
		@Order(11)
		void testAbsoluteDifference() {
			assertEquals(0, SessionR26.minimumAbsoluteDifference(List.of()));
			assertEquals(0, SessionR26.minimumAbsoluteDifference(List.of(9)));
			assertEquals(1, SessionR26.minimumAbsoluteDifference(List.of(9, 10)));
			assertEquals(1, SessionR26.minimumAbsoluteDifference(List.of(10, 9)));
			assertEquals(1, SessionR26.minimumAbsoluteDifference(List.of(-10, -9)));
			assertEquals(19, SessionR26.minimumAbsoluteDifference(List.of(10, -9)));
			assertEquals(19, SessionR26.minimumAbsoluteDifference(List.of(-10, 9)));
		}

		@Test
		@Order(12)
		void testAbsoluteDifferenceFileSmall() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex1/Small");
			testAbsoluteDifferenceFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(13)
		void testAbsoluteDifferenceFileMedium() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex1/Medium");
			testAbsoluteDifferenceFiles(dir, Duration.ofMillis(1_000));
		}

		@Test
		@Order(14)
		void testAbsoluteDifferenceFileLarge() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex1/Large");
			testAbsoluteDifferenceFiles(dir, Duration.ofMillis(1_000));
		}
	}

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

		private static void assertSimpleBotMatchesExpected(Path dataPath, Duration timeout) throws IOException {
			var fileName = dataPath.getFileName().toString();
			var outPath = dataPath.resolveSibling(fileName.replace(".data", ".out"));

			try (var readerData = Files.newBufferedReader(dataPath); var readerOut = Files.newBufferedReader(outPath)) {
				readerData.readLine();
				String line;
				var count = 1;
				while ((line = readerData.readLine()) != null) {
					var expectedLine = readerOut.readLine();
					assertNotNull(expectedLine, "missing output line for file " + fileName + " and case #" + count);
					var expected = Integer.parseInt(expectedLine);
					var input = line;
					var result = assertTimeoutPreemptively(timeout, () -> SessionR26.twoSimpleBots(input),
							"timeout with file " + dataPath + " and case #" + count);
					assertEquals(expected, result, "problem with file " + fileName + " and case #" + count);
					count++;
				}
				assertNull(readerOut.readLine(), "too many output lines in " + outPath.getFileName());
			}
		}

		private static void testTwoSimpleBotFiles(Path dir, Duration timeout) throws IOException {
			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;
				}
				assertSimpleBotMatchesExpected(dataPath, timeout);
			}
		}

		@Test
		@Order(20)
		void testTwoSimpleBots() {
			assertEquals(0, SessionR26.twoSimpleBots("0"));
			assertEquals(1, SessionR26.twoSimpleBots("1"));
			assertEquals(0, SessionR26.twoSimpleBots("10"));
			assertEquals(2, SessionR26.twoSimpleBots("1 2"));
			assertEquals(9, SessionR26.twoSimpleBots("2 1"));
			assertEquals(22, SessionR26.twoSimpleBots("4 2 6 1 8"));
			assertEquals(31, SessionR26.twoSimpleBots("8 2 4 2 5 1 8 8 4 0 1"));
		}

		@Test
		@Order(21)
		void testTwoSimpleBotFileSample() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/small");
			testTwoSimpleBotFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(22)
		void testTwoSimpleBotFileSet1() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/medium");
			testTwoSimpleBotFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(23)
		void testTwoSimpleBotFileSet2() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/large");
			testTwoSimpleBotFiles(dir, Duration.ofMillis(500));
		}
	}

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

		private static void assertSimpleBotMatchesExpected(Path dataPath, Duration timeout) throws IOException {
			var fileName = dataPath.getFileName().toString();
			var outPath = dataPath.resolveSibling(fileName.replace(".data", ".out"));

			try (var readerData = Files.newBufferedReader(dataPath); var readerOut = Files.newBufferedReader(outPath)) {
				readerData.readLine();
				String line;
				var count = 1;
				while ((line = readerData.readLine()) != null) {
					var expectedLine = readerOut.readLine();
					assertNotNull(expectedLine, "missing output line for file " + fileName + " and case #" + count);
					var expected = Integer.parseInt(expectedLine);
					var input = line;
					var result = assertTimeoutPreemptively(timeout, () -> SessionR26.twoSimpleBots(10, input),
							"timeout with file " + dataPath + " and case #" + count);
					assertEquals(expected, result, "problem with file " + fileName + " and case #" + count);
					count++;
				}
				assertNull(readerOut.readLine(), "too many output lines in " + outPath.getFileName());
			}
		}

		private static void testTwoSimpleBotFiles(Path dir, Duration timeout) throws IOException {
			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;
				}
				assertSimpleBotMatchesExpected(dataPath, timeout);
			}
		}

		@Test
		@Order(24)
		void testTwoSimpleBots() {
			var last = 10;
			assertEquals(0, SessionR26.twoSimpleBots(last, "0"));
			assertEquals(1, SessionR26.twoSimpleBots(last, "1"));
			assertEquals(0, SessionR26.twoSimpleBots(last, "10"));
			assertEquals(2, SessionR26.twoSimpleBots(last, "1 2"));
			assertEquals(9, SessionR26.twoSimpleBots(last, "2 1"));
			assertEquals(22, SessionR26.twoSimpleBots(last, "4 2 6 1 8"));
			assertEquals(31, SessionR26.twoSimpleBots(last, "8 2 4 2 5 1 8 8 4 0 1"));
		}

		@Test
		@Order(25)
		void testTwoSimpleBotFileSample() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/small");
			testTwoSimpleBotFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(26)
		void testTwoSimpleBotFileSet1() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/medium");
			testTwoSimpleBotFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(27)
		void testTwoSimpleBotFileSet2() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/large");
			testTwoSimpleBotFiles(dir, Duration.ofMillis(500));
		}

		@Test
		@Order(28)
		void testTwoSimpleBotsBonus() {
			var last = 10_000_000;
			assertEquals(0, SessionR26.twoSimpleBots(last, "0"));
			assertEquals(1, SessionR26.twoSimpleBots(last, "1"));
			assertEquals(10, SessionR26.twoSimpleBots(last, "10"));
			assertEquals(2, SessionR26.twoSimpleBots(last, "1 2"));
			var result = assertTimeoutPreemptively(Duration.ofMillis(10), () -> SessionR26.twoSimpleBots(last, "2 1"));
			assertEquals(9999999, result);
			result = assertTimeoutPreemptively(Duration.ofMillis(10),
					() -> SessionR26.twoSimpleBots(last, "4 2 6 1 8"));
			assertEquals(20000008, result);
			result = assertTimeoutPreemptively(Duration.ofMillis(10),
					() -> SessionR26.twoSimpleBots(last, "8 2 4 2 5 1 8 8 4 0 1"));
			assertEquals(40000001, result);
		}
	}

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

		private static void assertRaceMatchesExpected(Path dataPath, Duration timeout) throws IOException {
			var fileName = dataPath.getFileName().toString();
			var outPath = dataPath.resolveSibling(fileName.replace(".data", ".out"));
			var result = assertTimeoutPreemptively(timeout, () -> SessionR26.race(dataPath),
					"timeout with file " + dataPath);

			try (var reader = Files.newBufferedReader(outPath)) {
				var index = 0;
				for (var value : result) {
					var line = reader.readLine();
					assertNotNull(line, "list too small for " + fileName);
					assertEquals(Integer.parseInt(line), value,
							"problem with file " + fileName + " at line " + (index + 1));
					index++;
				}
				assertNull(reader.readLine(), "list too large " + fileName);
			}
		}

		private static void testRaceFiles(Path dir, Duration timeout) throws IOException {
			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;
				}
				assertRaceMatchesExpected(dataPath, timeout);
			}
		}

		@Test
		@Order(51)
		void testRace(@TempDir Path tempDir) throws IOException {
			var test = tempDir.resolve("test.data");

			Files.write(test, List.of("2;A 0;B 1;".split(";")));
			assertEquals(List.of(), SessionR26.race(test));

			Files.write(test, List.of("2;A 0;B 1;B".split(";")));
			assertEquals(List.of(0L), SessionR26.race(test));

			Files.write(test, List.of("2;A 0;B 1;B;A".split(";")));
			assertEquals(List.of(0L, 0L), SessionR26.race(test));

		}

		@Test
		@Order(51)
		void testRace2(@TempDir Path tempDir) throws IOException {
			var test = tempDir.resolve("test.data");

			Files.write(test, List.of("4;Alix 0;Aaron 0;Bob 1;Brian 1;Bob;Bob;Brian;Alix;Bob;Bob".split(";")));
			assertEquals(List.of(4L, 3L, 2L, 3L, 2L, 2L), SessionR26.race(test));

		}

		@Test
		@Order(54)
		void testRaceFileSmall() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/small");
			testRaceFiles(dir, Duration.ofMillis(20));
		}

		@Test
		@Order(55)
		void testRaceFileMedium() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/medium");
			testRaceFiles(dir, Duration.ofMillis(10000));
		}

		@Test
		@Order(56)
		void testRaceFileFastMedium() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/medium");
			testRaceFiles(dir, Duration.ofMillis(1000));
		}

		@Test
		@Order(57)
		void testRaceFileVeryFastMedium() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/medium");
			testRaceFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(58)
		void testRaceFileLarge() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/large");
			testRaceFiles(dir, Duration.ofMillis(10000));
		}
	}
}
