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.BitSet;
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 Session6Test {

	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/Session6").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 Ex1Factory {

		@Test
		@Order(1)
		void testToBitSet() {
			for (byte i = 0; i < 20; i++) {
				byte[] b = { i };
				assertEquals(BitSet.valueOf(b), Session6.of(i));
			}
			byte[] b = { 20 };
			assertEquals(BitSet.valueOf(b), Session6.of(20));
			byte[] b2 = { 127 };
			assertEquals(BitSet.valueOf(b2), Session6.of(127));
			long[] b3 = { 5875451 };
			assertEquals(BitSet.valueOf(b3), Session6.of(5875451));
		}

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

			Files.write(test, List.of("# 0"));
			assertEquals(1, Session6.factory(test));
			Files.write(test, List.of("#. 1"));
			assertEquals(1, Session6.factory(test));
			Files.write(test, List.of("#. 0"));
			assertEquals(0, Session6.factory(test));
			Files.write(test, List.of("## 1 0"));
			assertEquals(2, Session6.factory(test));
			Files.write(test, List.of("## 1 0 0-1"));
			assertEquals(1, Session6.factory(test));
			Files.write(test, List.of("###. 3 0 2"));
			assertEquals(0, Session6.factory(test));
			Files.write(test, List.of("###. 3 0 2 1"));
			assertEquals(3, Session6.factory(test));
			Files.write(test, List.of("###. 3 0 2 1-2-3"));
			assertEquals(1, Session6.factory(test));

			assertThrows(NullPointerException.class, () -> Session6.factory(null));
		}
		
		@Test
		@Order(4)
		void testFactoryFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Factory/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), () -> Session6.factory(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath.getFileName());
			}
		}

		@Test
		@Order(5)
		void testFactoryFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Factory/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(5_000), () -> Session6.factory(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath.getFileName());
			}
		}

		@Test
		@Order(6)
		void testFactoryFileLarge(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Factory/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(20_000), () -> Session6.factory(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath.getFileName());
			}
		}
	}
	
	@Nested
	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
	public final class Ex2Balance {

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

			Files.write(test, List.of("111".split("")));
			assertEquals(1, Session6.itHangsInTheBalance(test));
			Files.write(test, List.of("111111".split("")));
			assertEquals(1, Session6.itHangsInTheBalance(test));
			Files.write(test, List.of("222333".split("")));
			assertEquals(6, Session6.itHangsInTheBalance(test));
			Files.write(test, List.of("123456".split("")));
			assertEquals(6, Session6.itHangsInTheBalance(test));
			Files.write(test, List.of("234567".split("")));
			assertEquals(14, Session6.itHangsInTheBalance(test));

			assertThrows(NullPointerException.class, () -> Session6.itHangsInTheBalance(null));
		}
		
		@Test
		@Order(4)
		void testFactoryFileSmall(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Balance/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 = Long.parseLong(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session6.itHangsInTheBalance(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath.getFileName());
			}
		}
		
		@Test
		@Order(5)
		void testFactoryFileMedium(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Balance/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 = Long.parseLong(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(10_000), () -> Session6.itHangsInTheBalance(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath.getFileName());
			}
		}
		
		@Test
		@Order(6)
		void testFactoryFileLarge(@TempDir Path tempDir) throws IOException {
			Path dir = ensureInProjectOrShares("data/Balance/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 = Long.parseLong(Files.readString(outPath).trim());
				var result = assertTimeoutPreemptively(Duration.ofMillis(20_000), () -> Session6.itHangsInTheBalance(dataPath),
						"timeout with file " + dataPath);
				assertEquals(expected, result, "problem with file " + dataPath.getFileName());
			}
		}
	}
}
