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.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

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

	private static Path ensureInProjectOrShares(String file) throws IOException {
		var candidates = List.of(Paths.get(file), Paths.get("SessionE26").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("/opt/Exam26").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 Ex1LonguestInterval {

		private static void assertLonguestIntervalMatchesExpected(Path dataPath, Duration timeout) throws IOException {
			var fileName = dataPath.getFileName().toString();
			var outPath = dataPath.resolveSibling(fileName.replace(".data", ".out"));
			var values = Files.readAllLines(dataPath).stream().map(Integer::parseInt).toList();
			var expected = Integer.parseInt(Files.readString(outPath).trim());
			var result = assertTimeoutPreemptively(timeout, () -> SessionE26.longestInterval(values),
					"timeout with file " + dataPath);
			assertEquals(expected, result, "problem with file " + fileName);
		}

		private static void testLonguestIntervalFiles(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;
				}
				assertLonguestIntervalMatchesExpected(dataPath, timeout);
			}
		}

		@Test
		@Order(20)
		void testLonguestInterval() {
			assertEquals(0, SessionE26.longestInterval(List.of()));
			assertEquals(1, SessionE26.longestInterval(List.of(1)));
			assertEquals(2, SessionE26.longestInterval(List.of(1, 2)));
			assertEquals(2, SessionE26.longestInterval(List.of(2, 1)));
			assertEquals(3, SessionE26.longestInterval(List.of(2, 1, 3)));
			assertEquals(1, SessionE26.longestInterval(List.of(2, 0)));
			assertEquals(1, SessionE26.longestInterval(List.of(0, 2)));
			assertEquals(2, SessionE26.longestInterval(List.of(-2, -1)));
			assertEquals(1, SessionE26.longestInterval(List.of(-2, -4)));
			assertEquals(3, SessionE26.longestInterval(List.of(2, 6, 4, 8, 3, 9)));
		}

		@Test
		@Order(21)
		void testLonguestInterval2() {
			assertEquals(0, SessionE26.longestInterval(List.of()));
			assertEquals(1, SessionE26.longestInterval(List.of(1, 1)));
			assertEquals(3, SessionE26.longestInterval(List.of(2, 1, 3, 2)));
		}

		@Test
		@Order(24)
		void testLonguestIntervalFileSmall() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex1/Small");
			testLonguestIntervalFiles(dir, Duration.ofMillis(1_000));
		}

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

		@Test
		@Order(26)
		void testLonguestIntervalFileLarge() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex1/Large");
			testLonguestIntervalFiles(dir, Duration.ofMillis(3_000));
		}

		@Test
		@Order(28)
		void testLonguestIntervalBig() {
			var size = 10_000;

			var llist = IntStream.range(0, size).boxed().collect(Collectors.toCollection(LinkedList::new));
			Collections.shuffle(llist);
			var result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> SessionE26.longestInterval(llist),
					"La méthode allDistinct est trop lente");
			assertEquals(size, result);
		}

		@Test
		@Order(29)
		void testLonguestIntervalVeryBig() {
			var size = 1_000_000;

			var result = assertTimeoutPreemptively(Duration.ofMillis(1000),
					() -> SessionE26.longestInterval(Collections.nCopies(size, 1)),
					"La méthode allDistinct est trop lente");
			assertEquals(1, result);

			var list = IntStream.range(0, size).boxed().toList();
			result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> SessionE26.longestInterval(list),
					"La méthode allDistinct est trop lente");
			assertEquals(size, result);

			var mlist = IntStream.range(0, size).map(x -> x % 10).boxed().toList();
			result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> SessionE26.longestInterval(mlist),
					"La méthode allDistinct est trop lente");
			assertEquals(10, result);

			var slist = IntStream.range(0, size).boxed().collect(Collectors.toCollection(ArrayList::new));
			Collections.shuffle(slist);
			result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> SessionE26.longestInterval(slist),
					"La méthode allDistinct est trop lente");
			assertEquals(size, result);
		}
	}

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

		private static final int TARGET = 10;

		private static Set<SessionE26.Indices> readExpectedTwoSum(Path outPath) throws IOException {
			try (var lines = Files.lines(outPath)) {
				return lines.map(line -> line.split(" "))
						.map(parts -> new SessionE26.Indices(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])))
						.collect(Collectors.toSet());
			}
		}

		private static void assertTwoSumMatchesExpected(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).toArray();
			var expected = readExpectedTwoSum(outPath);
			var result = assertTimeoutPreemptively(timeout, () -> SessionE26.twoSum(values, TARGET),
					"timeout with file " + dataPath);
			assertEquals(expected.size(), result.size(), "problem with file " + fileName);
			assertEquals(expected, result, "problem with file " + fileName);
		}

		private static void testTwoSumFiles(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;
				}
				assertTwoSumMatchesExpected(dataPath, timeout);
			}
		}

		@Test
		@Order(31)
		void testTwoSum() {
			assertEquals(Set.of(), SessionE26.twoSum(new int[] {}, TARGET));
			assertEquals(Set.of(), SessionE26.twoSum(new int[] { 9 }, TARGET));
			assertEquals(Set.of(), SessionE26.twoSum(new int[] { 10 }, TARGET));
			assertEquals(Set.of(), SessionE26.twoSum(new int[] { 1, 10 }, TARGET));
			assertEquals(Set.of(new SessionE26.Indices(0, 1)), SessionE26.twoSum(new int[] { 1, 9 }, TARGET));
			assertEquals(Set.of(new SessionE26.Indices(0, 1)), SessionE26.twoSum(new int[] { 5, 5 }, TARGET));
			assertEquals(Set.of(new SessionE26.Indices(0, 1), new SessionE26.Indices(0, 3),
					new SessionE26.Indices(1, 2), new SessionE26.Indices(2, 3)),
					SessionE26.twoSum(new int[] { 1, 9, 1, 9 }, TARGET));
			assertEquals(Set.of(new SessionE26.Indices(0, 1), new SessionE26.Indices(2, 3)),
					SessionE26.twoSum(new int[] { -1, 11, 2, 8 }, TARGET));
			assertEquals(Set.of(new SessionE26.Indices(0, 1), new SessionE26.Indices(1, 2)),
					SessionE26.twoSum(new int[] { 10, 0, 10 }, TARGET));
		}

		@Test
		@Order(32)
		void testTwoSumFileSmall() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/Small");
			testTwoSumFiles(dir, Duration.ofMillis(1_000));
		}

		@Test
		@Order(33)
		void testTwoSumFileMedium() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/Medium");
			testTwoSumFiles(dir, Duration.ofMillis(1_000));
		}

		@Test
		@Order(34)
		void testTwoSumFileLarge() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex2/Large");
			testTwoSumFiles(dir, Duration.ofMillis(10_000));
		}
	}

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

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

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

		private static void testBingoFiles(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;
				}
				assertBingoMatchesExpected(dataPath, timeout);
			}
		}

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

			Files.write(test, List.of("1;A 0".split(";")));
			assertEquals(List.of(1), SessionE26.bingo(test));

			Files.write(test, List.of("2;A 0".split(";")));
			assertEquals(List.of(0), SessionE26.bingo(test));

			Files.write(test, List.of("2;A 0;A 1".split(";")));
			assertEquals(List.of(0, 1), SessionE26.bingo(test));

			Files.write(test, List.of("2;A 1;A 0".split(";")));
			assertEquals(List.of(0, 1), SessionE26.bingo(test));

			Files.write(test, List.of("2;A 0;A 0".split(";")));
			assertEquals(List.of(0, 0), SessionE26.bingo(test));

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

			Files.write(test, List.of("2;A 0;B 1;A 1;B 0".split(";")));
			assertEquals(List.of(0, 0, 1, 2), SessionE26.bingo(test));
		}

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

			Files.write(test, List.of("1;Alice 0".split(";")));
			assertEquals(List.of(1), SessionE26.bingo(test));

			Files.write(test, List.of("2;AA 0;BB 1;AA 1;BB 0".split(";")));
			assertEquals(List.of(0, 0, 1, 2), SessionE26.bingo(test));
		}

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

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

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

			Files.write(test, List.of("2;A 0;B 1;A 1;A 0;B 0".split(";")));
			assertEquals(List.of(0, 0, 1, 1, 2), SessionE26.bingo(test));
		}

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

		@Test
		@Order(55)
		void testBingoFileMedium() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/Medium");
			testBingoFiles(dir, Duration.ofMillis(100));
		}

		@Test
		@Order(56)
		void testBingoFileLarge() throws IOException {
			Path dir = ensureInProjectOrShares("data/Ex3/Large");
			testBingoFiles(dir, Duration.ofMillis(10_000));
		}
	}
}
