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

	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/Session5").resolve(relative);

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

	private static List<String> readExpectedLines(Path path) throws IOException {
		return Files.readAllLines(path).stream().map(String::trim).toList();
	}

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

		@Test
		@Order(1)
		void testDiscoverWord() {
			assertEquals(-1, Session5.discoverWord("", ""));
			assertEquals(1, Session5.discoverWord("a", "a"));
			assertEquals(1, Session5.discoverWord("aaaa", "a"));
			assertEquals(-1, Session5.discoverWord("dragon", "a"));
			assertEquals(6, Session5.discoverWord("dragon", "dragon"));
			assertEquals(10, Session5.discoverWord("cat", "arithmetics"));
			assertEquals(-1, Session5.discoverWord("carlovingian", "arufdwop"));
			assertEquals(5, Session5.discoverWord("u".repeat(100), "aeiou"));

			assertThrows(NullPointerException.class, () -> Session5.discoverWord(null, ""));
			assertThrows(NullPointerException.class, () -> Session5.discoverWord("", null));
		}

		@Test
		@Order(10)
		void testDiscoverWordEfficiency() {
			var size = 100_000;

			var result = assertTimeoutPreemptively(Duration.ofMillis(1000),
					() -> Session5.discoverWord("u".repeat(size), "a".repeat(size)),
					"La méthode allDistinct est trop lente");
			assertEquals(-1, result);

			result = assertTimeoutPreemptively(Duration.ofMillis(1000),
					() -> Session5.discoverWord("u".repeat(size), "a".repeat(size) + "u"),
					"La méthode allDistinct est trop lente");
			assertEquals(size + 1, result);
		}
	}

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

		@Test
		@Order(22)
		void testDiscoverTextFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Discover/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"));

				long expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session5.discoverText(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}

		@Test
		@Order(23)
		void testDiscoverTextMediumSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Discover/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"));

				long expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session5.discoverText(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}

		@Test
		@Order(24)
		void testDiscoverTextFileLarge(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Discover/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"));

				long expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(10_000), () -> Session5.discoverText(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}
	}

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

		@Test
		@Order(31)
		void testGuessWord() throws IOException {

			Path path = ensureInProjectOrShares("data/Guess/words0.data");

			assertEquals(List.of(), Session5.guessWord("*", path));
			assertEquals(List.of(), Session5.guessWord("bo*rd", path));
			assertEquals(List.of("mood", "wood"), Session5.guessWord("*ood", path));
			assertEquals(List.of("wood", "word"), Session5.guessWord("wo*d", path));
			assertEquals(List.of("cap", "cat"), Session5.guessWord("ca*", path));

			assertThrows(NullPointerException.class, () -> Session5.guessWord(null, path));
			assertThrows(NullPointerException.class, () -> Session5.guessWord("", null));
		}

		@Test
		@Order(32)
		void testGuessWordEfficiency(@TempDir Path tempDir) throws IOException {
			Path toGuess = ensureInProjectOrShares("data/Guess/guessWords0.data");
			Path words = ensureInProjectOrShares("data/Guess/words.data");

			var outPath = ensureInProjectOrShares("data/Guess/guessWords0.out");
			var expected = readExpectedLines(outPath);
			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session5.guessWords(toGuess, words),
					"timeout with file " + toGuess);
			assertEquals(expected, result, "problem with file " + toGuess);

		}

		@Test
		@Order(33)
		void testGuessWordEfficiency1(@TempDir Path tempDir) throws IOException {
			Path toGuess = ensureInProjectOrShares("data/Guess/guessWords1.data");
			Path words = ensureInProjectOrShares("data/Guess/words.data");

			var outPath = ensureInProjectOrShares("data/Guess/guessWords1.out");
			var expected = readExpectedLines(outPath);
			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session5.guessWords(toGuess, words),
					"timeout with file " + toGuess);
			assertEquals(expected, result, "problem with file " + toGuess);

		}

		@Test
		@Order(34)
		void testGuessWordEfficiency2(@TempDir Path tempDir) throws IOException {
			Path toGuess = ensureInProjectOrShares("data/Guess/guessWords2.data");
			Path words = ensureInProjectOrShares("data/Guess/words.data");

			var outPath = ensureInProjectOrShares("data/Guess/guessWords2.out");
			var expected = readExpectedLines(outPath);
			var result = assertTimeoutPreemptively(Duration.ofMillis(2_000), () -> Session5.guessWords(toGuess, words),
					"timeout with file " + toGuess);
			assertEquals(expected, result, "problem with file " + toGuess);

		}

		@Test
		@Order(35)
		void testGuessWordEfficiency3(@TempDir Path tempDir) throws IOException {
			Path toGuess = ensureInProjectOrShares("data/Guess/guessWords3.data");
			Path words = ensureInProjectOrShares("data/Guess/words.data");

			var outPath = ensureInProjectOrShares("data/Guess/guessWords3.out");
			var expected = readExpectedLines(outPath);
			var result = assertTimeoutPreemptively(Duration.ofMillis(2_000), () -> Session5.guessWords(toGuess, words),
					"timeout with file " + toGuess);
			assertEquals(expected, result, "problem with file " + toGuess);

		}
	}

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

		@Test
		@Order(41)
		void testRotateFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Rotate/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"));

				long expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(100), () -> Session5.rotate(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}

		@Test
		@Order(42)
		void testRotateFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Rotate/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"));

				long expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session5.rotate(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}

		@Test
		@Order(43)
		void testRotateFileLarge(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Rotate/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"));

				long expected = Integer.parseInt(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(50_000), () -> Session5.rotate(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath);
			}
		}

	}
}
