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

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.MessageQueue;
import eu.jonahbauer.wizard.core.states.GameData;
import eu.jonahbauer.wizard.core.states.game.Finished;
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 java.util.concurrent.ExecutionException;

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 RoundTest {
    private final UUID[] players = new UUID[] {
            new UUID(0, 0),
            new UUID(0, 1),
            new UUID(0, 2),
            new UUID(0, 3)
    };

    @SneakyThrows
    private Game performTest(long seed, Configuration configuration, int round, MessageQueue queue) {
        Game game = spy(new Game(seed, configuration, 10 * 60 * 1000, 10 * 60 * 1000, queue));
        doCallRealMethod().doAnswer(finish()).when(game).transition(any(StartingRound.class));
        queue.setGame(game);

        var playerList = List.of(players);

        GameData data = GameData.EMPTY.with(
                entry(PLAYERS, playerList),
                entry(ROUND, round),
                entry(SCORE, Map.of())
        );

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

        verify(game).transition(any(Finished.class));
        return game;
    }

    @Test
    public void run_Simple() throws ExecutionException, InterruptedException {
        MessageQueue queue = new MessageQueue()
                .sync(players) // starting_round
                .sync(players) // dealing
                .sync(players) // determining_trump
                .addPrediction(players[3], 3)
                .addPrediction(players[0], 0)
                .addPrediction(players[1], 3)
                .addPrediction(players[2], 1)
                // trick 0
                .sync(players)
                .addCard(players[3], Card.RED_7)
                .addCard(players[0], Card.GREEN_WIZARD)
                .addCard(players[1], Card.GREEN_10)
                .addCard(players[2], Card.RED_1)
                // trick 1
                .sync(players)
                .addCard(players[0], Card.RED_9)
                .addCard(players[1], Card.YELLOW_WIZARD)
                .addCard(players[2], Card.RED_3)
                .addCard(players[3], Card.RED_10)
                // trick 2
                .sync(players)
                .addCard(players[1], Card.YELLOW_JESTER)
                .addCard(players[2], Card.GREEN_12)
                .addCard(players[3], Card.GREEN_5)
                .addCard(players[0], Card.BLUE_7)
                // trick 3
                .sync(players)
                .addCard(players[2], Card.GREEN_1)
                .addCard(players[3], Card.YELLOW_10)
                .addCard(players[0], Card.BLUE_5)
                .addCard(players[1], Card.GREEN_2)
                // trick 4
                .sync(players)
                .addCard(players[1], Card.GREEN_7)
                .addCard(players[2], Card.GREEN_3)
                .addCard(players[3], Card.YELLOW_5)
                .addCard(players[0], Card.RED_13)
                // trick 5
                .sync(players)
                .addCard(players[1], Card.YELLOW_8)
                .addCard(players[2], Card.BLUE_9)
                .addCard(players[3], Card.YELLOW_9)
                .addCard(players[0], Card.GREEN_JESTER)
                // trick 5
                .sync(players)
                .addCard(players[3], Card.YELLOW_2)
                .addCard(players[0], Card.BLUE_2)
                .addCard(players[1], Card.BLUE_10)
                .addCard(players[2], Card.BLUE_6);

        int round = 6;
        Game game = performTest(0L, Configuration.DEFAULT, round, queue);
        game.await();

        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting_round
        order.verify(game).notify(any(StateMessage.class)); // dealing
        order.verify(game, atLeast(4)).notify(any(), any(HandMessage.class)); // hands
        order.verify(game).notify(any(StateMessage.class)); // determining_trump
        order.verify(game).notify(any(TrumpMessage.class)); // trump
        for (int i = 0; i < players.length; i++) {
            order.verify(game).notify(any(StateMessage.class)); // predicting
            order.verify(game).notify(any(UserInputMessage.class)); // user input
            order.verify(game).notify(any(PredictionMessage.class)); // user input
        }

        for (int i = 0; i < round + 1; i++) {
            order.verify(game).notify(any(StateMessage.class)); // starting trick
            for (int j = 0; j < players.length; j++) {
                order.verify(game).notify(any(StateMessage.class)); // playing_card
                order.verify(game).notify(any(UserInputMessage.class)); // user input
                order.verify(game).notify(any(CardMessage.class)); // user input
            }
            order.verify(game).notify(any(StateMessage.class)); // finishing_trick
            order.verify(game).notify(any(TrickMessage.class)); // trick
        }
        order.verify(game).notify(any(StateMessage.class)); // finishing_round
        order.verify(game).notify(argThat(message ->
                message instanceof ScoreMessage score
                        && score.getPoints().get(players[0]) == -10
                        && score.getPoints().get(players[1]) == 50
                        && score.getPoints().get(players[2]) == 30
                        && score.getPoints().get(players[3]) == -10
        )); // score
        order.verify(game).transition(any(StartingRound.class)); // next round
        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_Anniversary() throws ExecutionException, InterruptedException {
        MessageQueue queue = new MessageQueue()
                .sync(players) // starting_round
                .sync(players) // dealing
                .sync(players) // determining_trump
                .addPickTrump(players[2], Card.Suit.YELLOW)
                .sync(players) // determining_trump
                .addPrediction(players[3], 2)
                .addPrediction(players[0], 2)
                .addPrediction(players[1], 2)
                .begin()
                    .addPrediction(players[2], 1).assertThrows(IllegalArgumentException.class)
                    .addPrediction(players[2], 3)
                .end()
                // trick 0
                .sync(players)
                .begin()
                    .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class)
                    .addCard(players[3], Card.BLUE_2)
                .begin()
                    .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
                    .addCard(players[0], Card.YELLOW_8)
                .end()
                .addCard(players[1], Card.BLUE_9)
                .addCard(players[2], Card.GREEN_WIZARD)
                // trick 1
                .sync(players)
                .begin()
                    .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
                    .addCard(players[2], Card.YELLOW_4)
                .end()
                .addCard(players[3], Card.YELLOW_3)
                .addCard(players[0], Card.YELLOW_WIZARD)
                .addCard(players[1], Card.BOMB)
                // trick 2
                .sync(players)
                .addCard(players[0], Card.RED_3)
                .begin()
                    .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
                    .addCard(players[1], Card.RED_12)
                .end()
                .addCard(players[2], Card.RED_2)
                .addCard(players[3], Card.DRAGON)
                // trick 3
                .sync(players)
                .addCard(players[3], Card.BLUE_13)
                .begin()
                    .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class)
                    .addCard(players[0], Card.CLOUD_YELLOW)
                .begin()
                    .addCard(players[3], Card.RED_11).assertThrows(IllegalStateException.class)
                    .addCard(players[1], Card.YELLOW_13)
                .end()
                .addCard(players[2], Card.BLUE_1)
                // trick 4
                .sync(players)
                .addCard(players[1], Card.RED_7)
                .addCard(players[2], Card.YELLOW_11)
                .begin()
                    .addCard(players[3], Card.RED_11).assertThrows(IllegalArgumentException.class)
                    .addCard(players[3], Card.RED_5)
                .begin()
                    .addCard(players[0], Card.CLOUD).assertThrows(IllegalArgumentException.class)
                    .addCard(players[0], Card.CHANGELING_WIZARD)
                .end()
                // trick 5
                .sync(players)
                .addCard(players[0], Card.GREEN_7)
                .addCard(players[1], Card.FAIRY)
                .addCard(players[2], Card.YELLOW_7)
                .addCard(players[3], Card.BLUE_6)
                // trick 6
                .sync(players)
                .addCard(players[2], Card.BLUE_4)
                .addCard(players[3], Card.BLUE_11)
                .addCard(players[0], Card.GREEN_1)
                .addCard(players[1], Card.GREEN_11)
                // cloud
                .begin()
                    .addChangePrediction(players[1], 0).assertThrows(IllegalArgumentException.class)
                    .addChangePrediction(players[1], 2).assertThrows(IllegalArgumentException.class)
                    .addChangePrediction(players[1], 4).assertThrows(IllegalArgumentException.class)
                    .addChangePrediction(players[1], 1)
                .end();

        int round = 6;
        Game game = performTest(227L, Configuration.ANNIVERSARY_2021_PM1, round, queue);
        game.await();

        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // starting round
        order.verify(game).notify(any(StateMessage.class)); // dealing
        order.verify(game, times(4)).notify(any(), any(HandMessage.class)); // hands
        order.verify(game).notify(any(StateMessage.class)); // determining trump
        order.verify(game, times(2)).notify(any(TrumpMessage.class)); // werewolf
        order.verify(game).notify(any(UserInputMessage.class)); // user input
        order.verify(game).notify(any(TrumpMessage.class)); // trump
        order.verify(game).notify(any(), any(HandMessage.class)); // update hand
        for (int i = 0; i < players.length; i++) {
            order.verify(game).notify(any(StateMessage.class));
            order.verify(game).notify(any(UserInputMessage.class));
            order.verify(game).notify(any(PredictionMessage.class));
        }
        for (int i = 0; i < round + 1; i++) {
            order.verify(game).notify(any(StateMessage.class)); // starting trick
            for (int j = 0; j < players.length; j++) {
                order.verify(game).notify(any(StateMessage.class)); // playing_card
                order.verify(game).notify(any(UserInputMessage.class)); // user input
                order.verify(game).notify(any(CardMessage.class)); // user input
            }
            order.verify(game).notify(any(StateMessage.class)); // finishing_trick
            order.verify(game).notify(any(TrickMessage.class)); // trick
        }
        order.verify(game).notify(any(StateMessage.class)); // change prediction
        order.verify(game).notify(any(UserInputMessage.class));
        order.verify(game).notify(any(PredictionMessage.class));
        order.verify(game).notify(any(StateMessage.class)); // finishing_round
        order.verify(game).notify(argThat(message ->
                message instanceof ScoreMessage score
                        && score.getPoints().get(players[0]) == -10
                        && score.getPoints().get(players[1]) == 30
                        && score.getPoints().get(players[2]) == -10
                        && score.getPoints().get(players[3]) == 40
        )); // score
        order.verify(game).transition(any(StartingRound.class)); // next round
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }
}
