package fr.uge.code.camp;

import org.junit.jupiter.api.Test;

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.*;
import java.util.stream.IntStream;

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.Grid.Response;
import fr.uge.code.camp.Grid.Ship;
import fr.uge.code.camp.Grid.Response.Hit;
import fr.uge.code.camp.Grid.Response.Nothing;
import fr.uge.code.camp.Grid.Response.Sunk;

import static org.junit.jupiter.api.Assertions.*;
import static fr.uge.code.camp.Grid.Direction.*;

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

	private static final Response NOTHING = new Nothing();
	private static final Response HIT = new Hit();

	private static Response sunk(Ship ship1) {
		return new Sunk(ship1);
	}

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

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

	@Test
	@Order(11)
	void testBuildingBoard() {
		int lengthX = 10, lengthY = 10;
		var grid = new Grid(lengthX, lengthY);
		assertEquals(0, grid.numberOfShips());

		var ship1 = new Grid.Ship("Ship1", UP, 5);
		assertTrue(grid.addShip(ship1, 2, 3)); // and if a ship is added twice ?
		assertEquals(1, grid.numberOfShips());
		assertFalse(grid.addShip(ship1, 2, 3)); // ?
		assertEquals(1, grid.numberOfShips());
		var ship2 = new Grid.Ship("Ship2", UP, 5);
		assertTrue(grid.addShip(ship2, 3, 3));
		assertEquals(2, grid.numberOfShips());
		var ship3 = new Grid.Ship("Ship3", RIGHT, 2);
		assertTrue(grid.addShip(ship3, 7, 3));
		assertEquals(3, grid.numberOfShips());
		var ship4 = new Grid.Ship("Ship4", UP, 4);
		assertTrue(grid.addShip(ship4, 1, 3));
		assertEquals(4, grid.numberOfShips());
		var ship5 = new Grid.Ship("Ship5", UP, 4);
		assertFalse(grid.addShip(ship5, 1, 2));
		assertEquals(4, grid.numberOfShips());

		grid = new Grid(3, 3);
		assertFalse(grid.addShip(ship3, 7, 3));
		assertEquals(0, grid.numberOfShips());

	}

	@Test
	@Order(12)
	void testBuildingBoardMedium() {
		int lengthX = 1_000, lengthY = 100_000;
		int size = lengthX;
		var grid = new Grid(lengthX, lengthY);

		var res = assertTimeoutPreemptively(Duration.ofMillis(500), () -> IntStream.range(0, size).mapToObj(i -> {
			var ship = new Grid.Ship("Ship" + i, UP, 1 + (i % 100));
			return grid.addShip(ship, i, 10 * i);
		}).allMatch(p -> p));
		assertTrue(res);
		assertEquals(size, grid.numberOfShips());
	}

	@Test
	@Order(13)
	void testBuildingBoardEfficiency() {
		int lengthX = 10_000, lengthY = 1_000_000;
		int size = lengthX;
		var grid = new Grid(lengthX, lengthY);

		var res = assertTimeoutPreemptively(Duration.ofMillis(500), () -> IntStream.range(0, size).mapToObj(i -> {
			var ship = new Grid.Ship("Ship" + i, Grid.Direction.UP, 1 + (i % 100));
			return grid.addShip(ship, i, 10 * i);
		}).allMatch(p -> p));
		assertTrue(res);
	}

	@Test
	@Order(14)
	void testBuildingBoardFileSmall(@TempDir Path tempDir) throws IOException {
		Path dir = ensureInProjectOrShares("data/TestGridSmall");
		var files = Files.list(dir).filter(path -> path.toString().endsWith(".data")).toList();

		for (var dataPath : files) {		
			var fileName = dataPath.getFileName().toString();
			if(fileName.startsWith("._")) {
				continue;
			}
			var outPath = dir.resolve(fileName.replace(".data", ".out"));

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

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

			int expected = Integer.parseInt(Files.readString(outPath).trim());
			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session3.buildGrid(dataPath));
			assertEquals(expected, result.numberOfShips(), "problem with file " + dataPath);
		}
	}

	@Test
	@Order(21)
	void testBomb() {
		int lengthX = 10, lengthY = 10;
		var grid = new Grid(lengthX, lengthY);
		var ship1 = new Grid.Ship("ship1", Grid.Direction.UP, 2);
		var ship2 = new Grid.Ship("ship2", Grid.Direction.UP, 5);
		var ship3 = new Grid.Ship("ship3", Grid.Direction.RIGHT, 2);
		grid.addShip(ship1, 2, 3);
		grid.addShip(ship2, 3, 3);
		grid.addShip(ship3, 7, 3);
		assertEquals(3, grid.numberOfShips());

		assertEquals(HIT, grid.bomb(2, 3));
		assertEquals(NOTHING, grid.bomb(2, 3));
		assertEquals(sunk(ship1), grid.bomb(2, 4));
		assertEquals(2, grid.numberOfShips());
		assertEquals(NOTHING, grid.bomb(2, 4));
		assertEquals(NOTHING, grid.bomb(1, 4));
		assertEquals(HIT, grid.bomb(8, 3));
		assertEquals(NOTHING, grid.bomb(6, 3));
		assertEquals(2, grid.numberOfShips());
	}

	@Test
	@Order(22)
	void testBombMedium() {
		int lengthX = 100_000, lengthY = 10_000;

		var size = 10_000;
		var grid = new Grid(lengthX, lengthY);
		IntStream.range(0, size).forEach(i -> {
			grid.addShip(new Grid.Ship("Ship" + i, Grid.Direction.UP, 1), 10 * i, 0);
		});

		var result1 = assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
			return grid.bomb(1_000, 1_000);
		}, "La méthode bomb est trop lente");

		assertEquals(NOTHING, result1);

		var result2 = assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
			return grid.bomb(1_000, 0);
		}, "La méthode bomb est trop lente");

		assertInstanceOf(Grid.Response.Sunk.class, result2);
	}

	@Test
	@Order(23)
	void testBombEfficiency() {
		int lengthX = 1_000_000, lengthY = 1_000_000;

		var size = 10_000;
		var grid = new Grid(lengthX, lengthY);
		IntStream.range(0, size).forEach(i -> {
			grid.addShip(new Grid.Ship("Ship" + i, Grid.Direction.UP, 1), 10 * i, 0);
		});

		var result1 = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return grid.bomb(1_000, 1_000);
		}, "La méthode bomb est trop lente");

		assertEquals(NOTHING, result1);

		var result2 = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return grid.bomb(1_000, 0);
		}, "La méthode bomb est trop lente");

		assertInstanceOf(Grid.Response.Sunk.class, result2);
	}

	@Test
	@Order(24)
	void testBombEfficiency2() {
		int lengthX = 1_000_000_000, lengthY = 1_000_000_000;

		var size = 10_000;
		var grid = new Grid(lengthX, lengthY);
		IntStream.range(1, size + 1).forEach(i -> {
			grid.addShip(new Grid.Ship("Ship" + i, Grid.Direction.RIGHT, 100), 100 * i, lengthY - 1 - 10 * i);
		});

		var result1 = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return grid.bomb(1_000, 1_000);
		}, "La méthode bomb est trop lente");

		assertEquals(NOTHING, result1);

		var result2 = assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
			return grid.bomb(1_000_000, lengthY - 100_001);
		}, "La méthode bomb est trop lente");

		assertEquals(HIT, result2);
	}

	// bomb can be done more efficiently even, rather than having a high memory
	// footprint...
	// we can look in a radius around the point

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

			var line = Files.readString(outPath).trim();

			var expected = Arrays.stream(line.split(" ")).mapToInt(Integer::parseInt).toArray();
			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session3.bombGrid(dataPath));
			var nothings = result.stream().filter(r -> r instanceof Nothing).count();
			assertEquals(expected[0], nothings, "problem with file " + dataPath);
			var sunks = result.stream().filter(r -> r instanceof Sunk).count();
			assertEquals(expected[2], sunks, "problem with file " + dataPath);
		}
	}

	@Test
	@Order(31)
	void testAdvanceTime() {
		int lengthX = 10, lengthY = 10;
		var grid = new Grid(lengthX, lengthY);
		assertEquals(0, grid.numberOfShips());

		var ship1 = new Grid.Ship("ship1", Grid.Direction.UP, 5);
		assertTrue(grid.addShip(ship1, 0, 2));
		assertEquals(1, grid.numberOfShips());

		grid.advanceTime();

		var ship2 = new Grid.Ship("ship2", Grid.Direction.UP, 1);
		assertFalse(grid.addShip(ship2, 0, 3));
		assertEquals(1, grid.numberOfShips());
		assertTrue(grid.addShip(ship2, 0, 2));
		assertEquals(2, grid.numberOfShips());

		var ship3 = new Grid.Ship("ship3", Grid.Direction.RIGHT, 9);
		assertTrue(grid.addShip(ship3, 1, 0));
		assertEquals(3, grid.numberOfShips());

	}

	@Test
	@Order(32)
	void testAdvanceTimeOrder() {
		int lengthX = 10, lengthY = 10;
		var grid = new Grid(lengthX, lengthY);

		var ship1 = new Grid.Ship("ship1", Grid.Direction.UP, 1);
		assertTrue(grid.addShip(ship1, 1, 0));

		var ship2 = new Grid.Ship("ship2", Grid.Direction.RIGHT, 1);
		assertTrue(grid.addShip(ship2, 0, 1));

		grid.advanceTime();

		assertEquals(sunk(ship1), grid.bomb(1, 1));
	}

	@Test
	@Order(33)
	void testAdvanceTimeAndBomb() {
		int lengthX = 10, lengthY = 10;
		var grid = new Grid(lengthX, lengthY);

		var ship1 = new Grid.Ship("ship1", Grid.Direction.UP, 2);
		assertTrue(grid.addShip(ship1, 1, 0));

		assertEquals(HIT, grid.bomb(1, 1));

		grid.advanceTime();

		assertEquals(NOTHING, grid.bomb(1, 2));
		assertEquals(sunk(ship1), grid.bomb(1, 1));
	}

	// test that they move simultaneously ?
	@Test
	@Order(34)
	void testAdvanceTimeOrderEfficiency() {
		int lengthX = 1_000_000_000, lengthY = 1_000_000_000;

		var size = 10_000;
		var grid = new Grid(lengthX, lengthY);
		IntStream.range(0, size).forEach(i -> {
			grid.addShip(new Grid.Ship("Ship" + i, Grid.Direction.RIGHT, 100), // if ships are big, this is bad
					1000 * i, lengthY - 1 - 1000 * i);
		});

		assertTimeoutPreemptively(Duration.ofMillis(1500), grid::advanceTime, "La méthode advanceTime est trop lente");

		var result2 = assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
			return grid.bomb(1_000_001, lengthY - 1_000_001);
		}, "La méthode bomb est trop lente");

		assertEquals(HIT, result2);

		var result3 = assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
			return grid.bomb(1_000_000, lengthY - 1_000_001);
		}, "La méthode bomb est trop lente");

		assertEquals(NOTHING, result3);
	}
	
	@Test
	@Order(35)
	void testAdvanceTimeFileSmall(@TempDir Path tempDir) throws IOException {
		Path dir = ensureInProjectOrShares("data/TestBombsTurnsSmall");
		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"));

			var line = Files.readString(outPath).trim();

			var expected = Arrays.stream(line.split(" ")).mapToInt(Integer::parseInt).toArray();
			var result = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> Session3.simulateGrid(dataPath));
			var nothings = result.stream().filter(r -> r instanceof Nothing).count();
			assertEquals(expected[0], nothings, "problem with file " + dataPath);
			var sunks = result.stream().filter(r -> r instanceof Sunk).count();
			assertEquals(expected[2], sunks, "problem with file " + dataPath);
		}
	}
	
	@Test
	@Order(36)
	void testAdvanceTimeFileLarge(@TempDir Path tempDir) throws IOException {
		Path dir = ensureInProjectOrShares("data/TestBombsTurnsLarge");
		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"));

			var line = Files.readString(outPath).trim();

			var expected = Arrays.stream(line.split(" ")).mapToInt(Integer::parseInt).toArray();
			var result = assertTimeoutPreemptively(Duration.ofMillis(10_000), () -> Session3.simulateGrid(dataPath));
			var nothings = result.stream().filter(r -> r instanceof Nothing).count();
			assertEquals(expected[0], nothings, "problem with file " + dataPath);
			var sunks = result.stream().filter(r -> r instanceof Sunk).count();
			assertEquals(expected[2], sunks, "problem with file " + dataPath);
		}
	}

	@Test
	@Order(51)
	void testFindPath() {
		int lengthX = 1_000_000_000, lengthY = 1_000_000_000;

		var size = 10_000;
		var grid = new Grid(lengthX, lengthY);
		var list = IntStream.range(0, size).mapToObj(i -> {
			var ship = new Grid.Ship("Ship" + i, Grid.Direction.RIGHT, 100);
			grid.addShip(ship, // if ships are big, this is bad
					100 * i, 0);
			return ship;
		}).toList();

		var result1 = assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
			return grid.findPath(list.getFirst(), list.getLast());
		}, "La méthode findPath est trop lente");

		assertTrue(!result1.isEmpty());
		assertEquals(result1, list);
	}
}