package fr.uge.code.camp;

import org.junit.jupiter.api.Test;

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

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.io.TempDir;

import fr.uge.code.camp.Session1.MinMax;

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

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class Session1Test {

	@Test
	@Order(1)
	void testNumberOfOccurrences() {
		var list = List.of("a", "b", "a", "c", "a");
		assertEquals(3, Session1.numberOfOccurrences(list, "a"));
		assertEquals(1, Session1.numberOfOccurrences(list, "b"));
		assertEquals(0, Session1.numberOfOccurrences(list, "z"));
		assertEquals(0, Session1.numberOfOccurrences(List.of(), "a"));

		assertThrows(NullPointerException.class, () -> Session1.numberOfOccurrences(null, 1));
		assertThrows(NullPointerException.class, () -> Session1.numberOfOccurrences(List.of(), null));
	}

	@Test
	@Order(1)
	void testNumberOfOccurrencesEfficiency() {
		var size = 1_000_000;
		var list = IntStream.range(0, size).boxed().collect(Collectors.toList());

		assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			Session1.numberOfOccurrences(list, 1);
		}, "La méthode allDistinct est trop lente");

		var list2 = IntStream.range(0, size).boxed().collect(Collectors.toCollection(LinkedList::new));

		assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			Session1.numberOfOccurrences(list2, 1);
		}, "La méthode allDistinct est trop lente");
	}

	@Test
	@Order(11)
	void testAllDistinct() {
		assertTrue(Session1.allDistinct(List.of(1, 2, 3, 4)));
		assertTrue(Session1.allDistinct(List.of()));
		assertFalse(Session1.allDistinct(List.of(1, 2, 1)));

		assertThrows(NullPointerException.class, () -> Session1.allDistinct(null));
	}

	@Test
	@Order(12)
	void testAllDistinctEfficiency() {
		var size = 100_000;
		var list = IntStream.range(0, size).boxed().collect(Collectors.toList());

		var result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return Session1.allDistinct(list);
		}, "La méthode allDistinct est trop lente");

		assertTrue(result);
	}

	@Test
	@Order(21)
	void testListToString() {
		assertEquals("1;2;3", Session1.listToString(List.of(1, 2, 3)));
		assertEquals("hello;world", Session1.listToString(List.of("hello", "world")));
		assertEquals("", Session1.listToString(List.of()));

		assertThrows(NullPointerException.class, () -> Session1.listToString(null));
	}

	@Test
	@Order(41)
	void testMostFrequent() {
		var res1 = Session1.mostFrequent(List.of(1, 2, 1, 3));
		assertTrue(res1.isPresent());
		assertEquals(1, res1.get());

		assertEquals(Optional.empty(), Session1.mostFrequent(List.of()));
		assertThrows(NullPointerException.class, () -> Session1.mostFrequent(null));

		// Cas d'égalité.
		var resTie = Session1.mostFrequent(List.of(1, 2, 1, 2, 3));
		assertTrue(resTie.isPresent());
		assertTrue(Set.of(1, 2).contains(resTie.get()));
	}

	@Test
	@Order(51)
	void testWithoutDuplicates() {
		var input = List.of(1, 2, 1, 3, 2);
		var expected = List.of(1, 2, 3);

		assertEquals(expected, Session1.withoutDuplicates(input));
		assertEquals(List.of(), Session1.withoutDuplicates(List.of()));

		assertThrows(NullPointerException.class, () -> Session1.withoutDuplicates(null));
	}

	@Test
	@Order(52)
	void testWithoutDuplicatesEfficiency() {
		var size = 100_000;
		var list = IntStream.range(0, size).map(i -> i % 100).boxed().collect(Collectors.toList());

		var result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return Session1.withoutDuplicates(list);
		}, "La méthode withoutDuplicates est trop lente (probablement O(N^2) au lieu de O(N))");

		var expected = IntStream.range(0, 100).boxed().collect(Collectors.toList());

		assertEquals(100, result.size(), "La liste devrait contenir 100 éléments uniques");

		assertEquals(expected, result, "La liste filtrée ne correspond pas au résultat attendu");
	}

	@Test
	@Order(61)
	void testOneOfEachDuplicate() {
		var input = List.of(1, 2, 1, 3, 1, 2);
		var expected = List.of(1, 2);

		assertEquals(expected, Session1.oneOfEachDuplicate(input));
		assertEquals(List.of(), Session1.oneOfEachDuplicate(List.of(1, 2, 3)));

		assertThrows(NullPointerException.class, () -> Session1.oneOfEachDuplicate(null));
	}

	@Test
	@Order(71)
	void testOnlyEvenPosition() {
		var input = List.of(1, 2, 1, 3, 1, 3, 4);
		var expected = Set.of(1, 4);

		assertEquals(Set.of(1), Session1.onlyEvenPosition(List.of(1)));
		assertEquals(Set.of(), Session1.onlyEvenPosition(List.of(1, 1, 1)));
		assertEquals(Set.of(1, 3), Session1.onlyEvenPosition(List.of(1, 2, 3)));
		assertEquals(expected, Session1.onlyEvenPosition(input));

		assertThrows(NullPointerException.class, () -> Session1.onlyEvenPosition(null));
	}

	@Test
	@Order(72)
	void testOnlyEvenPositionEfficiency() {
		var size = 100_000;
		var list = IntStream.range(0, size).boxed().collect(Collectors.toList());

		var result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return Session1.onlyEvenPosition(list);
		}, "La méthode singles est trop lente");

		var set = IntStream.iterate(0, x -> x < size, x -> x + 2).boxed().collect(Collectors.toSet());
		assertEquals(set, result);
	}

	@Test
	@Order(81)
	void testSingles() {
		var input = List.of(1, 2, 1, 3, 4, 2);
		var expected = Set.of(3, 4);

		assertEquals(expected, Session1.singles(input));
		assertEquals(Set.of(), Session1.singles(List.of()));
		assertEquals(Set.of(1), Session1.singles(List.of(1)));
		assertEquals(Set.of(), Session1.singles(List.of(1, 1, 2, 2)));

		assertThrows(NullPointerException.class, () -> Session1.singles(null));
	}

	@Test
	@Order(82)
	void testSinglesEfficiency() {
		var size = 100_000;
		var list = IntStream.range(0, size).boxed().collect(Collectors.toList());
		// Ajoutons quelques doublons à la fin
		list.add(0);
		list.add(1);

		var result = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return Session1.singles(list);
		}, "La méthode singles est trop lente");

		var set = IntStream.range(2, size).boxed().collect(Collectors.toSet());
		assertEquals(set, result);
	}

	@Test
	@Order(101)
	void testAllDistinctInFile(@TempDir Path tempDir) throws IOException {
		var duplicateFile = tempDir.resolve("duplicates.txt");
		Files.write(duplicateFile, List.of("10", "20", "30", "20", "40"));

		var uniqueFile = tempDir.resolve("unique.txt");
		Files.write(uniqueFile, List.of("1", "2", "3", "4", "5"));

		var emptyFile = tempDir.resolve("empty.txt");
		Files.createFile(emptyFile);

		assertFalse(Session1.allDistinctInFile(duplicateFile),
				"Le fichier contient un doublon (20), doit renvoyer false");
		assertTrue(Session1.allDistinctInFile(uniqueFile),
				"Le fichier ne contient que des uniques, doit renvoyer true");
		assertTrue(Session1.allDistinctInFile(emptyFile), "Un fichier vide ne contient pas de doublons");
	}

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

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

	@Test
	@Order(102)
	void testAllDistinctInFileData() throws IOException {
		var path = ensureInProjectOrShares("data/numbers.data");
		assertFalse(Session1.allDistinctInFile(path),
				"Le fichier " + path.getFileName() + " contient un doublon (231), doit renvoyer false");

		path = ensureInProjectOrShares("data/numbers2.data");
		assertTrue(Session1.allDistinctInFile(path),
				"Le fichier " + path.getFileName() + " ne contient que des uniques, doit renvoyer true");

		path = ensureInProjectOrShares("data/numbers4.data");
		assertTrue(Session1.allDistinctInFile(path),
				"Le fichier " + path.getFileName() + " ne contient que des uniques, doit renvoyer true");

		path = ensureInProjectOrShares("data/numbers5.data");
		assertFalse(Session1.allDistinctInFile(path),
				"Le fichier " + path.getFileName() + " contient un doublon (589466429), doit renvoyer false");
	}
	

	@Test
	@Order(103)
	void testAllDistinctInFileBig() throws IOException {
		var path = ensureInProjectOrShares("data/lotsOfNumbers10000.data");
		var result = assertTimeoutPreemptively(Duration.ofMillis(10_000), () -> {
			return Session1.allDistinctInFile(path);
		}, "La méthode allDistinct est trop lente");

		assertFalse(result);
	}

	@Test
	@Order(104)
	void testNumberOfDistinctsNumbersInFileData() throws IOException {
		var path = ensureInProjectOrShares("data/numbers.data");
		assertEquals(6, Session1.numberOfDistinctsNumbersInFile(path));

		path = ensureInProjectOrShares("data/numbers2.data");
		assertEquals(7, Session1.numberOfDistinctsNumbersInFile(path));

		path = ensureInProjectOrShares("data/numbers4.data");
		assertEquals(49538, Session1.numberOfDistinctsNumbersInFile(path));

		path = ensureInProjectOrShares("data/numbers5.data");
		assertEquals(50498, Session1.numberOfDistinctsNumbersInFile(path));
	}

	@Test
	@Order(106)
	void testNumberOfDistinctsNumbersInFileDataBig1() throws IOException {
		var path = ensureInProjectOrShares("data/lotsOfNumbers10000.data");
		var result = assertTimeoutPreemptively(Duration.ofMillis(30_000), () -> {
			return Session1.numberOfDistinctsNumbersInFile(path);
		}, "La méthode allDistinct est trop lente");

		assertEquals(10000, result);
	}
	

	@Test
	@Order(107)
	void testNumberOfDistinctsNumbersInFileDataBig2() throws IOException {
		var path = ensureInProjectOrShares("data/lotsOfNumbers100000.data");
		var result = assertTimeoutPreemptively(Duration.ofMillis(30_000), () -> {
			return Session1.numberOfDistinctsNumbersInFile(path);
		}, "La méthode allDistinct est trop lente");

		assertEquals(100000, result);
	}

	@Test
	@Order(108)
	void testNumberOfDistinctsNumbersInFileDataBigBig() throws IOException {
		var path = ensureInProjectOrShares("data/lotsOfNumbers.data");
		
		var result = assertTimeoutPreemptively(Duration.ofMillis(180_000), () -> {
			return Session1.numberOfDistinctsNumbersInFile(path);
		}, "La méthode allDistinct est trop lente");

		assertEquals(446271120, result);
	}

	@Test
	@Order(109)
	void testAverageInFileData() throws IOException {
		var path = ensureInProjectOrShares("data/transactions1.data");
		assertEquals(60, Session1.averageInFile(path));

		path = ensureInProjectOrShares("data/transactions2.data");
		assertEquals(69, Session1.averageInFile(path));

		path = ensureInProjectOrShares("data/transactions3.data");
		assertEquals(49, Session1.averageInFile(path));

	}

	private static List<MinMax> minMaxAnswer(Path path) throws IOException {
		try (var reader = Files.newBufferedReader(path)) {
			var result = new ArrayList<MinMax>();
			String line;
			while ((line = reader.readLine()) != null) {
				var numbers = line.split(" ");
				var min = Integer.parseInt(numbers[0]);
				var max = Integer.parseInt(numbers[1]);
				result.add(new MinMax(min, max));
			}
			return result;
		}
	}

	@Test
	@Order(110)
	void testminMaxInFileData() throws IOException {
		var path = ensureInProjectOrShares("data/transactions1.data");
		assertEquals(minMaxAnswer(ensureInProjectOrShares("answer/" + path.getFileName())),
				Session1.minMaxInFile(path));

		path = ensureInProjectOrShares("data/transactions2.data");
		assertEquals(minMaxAnswer(ensureInProjectOrShares("answer/" + path.getFileName())),
				Session1.minMaxInFile(path));

		path = ensureInProjectOrShares("data/transactions3.data");
		assertEquals(minMaxAnswer(ensureInProjectOrShares("answer/" + path.getFileName())),
				Session1.minMaxInFile(path));

	}
}