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

import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.common.messages.observer.StateMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameTestUtils;
import eu.jonahbauer.wizard.core.machine.MessageQueue;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
import eu.jonahbauer.wizard.core.model.GameConfiguration;
import eu.jonahbauer.wizard.core.model.Configurations;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;

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

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

public class PredictingTest {
    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(GameConfiguration configuration, int round, MessageQueue queue) {
        Map<UUID, List<Card>> hands = Map.of(
                players[0], Collections.nCopies(round + 1, Card.HIDDEN),
                players[1], Collections.nCopies(round + 1, Card.HIDDEN),
                players[2], Collections.nCopies(round + 1, Card.HIDDEN),
                players[3], Collections.nCopies(round + 1, Card.HIDDEN)
        );

        Game game = spy(new Game(configuration, queue));
        doFinish().when(game).transition(any(), any(StartingTrick.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(TRUMP_SUIT, Card.Suit.NONE),
                entry(PREDICTIONS, GameTestUtils.emptyToIntMap(playerList)),
                entry(HANDS, hands),
                entry(CURRENT_PLAYER, players[(round + 1) % players.length])
        );

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

        verify(game, never()).transition(any(), isNull());
        return game;
    }

    @Test
    public void predicting_Simple() {
        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .addPrediction(players[0], 4)
                .addPrediction(players[1], 3)
                .addPrediction(players[2], 3)
                .addPrediction(players[3], 0);

        Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // predicting
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(StartingTrick.class)); // starting 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 predicting_WithWrongInput() {
        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .begin()
                    .addPrediction(players[0], -1).assertThrows(IllegalArgumentException.class)
                    .addPrediction(players[0], 6).assertThrows(IllegalArgumentException.class)
                    .addPrediction(players[0], 4)
                .begin()
                    .addPrediction(players[2], 3).assertThrows(IllegalStateException.class)
                    .addPrediction(players[1], 3)
                .begin()
                    .addPrediction(players[2], 5).assertThrows(IllegalArgumentException.class)
                    .addPrediction(players[2], 3)
                .begin()
                    .addCard(players[3], Card.GREEN_WIZARD).assertThrows(IllegalStateException.class)
                    .addPickTrump(players[3], Card.Suit.GREEN).assertThrows(IllegalStateException.class)
                    .addPrediction(players[3], 0)
                .end();

        Game game = performTest(Configurations.ANNIVERSARY_2021, 3, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // predicting
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(StartingTrick.class)); // starting 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 predicting_AddUp() {
        // play cards in given order
        MessageQueue queue = new MessageQueue()
                .addPrediction(players[0], 1)
                .addPrediction(players[1], 1)
                .addPrediction(players[2], 1)
                .begin()
                    .addPrediction(players[3], 1).assertThrows(IllegalArgumentException.class)
                    .addPrediction(players[3], 0)
                .end();

        Game game = performTest(Configurations.ANNIVERSARY_2021_PM1, 3, queue);

        // validate messages
        InOrder order = inOrder(game);
        order.verify(game).notify(any(StateMessage.class)); // predicting
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(Predicting.class)); // next player
        order.verify(game).notify(any(UserInputMessage.class)); // user input request
        order.verify(game).notify(any(PredictionMessage.class)); // user response
        order.verify(game).transition(any(), any(StartingTrick.class)); // starting trick
        order.verify(game).notify(any(StateMessage.class)); // finish
        order.verify(game, never()).notify(any());
        order.verify(game, never()).notify(any(), any());
    }
}
