package ch.zhaw.catan;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.awt.*;
import java.util.*;
import java.util.List;


/***
 * TODO Write your own tests in this class.
 *
 * Note:  Have a look at {@link ch.zhaw.catan.games.ThreePlayerStandard}. It can be used
 * to get several different game states.
 *
 */

/**
 * @class SiedlerGameTest
 *
 * contains all of the test cases for SiedlerGame class.
 * The Test cases are categorized into
 *  - Positive TestCases
 *      Tests the methods of SiedlerGame with the intended values
 *
 *  - Negative TestCases
 *      Tests the methods of SiedlerGame with values, that results to errors and failure
 *
 *  - SystemTestCases
 *      Tests the coordination of all methods and simulates a game with all moves programmed
 *      Checks if the methods are being executed and the values are set correctly
 */
public class SiedlerGameTest {

    private final static int DEFAULT_WINPOINTS = 5;
    private final static int DEFAULT_PLAYERAMOUNT = 4;

    /**
     * Property START_SETTLEMENT_POSITIONS
     *
     * Lists all positions of Settlement for every faction in the initialization phase.
     * Each faction is assigned to a list of specific settlements, which represents as a list of points
     */
    private final static Map<Config.Faction, ArrayList<Point>> START_SETTLEMENT_POSITIONS = Map.of(
            Config.Faction.BLUE,    new ArrayList<>(List.of(new Point(4, 4), new Point(5, 7))),
            Config.Faction.RED,     new ArrayList<>(List.of(new Point(10, 4), new Point(9, 7))),
            Config.Faction.GREEN,   new ArrayList<>(List.of(new Point(4, 18), new Point(5, 15))),
            Config.Faction.YELLOW,  new ArrayList<>(List.of(new Point(10, 18), new Point(9, 15)))
    );

    /**
     * Property START_ROADS_POSITION
     *
     * Lists all endpoints of roads for every faction in the initialization phase.
     * Each faction is assigned to a specific Road, which represents as a tuple of 2 points
     */
    private final static Map<Config.Faction, ArrayList<Tuple<Point, Point>>> START_ROADS_POSITIONS = Map.of(
            Config.Faction.BLUE, new ArrayList<>(List.of(
                    new Tuple<>(new Point(4, 4), new Point(4, 6)), new Tuple<>(new Point(4, 6), new Point(5, 7)))),
            Config.Faction.RED, new ArrayList<>(List.of(
                    new Tuple<>(new Point(10, 4), new Point(10, 6)), new Tuple<>(new Point(10, 6), new Point(9, 7)))),
            Config.Faction.GREEN, new ArrayList<>(List.of(
                    new Tuple<>(new Point(4, 18), new Point(4, 16)), new Tuple<>(new Point(4, 16), new Point(5, 15)))),
            Config.Faction.YELLOW, new ArrayList<>(List.of(
                    new Tuple<>(new Point(10, 18), new Point(10, 16)), new Tuple<>(new Point(10, 16), new Point(9, 15))))
    );

    /**
     * This test class contains all positive test cases.
     */
    @Nested
    @DisplayName("Positive test cases")
    class PositiveTestcases {

        /**
         * This test will check, if the game initializes with all 4 factions.
         * All factions should have no resources
         */
        @ParameterizedTest(name = "Test with {arguments} players")
        @ValueSource(ints = {2, 3, 4})
        @DisplayName("Game initializing with different amount of players within the domain (Players have no resources), expected ok")
        public void TestGameInitializationWithAllPlayercounts(int playerAmount) {
            SiedlerGame game = new SiedlerGame(DEFAULT_WINPOINTS, playerAmount);

            for (Config.Faction faction: game.getPlayerFactions()) {
                HashMap<Config.Resource, Integer> resources = game.getCurrentPlayerResource();

                Assertions.assertEquals(0, resources.get(Config.Resource.BRICK));
                Assertions.assertEquals(0, resources.get(Config.Resource.GRAIN));
                Assertions.assertEquals(0, resources.get(Config.Resource.LUMBER));
                Assertions.assertEquals(0, resources.get(Config.Resource.ORE));
                Assertions.assertEquals(0, resources.get(Config.Resource.WOOL));

                game.switchToNextPlayer();
            }
        }

        /**
         *
         */
        @Test
        @DisplayName("Test")
        public void TestHandle7() {
            SiedlerGame game = startGame();
            //todo
        }
    }

    /**
     * @Class NegAtiveTestcases
     *
     * contains all negative test cases
     */
    @Nested
    @DisplayName("Negative test cases")
    class NegativeTestcases {

        /**
         * Tests if siedlergame will start with one or five players.
         * 1 Player is below the minimum
         * 5 Players are above the maximum
         */
        @ParameterizedTest(name = "Test with {arguments} players")
        @ValueSource(ints = {1, 5})
        @DisplayName("Starting Siedler game with one player or 5 players, expects fail")
        public void startSiedlerGameWithOnePlayerorMoreThanMaximum(int playerAmount) {
            Exception exc = assertThrows(IllegalArgumentException.class, () -> {
                new SiedlerGame(DEFAULT_WINPOINTS, playerAmount);
            });

            Assertions.assertEquals(IllegalArgumentException.class, exc.getClass());
        }

        /**
         * Tests if the game crashes when passing an amount of points, that is below the minimum amount
         */
        @Test
        @DisplayName("Starting Siedler game with one player, expects fail")
        public void startSiedlerGameWithLowerThanMinimumWinPoints() {

            Exception exc = assertThrows(IllegalArgumentException.class, () -> {
                SiedlerGame game = new SiedlerGame(1, 4);
            });

            Assertions.assertEquals(IllegalArgumentException.class, exc.getClass());
        }


        /**
         * This testcase will test, if the methods placeInitialRoad and placeInitialSettlement are overwritting
         * already occupied positions if the same faction or any other faction are calling the method with the same positions
         *
         * Expected: Method placeInitialRoad placeInitialSettlement should return false, independent of the current faction playing
         */
        @Test
        @DisplayName("Test placeInitialRoad and placeInitialSettlement with already occupied positions")
        public void testPlaceInitialStructuresInAlreadyOccupiedPositions() {
            SiedlerGame game = startGame();

            Point settlementPoint = START_SETTLEMENT_POSITIONS.get(Config.Faction.BLUE).get(1);

            Assertions.assertTrue(game.placeInitialSettlement(settlementPoint, false));
            Assertions.assertFalse(game.placeInitialSettlement(settlementPoint, false));

            Tuple<Point, Point> roadEndpoints = START_ROADS_POSITIONS.get(Config.Faction.BLUE).get(1);

            Assertions.assertTrue(game.placeInitialRoad(roadEndpoints.first, roadEndpoints.second));
            Assertions.assertFalse(game.placeInitialRoad(roadEndpoints.first, roadEndpoints.second));

            // Switch to other player to see if the occupied fields can be overwritten, when switched to the next player/faction
            game.switchToNextPlayer();

            Assertions.assertFalse(game.placeInitialRoad(roadEndpoints.first, roadEndpoints.second));
            Assertions.assertFalse(game.placeInitialSettlement(settlementPoint, false));

        }

        /**
         * This testcase will test, if the methods buildRoad, buildCity buildSettlement are overwritting
         * already occupied positions if the same faction or any other faction are calling the method with the same positions
         *
         * Expected: Method placeInitialRoad placeInitialSettlement should return false, independent of the current faction playing
         */
        @Test
        @DisplayName("Test buildRoad, buildCity and buildSettlement with already occupied positions")
        public void testBuildStructuresOnOccupiedFields() {

        }
    }

    /**
     * @Class SystemTestCases
     *
     * This class simulates a running game and tests multiple sequences of this game.
     */
    @Nested
    @DisplayName("System test cases")
    class SystemTestcases {

        /**
         * TestS if the players can place initial settlements and roads
         */
        @Test
        @DisplayName("2 Players initialize a settlement and position")
        public void TestGameInSetupPhase() {
            SiedlerGame game = startGame();

            Assertions.assertTrue(game.placeInitialSettlement(START_SETTLEMENT_POSITIONS.get(game.getCurrentPlayerFaction()).get(0), false));
            Tuple<Point, Point> road1 = START_ROADS_POSITIONS.get(game.getCurrentPlayerFaction()).get(0);
            Assertions.assertTrue(game.placeInitialRoad(road1.first, road1.second));

            game.switchToNextPlayer();

            Assertions.assertTrue(game.placeInitialSettlement(START_SETTLEMENT_POSITIONS.get(game.getCurrentPlayerFaction()).get(1), false));
            Tuple<Point, Point> road2 = START_ROADS_POSITIONS.get(game.getCurrentPlayerFaction()).get(1);
            Assertions.assertTrue(game.placeInitialRoad(road2.first, road2.second));

        }

        /**
         * tests, if the players can do all pf the actions in the building phase
         */
        @Test
        public void TestGameAfterSetupPhase() {
            SiedlerGame game = gameAfterSetupPhase();

            System.out.println(game.getBoard().toString());

            throwDiceSeveralTimes(game, 5, 5);
            throwDiceSeveralTimes(game, 5, 5);
            throwDiceSeveralTimes(game, 5, 5);
            throwDiceSeveralTimes(game, 5, 5);
            throwDiceSeveralTimes(game, 5, 5);

            //game.switchToNextPlayer();

            //throwDiceSeveralTimes(game, 6, 5);
            //throwDiceSeveralTimes(game, 6, 5);
            //throwDiceSeveralTimes(game, 6, 5);
            //throwDiceSeveralTimes(game, 6, 5);
            //throwDiceSeveralTimes(game, 6, 5);

            soutDistribution(game);
        }


        /**
         * tests, if a player wins when reached the amount of points.
         */
        @Test
        @DisplayName("End of Game test")
        public void TestGameEnd() {

        }

        //TEMPORARY METHOD, WILL BE REMOVED SOON
        private static void soutDistribution(SiedlerGame game) {
            System.out.println("\n\nVerteilung \n\n");
            for (int i = 0 ; i < game.getPlayerFactions().size(); i++) {
                HashMap<Config.Resource, Integer> resources = game.getCurrentPlayerResource();
                System.out.println(game.getCurrentPlayerFaction().toString() + "\n");
                System.out.println(" BRICK " + resources.get(Config.Resource.BRICK).toString());
                System.out.println(" GRAIN " + resources.get(Config.Resource.GRAIN));
                System.out.println(" LUMBER " + resources.get(Config.Resource.LUMBER));
                System.out.println(" ORE " + resources.get(Config.Resource.ORE));
                System.out.println(" WOOL " + resources.get(Config.Resource.WOOL));

                game.switchToNextPlayer();
            }
        }
    }

    /**
     * Initializes a game with the default values of winning points and amount of player
     *
     * @return
     */
    private static SiedlerGame startGame() {
        return new SiedlerGame(DEFAULT_WINPOINTS, DEFAULT_PLAYERAMOUNT);
    }

    /**
     * This method will set the game to the timeline after the setup.
     * Players have already set each 2 Settlements and 2 Roads.
     *
     * @return SiedlerGame  The game after the setup phase
     */
    private static SiedlerGame gameAfterSetupPhase() {
        SiedlerGame game = startGame();

        for (int i = 0; i < game.getPlayerFactions().size(); i++) {
            Config.Faction f = game.getCurrentPlayerFaction();

            game.placeInitialSettlement(START_SETTLEMENT_POSITIONS.get(f).get(0), false);
            Tuple<Point, Point> firstRoad = START_ROADS_POSITIONS.get(f).get(0);
            game.placeInitialRoad(firstRoad.first, firstRoad.second);

            game.switchToNextPlayer();
        }
        for (int i = 0; i < game.getPlayerFactions().size(); i++) {
            game.switchToPreviousPlayer();
            Config.Faction f = game.getCurrentPlayerFaction();

            game.placeInitialSettlement(START_SETTLEMENT_POSITIONS.get(f).get(1), false);
            Tuple<Point, Point> secondRoad = START_ROADS_POSITIONS.get(f).get(1);
            game.placeInitialRoad(secondRoad.first, secondRoad.second);
        }

        System.out.println(game.getBoard().toString());

        return game;
    }

    /**
     * This method will call the method "throwDice" multiple times and passes the set value of the dice
     *
     * @param game              Type SiedlerGame, the game itself in a running state
     * @param dice              Type int, Set Value of dice
     * @param amountDiceThrows  Type int, The amount of dice throws
     */
    private static void throwDiceSeveralTimes(SiedlerGame game, int dice, int amountDiceThrows) {

        //System.out.println(game.getCurrentPlayerFaction().toString() + " got " + dice + " and throw " + amountDiceThrows + " times");

        for (int i = 0; i < amountDiceThrows; i++) {
            game.throwDice(dice);
        }
    }

    /**
     * This method will setup a game to a state before a player has the points
     * for being able to win the game
     *
     * @return SiedlerGame which is near the
     */
    private static SiedlerGame gameNearEnd() {
        SiedlerGame game = gameAfterSetupPhase();

        return game;
    }

}