import java.util.Properties;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import java.io.PrintStream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import org.junit.Test;

import com.codingame.gameengine.runner.MultiplayerGameRunner;
import com.codingame.gameengine.runner.dto.*;

public class TrollTest implements Cloneable {

    // ==================== Game Runner encapsulation

    private int league = 1; // @#$%^&* this parameter is *global* despite API!
    private Long seed;
    private Integer roadLength;
    private Integer initialStones;

    private TrollTest branch() {
        try { return (TrollTest) clone(); }
        catch (CloneNotSupportedException e) { throw new InternalError(e); }
    }
    private TrollTest setLeague(int l) { league = l; return this; }
    private TrollTest setSeed(long s) { seed = s; return this; }
    private TrollTest setRoadLength(int l) { roadLength = l; return this; }
    private TrollTest setInitialStones(int s) { initialStones = s; return this; }

    private GameResult runGame(String left, String right) {
        MultiplayerGameRunner gameRunner = new MultiplayerGameRunner();
        gameRunner.setLeagueLevel(league); // mandatory
        if (seed != null) gameRunner.setSeed(seed); else gameRunner.setSeed(0l);

        Properties gameParameters = new Properties();
        gameRunner.setGameParameters(gameParameters);
        if (roadLength != null)
            gameParameters.setProperty("roadLength", roadLength.toString());
        if (initialStones != null)
            gameParameters.setProperty("initialStones",
                                       initialStones.toString());

        gameRunner.addAgent(left);
        gameRunner.addAgent(right);

        return gameRunner.simulate();
    }

    // ==================== Single-round game result testing

    @Test
    public void drawGame() {
        assertIsDraw(runGame(agentOne, agentOne));
    }

    @Test
    public void defeatGames() {
        assertIsDefeat(runGame(agentCrash, agentCrash));
        assertIsDefeat(runGame(agentGarbage, agentGarbage));
    }

    @Test
    public void simpleGames() {
        // wins by direct reach, no fastforward
        assertWinLose(agentTwo, agentOne);

        // win by fastforward after loser exhaustion
        assertWinLose(agentOne, agentAllIn);

        // win despite fastforward after winner exhaustion
        // (harder to construct :-D )
        assertWinLose(agent(1,2,2,2,8), agent(3,1,1,1,8));
    }

    // ==================== Cheating games (league 1 perk)

    @Test
    public void cheatingGames() {
        // win by cheating (works in league 1, which is the default)
        assertWinLose(agentCheat, agentTwo);

        // league 2 randomizes: we should be able to get a win and a loss
        branch().setLeague(2).setSeed(0)
            .setRoadLength(6).setInitialStones(15)
            .assertWinLose(agentCheat, agentTwo);
        branch().setLeague(2).setSeed(1)
            .setRoadLength(6).setInitialStones(15)
            .assertWinLose(agentTwo, agentCheat);

        // league 3 forbids
        Random r = new Random();
        for (int i = 0; i < 16; i++) {
            branch().setLeague(3).setSeed(r.nextLong())
                .setRoadLength(6).setInitialStones(15)
                .assertWinLose(agentTwo, agentCheat);
        }
    }

    // ==================== Map generation testing

    static private class GameMap {
        int roadLength;
        int initialStones;
        @Override public boolean equals(Object gm) {
            if (gm instanceof GameMap) {
                return roadLength == ((GameMap) gm).roadLength
                    && initialStones == ((GameMap) gm).initialStones;
            }
            else return false;
        }
        @Override public int hashCode() {
            return roadLength << 16 | initialStones;
        }
    }

    @Test
    public void maps() {
        TrollTest test = branch();

        // league 1, expect a single map
        for (long s = 0; s < 16; s++) {
            test.setSeed(s);
            GameMap map = test.measureMap();
            assertEquals("Level 1 roadLength", 6, map.roadLength);
            assertEquals("Level 1 initialStones", 15, map.initialStones);
        }

        // league 2, expect one of four maps
        test.setLeague(2);
        HashSet<GameMap> maps = new HashSet<GameMap>();
        Random r = new Random();
        for (int i = 0; i < 16; i++) {
            test.setSeed(r.nextLong());
            GameMap map = test.measureMap();
            maps.add(map);
        }
        assertEquals("Level 2 has four maps", 4, maps.size());

        // league 3, maps simply have constraints
        test.setLeague(3);
        for (long s = 0; s < 16; s++) {
            test.setSeed(s);
            GameMap map = test.measureMap();
            assertTrue("Level 3 road length is at least 6",
                       map.roadLength >= 6);
            assertTrue("Level 3 road length is at most 14",
                       map.roadLength <= 14);
            assertTrue("Level 3 road length is even",
                       map.roadLength % 2 == 0);
            assertTrue("Level 3 stones is at least 15",
                       map.initialStones >= 15);
            assertTrue("Level 3 stones is at most 50",
                       map.initialStones <= 50);
        }
    }

    /*
     * There's currently no way to extract the parameters (and verify
     * them!) from a game.  So for now we measure it from other
     * traces.
     */
    private GameMap measureMap() {
        GameMap result = new GameMap();

        // roadLength is twice the number of moves a troll takes
        // before the end of the game in a
        // position-independent-strategy game.
        GameResult game = runGame(agentOne, agentTwo);
        result.roadLength = 2 * game.summaries.stream()
            .filter(s -> s.contains("walks"))
            .collect(Collectors.counting()).intValue();

        // initialStones is the number of times a troll stands still
        // in a one-stone-throw draw.
        game = runGame(agentOne, agentOne);
        result.initialStones = game.summaries.stream()
            .filter(s -> s.contains("still"))
            .collect(Collectors.counting()).intValue();

        return result;
    }

    // ==================== Common test agents

    // great thanks to @dbdr for the intense moral support leading to
    // the following acceptance of failure:
    static private final String agentOne = "yes 1";
    static private final String agentTwo = "yes 2";
    static private final String agentAllIn = "yes 15";
    static private final String agentCrash = "false"; // SLOW, try and avoid
    static private final String agentGarbage = "yes assuredly_not_an_int";
    static private final String agentCheat = agent(-100,25,25,25,25);

    static private String agent(int... tosses) {
        String cmd = "echo -e ";
        for (int i = 0; i < tosses.length; i++) {
            if (i > 0) cmd += "\\n";
            cmd += tosses[i];
        }
        return cmd;
    }

    // ==================== Debug routines

    static void dumpGameResult(PrintStream p, GameResult gameResult) {
        for (AgentDto agent : gameResult.agents) dumpAgent(p, agent);
        dumpMap(p, "errors", gameResult.errors);
        dumpString(p, "failCause", gameResult.failCause);
        dumpMap(p, "ids", gameResult.ids);
        dumpString(p, "metadata", gameResult.metadata);
        dumpMap(p, "outputs", gameResult.outputs);
        dumpList(p, "summaries", gameResult.summaries);
        dumpList(p, "tooltips", gameResult.tooltips);
        // dumpList(p, "views", gameResult.views);
    }

    static private <V> void dumpList(PrintStream p, String tag, List<V> list) {
        ListIterator i = list.listIterator();
        while (i.hasNext()) {
            if (tag != null) p.print(tag + " ");
            p.print(i.nextIndex() + ": ");
            dumpGeneric(p, i.next());
        }
    }

    static private <K,V> void dumpMap(PrintStream p, String tag, Map<K,V> map) {
        for (Map.Entry kv : map.entrySet()) {
            p.print(tag + " for " + kv.getKey() + ": ");
            dumpGeneric(p, kv.getValue());
        }
    }

    static private <E,V> void dumpGeneric(PrintStream p, E v) {
        if (v instanceof List) {
            dumpList(p, null, (List<?>) v);
        }
        else {
            p.println(v);
        }
    }

    static private void dumpString(PrintStream p, String tag, String msg) {
        p.println(tag + ": " + msg);
    }

    static void dumpAgent(PrintStream p, AgentDto agent) {
        p.println("[agent] " + agent.agentId + ": " + agent.avatar + " " + agent.index + " " + agent.name);
    }

    // ==================== Test assertions

    private void assertWinLose(String winner, String loser) {
        assertLeftWin(runGame(winner, loser));
        assertRightWin(runGame(loser, winner));
    }

    static void assertLeftWin(GameResult gameResult) {
        assertLeftWin(gameResult, false);
    }
    static void assertLeftWin(GameResult gameResult, boolean strict) {
        int[] scores = assertTwoScores(gameResult);
        if (scores == null) return;

        int s1 = scores[0], s2 = scores[1];
        assertTrue("Left player has higher score than right player", s1 > s2);
        if (strict) assertTrue("Right player isn't disqualified", s2 >= 0);
    }

    static void assertRightWin(GameResult gameResult) {
        assertRightWin(gameResult, false);
    }
    static void assertRightWin(GameResult gameResult, boolean strict) {
        int[] scores = assertTwoScores(gameResult);
        if (scores == null) return;

        int s1 = scores[0], s2 = scores[1];
        assertTrue("Right player has higher score than right player", s2 > s1);
        if (strict) assertTrue("Left player isn't disqualified", s1 >= 0);
    }

    static void assertIsDraw(GameResult gameResult) {
        int[] scores = assertTwoScores(gameResult);
        if (scores == null) return;

        int s1 = scores[0], s2 = scores[1];
        assertEquals("Player scores are equal", s1, s2);
        assertTrue("Player scores are non-negative", s1 >= 0);
    }

    static void assertIsDefeat(GameResult gameResult) {
        int[] scores = assertTwoScores(gameResult);
        if (scores == null) return;

        int s1 = scores[0], s2 = scores[1];
        assertTrue("First player score is negative", s1 < 0);
        assertTrue("Second player score is negative", s2 < 0);
    }

    static int[] assertTwoScores(GameResult gameResult) {
        Map<Integer,Integer> scores = gameResult.scores;
        assertEquals("Two scores are reported", scores.size(), 2);
        assertTrue("Player 0 has a score", scores.containsKey(0));
        assertTrue("Player 1 has a score", scores.containsKey(1));
        return new int[] { scores.get(0), scores.get(1) };
    }
}
