#include "catch.hpp"

#include <map>
#include <optional>
#include <utility>

#include <QPoint>
#include <QVector2D>

#include "BuildingType.h"
#include "Rotation.h"
#include "TunnelCompletion.h"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

namespace
{

using TunnelMap = std::map<std::pair<int, int>, TunnelTileInfo>;

TunnelLookup makeLookup(const TunnelMap& tunnels)
{
    return [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
    {
        const TunnelMap::const_iterator it = tunnels.find({tile.x(), tile.y()});
        if (it == tunnels.end()) { return std::nullopt; }
        return it->second;
    };
}

// Cursor at the centre of a tile.
QVector2D tileCentre(QPoint tile)
{
    return QVector2D(static_cast<float>(tile.x()) + 0.5f,
                     static_cast<float>(tile.y()) + 0.5f);
}

}  // namespace

// ---------------------------------------------------------------------------
// No match
// ---------------------------------------------------------------------------

TEST_CASE("No tunnels: ghost defaults to a Tunnel Entry with no partner")
{
    const TunnelMap tunnels;
    const TunnelCompletion result = resolveTunnelCompletion(
        makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));

    REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
    REQUIRE(!result.partnerTile.has_value());
}

// ---------------------------------------------------------------------------
// Entry-completion: an existing exit ahead keeps the ghost an entry
// ---------------------------------------------------------------------------

TEST_CASE("Existing exit ahead completes as an entry")
{
    TunnelMap tunnels;
    tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East};

    const TunnelCompletion result = resolveTunnelCompletion(
        makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));

    REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
    REQUIRE(result.partnerTile.has_value());
    REQUIRE(*result.partnerTile == QPoint(8, 5));
}

// ---------------------------------------------------------------------------
// Exit-completion: an existing entry behind flips the ghost to an exit
// ---------------------------------------------------------------------------

TEST_CASE("Existing entry behind completes as an exit")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};

    const TunnelCompletion result = resolveTunnelCompletion(
        makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));

    REQUIRE(result.resolvedType == BuildingType::TunnelExit);
    REQUIRE(result.partnerTile.has_value());
    REQUIRE(*result.partnerTile == QPoint(2, 5));
}

// ---------------------------------------------------------------------------
// Both matches: the sub-tile cursor position breaks the tie
// ---------------------------------------------------------------------------

TEST_CASE("Both matches resolve to the partner nearer the sub-tile cursor")
{
    TunnelMap tunnels;
    tunnels[{3, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // behind
    tunnels[{7, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East}; // ahead

    const TunnelLookup lookup = makeLookup(tunnels);

    SECTION("cursor near the left edge completes the entry (ghost becomes exit)")
    {
        const QVector2D cursor(5.1f, 5.5f);
        const TunnelCompletion result =
            resolveTunnelCompletion(lookup, QPoint(5, 5), Rotation::East, 10, cursor);
        REQUIRE(result.resolvedType == BuildingType::TunnelExit);
        REQUIRE(*result.partnerTile == QPoint(3, 5));
    }

    SECTION("cursor near the right edge completes the exit (ghost stays entry)")
    {
        const QVector2D cursor(5.9f, 5.5f);
        const TunnelCompletion result =
            resolveTunnelCompletion(lookup, QPoint(5, 5), Rotation::East, 10, cursor);
        REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
        REQUIRE(*result.partnerTile == QPoint(7, 5));
    }
}

// ---------------------------------------------------------------------------
// A same-direction tunnel between the ghost and a candidate blocks the pairing
// ---------------------------------------------------------------------------

TEST_CASE("A same-direction entry between the ghost and an exit blocks completion")
{
    TunnelMap tunnels;
    tunnels[{7, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // blocker
    tunnels[{9, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East};

    const TunnelCompletion result = resolveTunnelCompletion(
        makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));

    // The forward scan stops at the same-direction entry, which is not an exit, so no
    // entry-completion. Nothing behind, so no exit-completion either.
    REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
    REQUIRE(!result.partnerTile.has_value());
}

// ---------------------------------------------------------------------------
// Differently-facing tunnels are skipped by the scan
// ---------------------------------------------------------------------------

TEST_CASE("A differently-facing tunnel ahead is skipped, not treated as a match")
{
    TunnelMap tunnels;
    tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::North}; // skipped
    tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East};  // match

    const TunnelCompletion result = resolveTunnelCompletion(
        makeLookup(tunnels), QPoint(5, 5), Rotation::East, 10, tileCentre(QPoint(5, 5)));

    REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
    REQUIRE(*result.partnerTile == QPoint(8, 5));
}

// ---------------------------------------------------------------------------
// Distance limit
// ---------------------------------------------------------------------------

TEST_CASE("A candidate beyond the max distance is not matched")
{
    TunnelMap tunnels;
    tunnels[{8, 5}] = TunnelTileInfo{BuildingType::TunnelExit, Rotation::East}; // distance 3

    const TunnelCompletion result = resolveTunnelCompletion(
        makeLookup(tunnels), QPoint(5, 5), Rotation::East, 2, tileCentre(QPoint(5, 5)));

    REQUIRE(result.resolvedType == BuildingType::TunnelEntry);
    REQUIRE(!result.partnerTile.has_value());
}

// ---------------------------------------------------------------------------
// findTunnelPartner: the matching end of an existing selected tunnel
// (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT)
// ---------------------------------------------------------------------------

TEST_CASE("findTunnelPartner: a selected entry finds its exit ahead")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};
    tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East};

    const std::optional<QPoint> partner = findTunnelPartner(
        makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 10);

    REQUIRE(partner.has_value());
    REQUIRE(*partner == QPoint(6, 5));
}

TEST_CASE("findTunnelPartner: a selected exit finds its entry behind")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};
    tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East};

    const std::optional<QPoint> partner = findTunnelPartner(
        makeLookup(tunnels), QPoint(6, 5), BuildingType::TunnelExit, Rotation::East, 10);

    REQUIRE(partner.has_value());
    REQUIRE(*partner == QPoint(2, 5));
}

TEST_CASE("findTunnelPartner: an unpaired tunnel has no partner")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};

    REQUIRE(!findTunnelPartner(makeLookup(tunnels), QPoint(2, 5),
                               BuildingType::TunnelEntry, Rotation::East, 10).has_value());
}

TEST_CASE("findTunnelPartner: a same-direction tunnel between blocks the pairing")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // selected
    tunnels[{4, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East}; // blocker
    tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East};

    // The scan from the selected entry stops at the same-direction entry, which is not
    // an exit, so the exit beyond it is not paired.
    REQUIRE(!findTunnelPartner(makeLookup(tunnels), QPoint(2, 5),
                               BuildingType::TunnelEntry, Rotation::East, 10).has_value());
}

TEST_CASE("findTunnelPartner: a differently-facing tunnel is skipped")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};
    tunnels[{4, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::North}; // skipped
    tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East};  // match

    const std::optional<QPoint> partner = findTunnelPartner(
        makeLookup(tunnels), QPoint(2, 5), BuildingType::TunnelEntry, Rotation::East, 10);

    REQUIRE(partner.has_value());
    REQUIRE(*partner == QPoint(6, 5));
}

TEST_CASE("findTunnelPartner: a partner beyond the max distance is not matched")
{
    TunnelMap tunnels;
    tunnels[{2, 5}] = TunnelTileInfo{BuildingType::TunnelEntry, Rotation::East};
    tunnels[{6, 5}] = TunnelTileInfo{BuildingType::TunnelExit,  Rotation::East}; // distance 4

    REQUIRE(!findTunnelPartner(makeLookup(tunnels), QPoint(2, 5),
                               BuildingType::TunnelEntry, Rotation::East, 3).has_value());
}
