package org.mage.test.serverside.base.impl;

import mage.constants.PhaseStep;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.constants.Zone;
import mage.filter.Filter;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.players.Player;
import org.junit.Assert;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestAPI;
import org.mage.test.serverside.base.MageTestBase;

import java.util.List;
import java.util.UUID;

/**
 * API for test initialization and asserting the test results.
 *
 * @author ayratn
 */
public abstract class CardTestAPIImpl extends MageTestBase implements CardTestAPI {

    /**
     * Default game initialization params for red player (that plays with Mountains)
     */
    @Override
    public void useRedDefault() {
        // *** ComputerA ***
        // battlefield:ComputerA:Mountain:5
        addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
        // hand:ComputerA:Mountain:4
        addCard(Zone.HAND, playerA, "Mountain", 5);
        // library:ComputerA:clear:0
        removeAllCardsFromLibrary(playerA);
        // library:ComputerA:Mountain:10
        addCard(Zone.LIBRARY, playerA, "Mountain", 10);

        // *** ComputerB ***
        // battlefield:ComputerB:Plains:2
        addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
        // hand:ComputerB:Plains:2
        addCard(Zone.HAND, playerB, "Plains", 2);
        // library:ComputerB:clear:0
        removeAllCardsFromLibrary(playerB);
        // library:ComputerB:Plains:10
        addCard(Zone.LIBRARY, playerB, "Plains", 10);
    }

    /**
     * Default game initialization params for white player (that plays with Plains)
     */
    public void useWhiteDefault() {
        // *** ComputerA ***
        addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
        addCard(Zone.HAND, playerA, "Plains", 5);
        removeAllCardsFromLibrary(playerA);
        addCard(Zone.LIBRARY, playerA, "Plains", 10);

        // *** ComputerB ***
        addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
        addCard(Zone.HAND, playerB, "Plains", 2);
        removeAllCardsFromLibrary(playerB);
        addCard(Zone.LIBRARY, playerB, "Plains", 10);
    }

    /**
     * Removes all cards from player's library from the game.
     * Usually this should be used once before initialization to form the library in certain order.
     *
     * @param player {@link Player} to remove all library cards from.
     */
    public void removeAllCardsFromLibrary(Player player) {
        if (player.equals(playerA)) {
            commandsA.put(Zone.LIBRARY, "clear");
        } else if (player.equals(playerB)) {
            commandsB.put(Zone.LIBRARY, "clear");
        }
    }

    /**
     * Add a card to specified zone of specified player.
     *
     * @param gameZone {@link mage.constants.Zone} to add cards to.
     * @param player   {@link Player} to add cards for. Use either playerA or playerB.
     * @param cardName Card name in string format.
     */
    @Override
    public void addCard(Zone gameZone, TestPlayer player, String cardName) {
        addCard(gameZone, player, cardName, 1, false);
    }

    /**
     * Add any amount of cards to specified zone of specified player.
     *
     * @param gameZone {@link mage.constants.Zone} to add cards to.
     * @param player   {@link Player} to add cards for. Use either playerA or playerB.
     * @param cardName Card name in string format.
     * @param count    Amount of cards to be added.
     */
    @Override
    public void addCard(Zone gameZone, TestPlayer player, String cardName, int count) {
        addCard(gameZone, player, cardName, count, false);
    }

    /**
     * Add any amount of cards to specified zone of specified player.
     *
     * @param gameZone {@link mage.constants.Zone} to add cards to.
     * @param player   {@link Player} to add cards for. Use either playerA or playerB.
     * @param cardName Card name in string format.
     * @param count    Amount of cards to be added.
     * @param tapped   In case gameZone is Battlefield, determines whether permanent should be tapped.
     *                 In case gameZone is other than Battlefield, {@link IllegalArgumentException} is thrown
     */
    @Override
    public void addCard(Zone gameZone, TestPlayer player, String cardName, int count, boolean tapped) {


        if (gameZone == Zone.BATTLEFIELD) {
            for (int i = 0; i < count; i++) {
                CardInfo cardInfo = CardRepository.instance.findCard(cardName);
                Card card = cardInfo != null ? cardInfo.getCard() : null;
                if (card == null) {
                    throw new IllegalArgumentException("[TEST] Couldn't find a card: " + cardName);
                }
                PermanentCard p = new PermanentCard(card, null, currentGame);
                p.setTapped(tapped);
                if (player.equals(playerA)) {
                    battlefieldCardsA.add(p);
                } else if (player.equals(playerB)) {
                    battlefieldCardsB.add(p);
                }
            }
        } else {
            if (tapped) {
                throw new IllegalArgumentException("Parameter tapped=true can be used only for Zone.BATTLEFIELD.");
            }
            List<Card> cards = getCardList(gameZone, player);
            for (int i = 0; i < count; i++) {
                CardInfo cardInfo = CardRepository.instance.findCard(cardName);
                Card card = cardInfo != null ? cardInfo.getCard() : null;
                cards.add(card);
            }
        }
    }

    /**
     * Returns card list containter for specified game zone and player.
     *
     * @param gameZone
     * @param player
     * @return
     */
    private List<Card> getCardList(Zone gameZone, Player player) {
        if (player.equals(playerA)) {
            if (gameZone == Zone.HAND) {
                return handCardsA;
            } else if (gameZone == Zone.GRAVEYARD) {
                return graveyardCardsA;
            } else if (gameZone == Zone.LIBRARY) {
                return libraryCardsA;
            }
        } else if (player.equals(playerB)) {
            if (gameZone == Zone.HAND) {
                return handCardsB;
            } else if (gameZone == Zone.GRAVEYARD) {
                return graveyardCardsB;
            } else if (gameZone == Zone.LIBRARY) {
                return libraryCardsB;
            }
        }
        return null;
    }

    /**
     * Set player's initial life count.
     *
     * @param player {@link Player} to set life count for.
     * @param life   Life count to set.
     */
    @Override
    public void setLife(TestPlayer player, int life) {
        if (player.equals(playerA)) {
            commandsA.put(Zone.OUTSIDE, "life:" + String.valueOf(life));
        } else if (player.equals(playerB)) {
            commandsB.put(Zone.OUTSIDE, "life:" + String.valueOf(life));
        }
    }

    /**
     * Define turn number to stop the game on.
     */
    @Override
    public void setStopOnTurn(int turn) {
        stopOnTurn = turn == -1 ? null : turn;
        stopAtStep = PhaseStep.UNTAP;
    }

    /**
     * Define turn number and step to stop the game on.
     */
    @Override
    public void setStopAt(int turn, PhaseStep step) {
        stopOnTurn = turn == -1 ? null : turn;
        stopAtStep = step;
    }

    /**
     * Assert turn number after test execution.
     *
     * @param turn Expected turn number to compare with. 1-based.
     */
    @Override
    public void assertTurn(int turn) throws AssertionError {
        Assert.assertEquals("Turn numbers are not equal", turn, currentGame.getTurnNum());
    }

    /**
     * Assert game result after test execution.
     *
     * @param result Expected {@link GameResult} to compare with.
     */
    @Override
    public void assertResult(Player player, GameResult result) throws AssertionError {
        if (player.equals(playerA)) {
            GameResult actual = CardTestAPI.GameResult.DRAW;
            switch (currentGame.getWinner()) {
                case "Player PlayerA is the winner":
                    actual = CardTestAPI.GameResult.WON;
                    break;
                case "Player PlayerB is the winner":
                    actual = CardTestAPI.GameResult.LOST;
                    break;
            }
            Assert.assertEquals("Game results are not equal", result, actual);
        } else if (player.equals(playerB)) {
            GameResult actual = CardTestAPI.GameResult.DRAW;
            switch (currentGame.getWinner()) {
                case "Player PlayerB is the winner":
                    actual = CardTestAPI.GameResult.WON;
                    break;
                case "Player PlayerA is the winner":
                    actual = CardTestAPI.GameResult.LOST;
                    break;
            }
            Assert.assertEquals("Game results are not equal", result, actual);
        }
    }

    /**
     * Assert player's life count after test execution.
     *
     * @param player {@link Player} to get life for comparison.
     * @param life   Expected player's life to compare with.
     */
    @Override
    public void assertLife(Player player, int life) throws AssertionError {
        int actual = currentGame.getPlayer(player.getId()).getLife();
        Assert.assertEquals("Life amounts are not equal", life, actual);
    }

    /**
     * Assert creature's power and toughness by card name.
     * <p/>
     * Throws {@link AssertionError} in the following cases:
     * 1. no such player
     * 2. no such creature under player's control
     * 3. depending on comparison scope:
     * 3a. any: no creature under player's control with the specified p\t params
     * 3b. all: there is at least one creature with the cardName with the different p\t params
     *
     * @param player    {@link Player} to get creatures for comparison.
     * @param cardName  Card name to compare with.
     * @param power     Expected power to compare with.
     * @param toughness Expected toughness to compare with.
     * @param scope     {@link mage.filter.Filter.ComparisonScope} Use ANY, if you want "at least one creature with given name should have specified p\t"
     *                  Use ALL, if you want "all creature with gived name should have specified p\t"
     */
    @Override
    public void assertPowerToughness(Player player, String cardName, int power, int toughness, Filter.ComparisonScope scope)
            throws AssertionError {
        int count = 0;
        int fit = 0;
        for (Permanent permanent : currentGame.getBattlefield().getAllActivePermanents(player.getId())) {
            if (permanent.getName().equals(cardName)) {
                count++;
                if (scope == Filter.ComparisonScope.All) {
                    Assert.assertEquals("Power is not the same (" + power + " vs. " + permanent.getPower().getValue() + ')',
                            power, permanent.getPower().getValue());
                    Assert.assertEquals("Toughness is not the same (" + toughness + " vs. " + permanent.getToughness().getValue() + ')',
                            toughness, permanent.getToughness().getValue());
                } else if (scope == Filter.ComparisonScope.Any) {
                    if (power == permanent.getPower().getValue() && toughness == permanent.getToughness().getValue()) {
                        fit++;
                        break;
                    }
                }
            }
        }

        Assert.assertTrue("There is no such permanent under player's control, player=" + player.getName() +
                ", cardName=" + cardName, count > 0);

        if (scope == Filter.ComparisonScope.Any) {
            Assert.assertTrue("There is no such creature under player's control with specified power&toughness, player=" + player.getName() +
                    ", cardName=" + cardName, fit > 0);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void assertAbilities(Player player, String cardName, List<Ability> abilities)
            throws AssertionError {
        int count = 0;
        Permanent found = null;
        for (Permanent permanent : currentGame.getBattlefield().getAllActivePermanents(player.getId())) {
            if (permanent.getName().equals(cardName)) {
                found = permanent;
            }
        }

        Assert.assertNotNull("There is no such permanent under player's control, player=" + player.getName() +
                ", cardName=" + cardName, found);

        Assert.assertTrue("There is more than one such permanent under player's control, player=" + player.getName() +
                ", cardName=" + cardName, count == 1);

        for (Ability ability : abilities) {
            Assert.assertTrue("No such ability=" + ability.toString() + ", player=" + player.getName() +
                    ", cardName" + cardName, found.getAbilities().contains(ability));
        }
    }

    /**
     * Assert permanent count under player's control.
     *
     * @param player {@link Player} which permanents should be counted.
     * @param count  Expected count.
     */
    @Override
    public void assertPermanentCount(Player player, int count) throws AssertionError {
        int actualCount = 0;
        for (Permanent permanent : currentGame.getBattlefield().getAllPermanents()) {
            if (permanent.getControllerId().equals(player.getId())) {
                actualCount++;
            }
        }
        Assert.assertEquals("(Battlefield) Card counts are not equal ", count, actualCount);
    }

    /**
     * Assert permanent count under player's control.
     *
     * @param player   {@link Player} which permanents should be counted.
     * @param cardName Name of the cards that should be counted.
     * @param count    Expected count.
     */
    @Override
    public void assertPermanentCount(Player player, String cardName, int count) throws AssertionError {
        int actualCount = 0;
        for (Permanent permanent : currentGame.getBattlefield().getAllPermanents()) {
            if (permanent.getControllerId().equals(player.getId())) {
                if (permanent.getName().equals(cardName)) {
                    actualCount++;
                }
            }
        }
        Assert.assertEquals("(Battlefield) Card counts are not equal (" + cardName + ')', count, actualCount);
    }

    public Permanent getPermanent(String cardName, UUID controller) {
        Permanent permanent0 = null;
        int count = 0;
        for (Permanent permanent : currentGame.getBattlefield().getAllPermanents()) {
            if (permanent.getControllerId().equals(controller)) {
                if (permanent.getName().equals(cardName)) {
                    permanent0 = permanent;
                    count++;
                }
            }
        }
        Assert.assertNotNull("Couldn't find a card with specified name: " + cardName, permanent0);
        Assert.assertEquals("More than one permanent was found: " + cardName + '(' + count + ')', 1, count);
        return permanent0;
    }

    public void playLand(Player player, String cardName) {
        player.addAction("play:"+cardName);
    }

    public void castSpell(Player player, String cardName) {
        player.addAction("cast:"+cardName);
    }

    public void addFixedTarget(Player player, String cardName, Player target) {
        player.addAction("cast:"+cardName + ";name=" + target.getName());
    }

    public void addFixedTarget(Player player, String cardName, String targetName) {
        player.addAction("cast:"+cardName + ";name=" + targetName);
    }

    public void useAbility(Player player, String cardName) {
    }

    public void attack(Player player, String cardName) {
        player.addAction("attack:"+cardName);
    }
}
