package eu.jonahbauer.wizard.core.states.trick;

import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.common.model.Configuration;
import eu.jonahbauer.wizard.core.Game;
import eu.jonahbauer.wizard.core.GameTestUtils;
import eu.jonahbauer.wizard.core.MessageQueue;
import eu.jonahbauer.wizard.core.states.GameData;
import eu.jonahbauer.wizard.core.states.round.FinishingRound;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;

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

import static eu.jonahbauer.wizard.core.GameTestUtils.doFinish;
import static eu.jonahbauer.wizard.core.GameTestUtils.finish;
import static eu.jonahbauer.wizard.core.states.GameData.*;
import static eu.jonahbauer.wizard.core.states.GameData.TypedValue.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class TrickTest {

    private final UUID[] players = new UUID[] {
            new UUID(0, 0),
            new UUID(0, 1),
            new UUID(0, 2),
            new UUID(0, 3)
    };

    @SneakyThrows
    @SuppressWarnings("SameParameterValue")
    private Game performTest(Configuration configuration, int round, int trick, Map<UUID, List<Card>> hands, Card.Suit trump, MessageQueue queue) {
        Game game = spy(new Game(configuration, 10 * 60 * 1000, 10 * 60 * 1000, queue));
        doCallRealMethod().doAnswer(finish()).when(game).transition(any(StartingTrick.class));
        doFinish().when(game).transition(any(FinishingRound.class));
        queue.setGame(game);

        var playerList = List.of(players);

        GameData data = GameData.EMPTY.with(
                entry(PLAYERS, playerList),
                entry(ROUND, round),
                entry(SCORE, Map.of()),
                entry(HANDS, hands),
                entry(PREDICTIONS, GameTestUtils.emptyToIntMap(playerList)),
                entry(TRICKS, GameTestUtils.emptyToIntMap(playerList)),
                entry(TRICK, trick),
                entry(TRUMP_SUIT, trump),
                entry(CURRENT_PLAYER, players[0])
        );

        game.resume(new StartingTrick(data));
        game.await();

        return game;
    }

    @Test
    public void run_Simple() {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], List.of(Card.RED_1),
                players[1], List.of(Card.YELLOW_1),
                players[2], List.of(Card.GREEN_1),
                players[3], List.of(Card.BLUE_1)
        );

        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .sync(players)
                .addCards(List.of(players), hands, 0);

        Game game = performTest(Configuration.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting trick
        for (int i = 0; i < players.length; i++) {
            order.verify(game).notify(any(StateMessage.class)); // playing card
            order.verify(game).notify(any(UserInputMessage.class)); // user input request
            order.verify(game).notify(any(CardMessage.class)); // user response
        }
        order.verify(game).notify(any(StateMessage.class)); // finishing trick
        order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner
        order.verify(game).transition(any(FinishingRound.class)); // round is finished
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }
    @Test
    public void run_WithWrongInput() {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], List.of(Card.RED_1),
                players[1], List.of(Card.YELLOW_1),
                players[2], List.of(Card.GREEN_1),
                players[3], List.of(Card.BLUE_1)
        );

        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .sync(players)
                .addCard(players[0], Card.RED_1)
                .begin()
                    .addCard(players[2], Card.GREEN_1).assertThrows(IllegalStateException.class)
                    .addCard(players[1], Card.GREEN_1).assertThrows(IllegalArgumentException.class)
                    .addCard(players[1], Card.YELLOW_1)
                .end()
                .addCard(players[2], Card.GREEN_1)
                .begin()
                    .addPrediction(players[3], 1).assertThrows(IllegalStateException.class)
                    .addCard(players[3], Card.BLUE_1)
                .end();

        Game game = performTest(Configuration.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting trick
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // finishing trick
        order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner
        order.verify(game).transition(any(FinishingRound.class)); // round is finished
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }

    @Test
    public void run_WithCloud() {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], List.of(Card.RED_1),
                players[1], List.of(Card.CLOUD),
                players[2], List.of(Card.GREEN_1),
                players[3], List.of(Card.BLUE_1)
        );

        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .sync(players)
                .addCard(players[0], Card.RED_1)
                .addCard(players[1], Card.CLOUD_BLUE)
                .addCard(players[2], Card.GREEN_1)
                .addCard(players[3], Card.BLUE_1)
                .addChangePrediction(players[2], 1);

        Game game = performTest(Configuration.ANNIVERSARY_2021, 0, 0, hands, Card.Suit.GREEN, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting trick
        for (int i = 0; i < players.length; i++) {
            order.verify(game).notify(any(StateMessage.class)); // playing card
            order.verify(game).notify(any(UserInputMessage.class)); // user input request
            order.verify(game).notify(any(CardMessage.class)); // user response
        }
        order.verify(game).notify(any(StateMessage.class)); // finishing trick
        order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[2])); // trick with correct winner
        order.verify(game).notify(any(StateMessage.class)); // changing prediction
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(FinishingRound.class)); // round is finished
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }

    @Test
    public void run_WithJuggler() {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], List.of(Card.RED_1, Card.GREEN_12),
                players[1], List.of(Card.JUGGLER, Card.YELLOW_3),
                players[2], List.of(Card.GREEN_1, Card.BLUE_4),
                players[3], List.of(Card.RED_5, Card.BLUE_1)
        );

        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .sync(players)
                .addCard(players[0], Card.RED_1)
                .addCard(players[1], Card.JUGGLER_RED)
                .addCard(players[2], Card.GREEN_1)
                .addCard(players[3], Card.RED_5)
                .begin()
                    .addJuggle(players[0], Card.GREEN_12)
                    .addJuggle(players[1], Card.YELLOW_3)
                    .addJuggle(players[2], Card.BLUE_4)
                    .addJuggle(players[3], Card.BLUE_1)
                .end();

        Game game = performTest(Configuration.ANNIVERSARY_2021, 1, 0, hands, Card.Suit.YELLOW, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting trick
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // finishing trick
        order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[1])); // trick with correct winner
        order.verify(game).notify(any(StateMessage.class)); // juggling
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game, times(4)).notify(any(), any(HandMessage.class));
        order.verify(game).transition(any(StartingTrick.class)); // there is another trick
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }

    @Test
    public void run_WithChangeling() {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], List.of(Card.CHANGELING),
                players[1], List.of(Card.RED_1),
                players[2], List.of(Card.GREEN_1),
                players[3], List.of(Card.BLUE_1)
        );

        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .sync(players)
                .addCard(players[0], Card.CHANGELING_JESTER)
                .addCard(players[1], Card.RED_1)
                .addCard(players[2], Card.GREEN_1)
                .addCard(players[3], Card.BLUE_1);

        Game game = performTest(Configuration.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting trick
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // finishing trick
        order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[3])); // trick with correct winner
        order.verify(game).transition(any(FinishingRound.class)); // round is finished
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }

    @Test
    public void run_WithChangeling2() {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], List.of(Card.CHANGELING),
                players[1], List.of(Card.RED_1),
                players[2], List.of(Card.GREEN_WIZARD),
                players[3], List.of(Card.BLUE_1)
        );

        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .sync(players)
                .addCard(players[0], Card.CHANGELING_WIZARD)
                .addCard(players[1], Card.RED_1)
                .addCard(players[2], Card.GREEN_WIZARD)
                .addCard(players[3], Card.BLUE_1);

        Game game = performTest(Configuration.DEFAULT, 0, 0, hands, Card.Suit.BLUE, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting trick
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // playing card
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(CardMessage.class)); // user response
        order.verify(game).notify(any(StateMessage.class)); // finishing trick
        order.verify(game).notify(argThat(msg -> msg instanceof TrickMessage trick && trick.getPlayer() == players[0])); // trick with correct winner
        order.verify(game).transition(any(FinishingRound.class)); // round is finished
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }
}
