package org.ggp.base.util.gdl.scrambler;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;

import org.ggp.base.util.game.Game;
import org.ggp.base.util.game.GameRepository;
import org.ggp.base.util.gdl.GdlVisitor;
import org.ggp.base.util.gdl.GdlVisitors;
import org.ggp.base.util.gdl.factory.GdlFactory;
import org.ggp.base.util.gdl.factory.exceptions.GdlFormatException;
import org.ggp.base.util.gdl.grammar.Gdl;
import org.ggp.base.util.gdl.grammar.GdlConstant;
import org.ggp.base.util.gdl.grammar.GdlVariable;
import org.ggp.base.util.statemachine.implementation.prover.ProverStateMachine;
import org.ggp.base.util.symbol.factory.exceptions.SymbolFormatException;
import org.junit.Assert;
import org.junit.Test;

/**
 * Unit tests for the GdlScrambler class, which provides a way
 * to scramble and unscramble Gdl objects without changing the
 * underlying physics of the games they represent.
 *
 * @author Sam
 */
public class GdlScramblerTest extends Assert {
    /**
     * When scrambling is disabled, the "NoOpGdlScrambler" is used. This class
     * simply renders the Gdl and parses it in the naive way, without doing any
     * special modification. This is the trivial case of "scrambling".
     */
    @Test
    public void testNoOpScrambler() throws GdlFormatException, SymbolFormatException {
        runScramblerTest(new NoOpGdlScrambler());
    }

    /**
     * When scrambling is enabled, the "MappingGdlScrambler" is used. This class
     * systematically replaces all of the constant and variable names in the Gdl
     * with scrambled versions, drawing new random tokens first from a list of
     * English words, and appending suffixes when the original list is exhausted.
     */
    @Test
    public void testMappingScrambler() throws GdlFormatException, SymbolFormatException {
        runScramblerTest(new MappingGdlScrambler(new Random()));
    }

    /**
     * Furthermore, the mapping scrambler can be initialized with a Random object,
     * which can be used to ensure deterministic, reproducible scrambling. This can
     * be used to scramble a specific match in the same way, even if it stored and
     * reloaded in the meantime.
     */
    @Test
    public void testMappingScramblerConsistency() {
        GdlScrambler aScrambler = new MappingGdlScrambler(new Random(123));
        GdlScrambler bScrambler = new MappingGdlScrambler(new Random(123));
        GdlScrambler cScrambler = new MappingGdlScrambler(new Random(234));
        GameRepository repo = GameRepository.getDefaultRepository();
        for (String gameKey : repo.getGameKeys()) {
            Game game = repo.getGame(gameKey);
            StringBuilder aScrambledRules = new StringBuilder();
            StringBuilder bScrambledRules = new StringBuilder();
            StringBuilder cScrambledRules = new StringBuilder();
            StringBuilder dScrambledRules = new StringBuilder();
            StringBuilder eScrambledRules = new StringBuilder();
            StringBuilder fScrambledRules = new StringBuilder();
            for(Gdl rule : game.getRules()) {
                aScrambledRules.append(aScrambler.scramble(rule)+"\n");
                bScrambledRules.append(bScrambler.scramble(rule)+"\n");
                cScrambledRules.append(cScrambler.scramble(rule)+"\n");
            }
            for(Gdl rule : game.getRules()) {
                dScrambledRules.append(aScrambler.scramble(rule)+"\n");
                eScrambledRules.append(bScrambler.scramble(rule)+"\n");
                fScrambledRules.append(cScrambler.scramble(rule)+"\n");
            }
            assertEquals(aScrambledRules.toString(), bScrambledRules.toString());
            assertEquals(aScrambledRules.toString(), dScrambledRules.toString());
            assertEquals(aScrambledRules.toString(), eScrambledRules.toString());
            assertFalse(aScrambledRules.toString().equals(cScrambledRules.toString()));
            assertEquals(cScrambledRules.toString(), fScrambledRules.toString());
        }
    }

    private void runScramblerTest(GdlScrambler scrambler) throws SymbolFormatException, GdlFormatException {
        GameRepository repo = GameRepository.getDefaultRepository();
        for (String gameKey : repo.getGameKeys()) {
            Game game = repo.getGame(gameKey);
            List<Gdl> theScrambledRules = new ArrayList<Gdl>();
            for(Gdl rule : game.getRules()) {
                String renderedRule = rule.toString();
                String renderedScrambledRule = scrambler.scramble(rule).toString();
                String renderedUnscrambledRule = scrambler.unscramble(renderedScrambledRule).toString();
                theScrambledRules.add(GdlFactory.create(renderedScrambledRule));
                // If the scrambler claims that it scrambles the game, then the
                // scrambled rules should be different than the original rules.
                // Otherwise they should be identical.
                if (scrambler.scrambles() && !isNeverScrambled(rule)) {
                    assertNotEquals(gameKey, renderedRule, renderedScrambledRule);
                } else {
                    assertEquals(gameKey, renderedRule, renderedScrambledRule);
                }
                // One important property for any scrambler is that the original
                // and the unscrambled Gdl must be the same. This guarantees that
                // the server can correctly unscramble responses from the players.
                assertEquals(gameKey, renderedRule, renderedUnscrambledRule);
            }

            // An important property for any scrambler is that the scrambled rules
            // have the same physics as the regular rules. For example, the number
            // of roles in each game should be the same, and the number of facts
            // that are true in the initial state should be the same. There could
            // be more thorough verification here, like looking at the number of
            // legal joint moves in the first state, or simulating entire matches,
            // but that would be expensive.
            ProverStateMachine pNormal = new ProverStateMachine();
            ProverStateMachine pScrambled = new ProverStateMachine();
            pNormal.initialize(game.getRules());
            pScrambled.initialize(theScrambledRules);
            assertEquals(gameKey, pNormal.getRoles().size(), pScrambled.getRoles().size());
            assertEquals(gameKey, pNormal.getInitialState().getContents().size(), pScrambled.getInitialState().getContents().size());
        }
    }

    private boolean isNeverScrambled(Gdl gdl) {
        final AtomicBoolean containsMappedConstant = new AtomicBoolean(false);
        GdlVisitors.visitAll(gdl, new GdlVisitor() {
            @Override
            public void visitConstant(GdlConstant constant) {
                if (MappingGdlScrambler.shouldMap(constant.getValue())) {
                    containsMappedConstant.set(true);
                }
            }
            @Override
            public void visitVariable(GdlVariable variable) {
                //Right now, these are (effectively) always mapped
                containsMappedConstant.set(true);
            }
        });
        return !containsMappedConstant.get();
    }
}