#include "gtest/gtest.h"
//
// Created by sebastian on 05.08.26.
//
#include "helpers/SequenceDiceSource.h"
#include "ludo/LudoGameMode.h"

TEST(LudoGameMode, SecondSixMovesOutOfBlockedEntry) {
    ludo::LudoGameMode mode({0});
    mode.setDiceSource(std::make_unique<SequenceDiceSource>(std::vector<int>{6, 6}));

    mode.sendCommand(ludo::RollDiceCommand{0});
    mode.pollEvents(); // Events des ersten Wurfs verwerfen, nicht Teil dieser Prüfung
    ASSERT_EQ(mode.getCurrentPlayer(), 0u);
    ASSERT_EQ(mode.getState().getPhase(), ludo::TurnPhase::AwaitingRoll);

    mode.sendCommand(ludo::RollDiceCommand{0});
    mode.pollEvents(); // Events des zweiten Wurfs verwerfen
    ASSERT_EQ(mode.getState().getPhase(), ludo::TurnPhase::AwaitingPieceSelection);

    auto movable = mode.getMoveablePieceIndices(ludo::PlayerColor::Red, mode.getState().getDiceValue());
    ASSERT_EQ(movable.size(), 1u);
    ASSERT_EQ(movable[0], 0);

    mode.sendCommand(ludo::MovePieceCommand{0, 0});

    auto events = mode.pollEvents(); // jetzt NUR die Events dieses einen Commands
    ASSERT_FALSE(events.empty());
    auto* moved = std::get_if<ludo::PieceMovedEvent>(&events.front());
    ASSERT_NE(moved, nullptr);
    EXPECT_EQ(moved->fromPosition, 0);
    EXPECT_EQ(moved->toPosition, 6);
    EXPECT_FALSE(moved->captured);

    const auto& piece = mode.getState().getPieces()[0];
    EXPECT_EQ(piece.state, ludo::PieceState::OnTrack);
    EXPECT_EQ(piece.position, 6);
}
using ColorPosParam = std::tuple<ludo::PlayerColor, int>;

class MoveOutOfHomeTest : public ::testing::TestWithParam<ColorPosParam> {};

void assertPathIsWellFormed(const std::vector<ludo::PathNode>& path) {
    using ludo::PieceState;
    for (size_t i = 1; i < path.size(); ++i) {
        const auto& prev = path[i - 1];
        const auto& cur = path[i];

        if (prev.state == PieceState::OnTrack && cur.state == PieceState::OnTrack) {
            EXPECT_EQ(cur.position, (prev.position + 1) % ludo::kTrackLength) << "at i=" << i;
        } else if (prev.state == PieceState::InHomeStretch && cur.state == PieceState::InHomeStretch) {
            EXPECT_EQ(cur.position, prev.position + 1) << "at i=" << i;
        } else if (prev.state == PieceState::OnTrack && cur.state == PieceState::InHomeStretch) {
            EXPECT_EQ(cur.position, 0) << "at i=" << i;
        } else if (prev.state == PieceState::InHomeStretch && cur.state == PieceState::Finished) {
            EXPECT_EQ(prev.position, ludo::kHomeStretchLength - 1) << "at i=" << i;
        } else if (prev.state == PieceState::AtHome && cur.state == PieceState::OnTrack) {
            // ok, Position ist der Entry-Offset - separat geprüft
        } else {
            ADD_FAILURE() << "unerwarteter Zustandswechsel bei i=" << i;
        }
    }
}

TEST_P(MoveOutOfHomeTest, EntersTrackAtEntryOffset) {
    auto [color, startPos] = GetParam();
    const int entry = ludo::LudoGameMode::getEntryOffset(color);

    auto path = ludo::LudoGameMode::computePath(
        color, ludo::PieceState::AtHome, startPos,
        ludo::PieceState::OnTrack, entry);

    ASSERT_EQ(path.size(), 2u);
    EXPECT_EQ(path[0].state, ludo::PieceState::AtHome);
    EXPECT_EQ(path[0].position, startPos);
    EXPECT_EQ(path[1].state, ludo::PieceState::OnTrack);
    EXPECT_EQ(path[1].position, entry);
    assertPathIsWellFormed(path);
}

INSTANTIATE_TEST_SUITE_P(
    AllColorsAllHomeSlots,
    MoveOutOfHomeTest,
    ::testing::Combine(
        ::testing::Values(ludo::PlayerColor::Red, ludo::PlayerColor::Blue,
                           ludo::PlayerColor::Green, ludo::PlayerColor::Yellow),
        ::testing::Range(0, 4)));

int expectedStepCount(ludo::PlayerColor color, ludo::PieceState fromState, int fromPos, ludo::PieceState toState, int toPos) {
    using ludo::PieceState;
    const int entry = ludo::LudoGameMode::getEntryOffset(color);
    int steps = 0;
    PieceState state = fromState;
    int pos = fromPos;

    if (state == PieceState::AtHome) {
        steps += 1;
        state = PieceState::OnTrack;
        pos = entry;
    }
    if (state == PieceState::OnTrack) {
        if (toState == PieceState::OnTrack) {
            return steps + (toPos - pos + ludo::kTrackLength) % ludo::kTrackLength;
        }
        steps += (entry - pos + ludo::kTrackLength) % ludo::kTrackLength; // bis zum Entry
        steps += 1; // Abbiegen in Home Stretch (Pos 0)
        state = PieceState::InHomeStretch;
        pos = 0;
    }
    // state == InHomeStretch
    if (toState == PieceState::InHomeStretch) {
        return steps + (toPos - pos);
    }
    // toState == Finished
    return steps + (ludo::kHomeStretchLength - 1 - pos) + 1;
}



TEST(LudoGameMode, PathLengthMatchesIndependentFormula) {
    using namespace ludo;
    const std::vector<PlayerColor> colors = {
        PlayerColor::Red, PlayerColor::Blue, PlayerColor::Green, PlayerColor::Yellow};

    for (auto color : colors) {
        for (int start = 0; start < kTrackLength; ++start) {
            auto path = LudoGameMode::computePath(
                color, PieceState::OnTrack, start,
                PieceState::InHomeStretch, 0);

            EXPECT_EQ(path.size() - 1,
                      expectedStepCount(color, PieceState::OnTrack, start,
                                         PieceState::InHomeStretch, 0))
                << "color=" << static_cast<int>(color) << " start=" << start;

            EXPECT_EQ(path.front(), (PathNode{PieceState::OnTrack, start}));
            EXPECT_EQ(path.back(), (PathNode{PieceState::InHomeStretch, 0}));

            assertPathIsWellFormed(path);
        }
    }
}

TEST(ComputePath, CaptureFromOnTrackReturnsAtHome) {
    auto path = ludo::LudoGameMode::computePath(
        ludo::PlayerColor::Red, ludo::PieceState::OnTrack, 17,
        ludo::PieceState::AtHome, 2);

    ASSERT_EQ(path.size(), 2u);
    EXPECT_EQ(path[0].state, ludo::PieceState::OnTrack);
    EXPECT_EQ(path[0].position, 17);
    EXPECT_EQ(path[1].state, ludo::PieceState::AtHome);
    EXPECT_EQ(path[1].position, 2);
}

TEST(ComputePath, ThrowsOnCaptureFromInHomeStretch) {
    EXPECT_THROW(
        ludo::LudoGameMode::computePath(
            ludo::PlayerColor::Red, ludo::PieceState::InHomeStretch, 1,
            ludo::PieceState::AtHome, 0),
        std::invalid_argument);
}

TEST(ComputePath, ThrowsOnCaptureFromAtHome) {
    EXPECT_THROW(
        ludo::LudoGameMode::computePath(
            ludo::PlayerColor::Red, ludo::PieceState::AtHome, 0,
            ludo::PieceState::AtHome, 1), // targetPosition != startPosition, also kein Early-Return
        std::invalid_argument);
}