#include <library/cpp/testing/gtest/gtest.h>
#include <maps/wikimap/mapspro/services/mrc/libs/graph/include/graph.h>
#include <util/stream/output.h>
#include <util/string/cast.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <boost/range/algorithm.hpp>

#include <sstream>

template <>
void Out<maps::mrc::graph::DirectedId>(IOutputStream& out, const maps::mrc::graph::DirectedId& id)
{
    out << id.roadElementId() << ':' << id.direction();
}

template <>
void Out<maps::mrc::graph::Trace>(IOutputStream& out, const maps::mrc::graph::Trace& trace)
{
    auto toString = [&](const auto& id) { return std::string(ToString(id)); };

    out << "[" << maps::wiki::common::join(trace, toString, ",") << "]";
}

namespace maps::mrc::graph::tests {

using namespace object;

/* ROAD GRAPH SCHEME:

                  [105]
            (206)<ooooo>(205)       (207)
                   <----  ^           ^
                        | o           o
                        | o   [104]   o
                  {303} | o |         o [106]
                        | o | {302}   o
     <--{301}           | o |         o
     ----          -----x v x---->    v   [108]       [109]
(201)<ooooo>(202)<ooooo>(203)<ooooo>(204)<ooooo>(209)<ooooo>(210)
      [101]       [102]       [103]   ^
                                      o
                                      o
                                      o [107]
                                      o
                                      o
                                      v
                                    (208)
*/
const RoadElements ROAD_ELEMENTS {
    RoadElement(
        RevisionID{101, 1},
        geolib3::Polyline2{geolib3::PointsVector{{0, 0}, {1, 0}}}
    )
        .startZLevel(0)
        .startJunctionId(201)
        .endZLevel(0)
        .endZLevel(0)
        .endJunctionId(202)
        .direction(RoadElement::Direction::Both)
        .accessId(AccessId::Car | AccessId::Truck | AccessId::Bus)
    , /////////////////////////////////////////////////
    RoadElement(
        RevisionID{102, 1},
        geolib3::Polyline2{geolib3::PointsVector{{1, 0}, {2, 0}}}
    )
        .startZLevel(0)
        .startJunctionId(202)
        .endZLevel(0)
        .endJunctionId(203)
        .direction(RoadElement::Direction::Both)
        .accessId(AccessId::Car | AccessId::Truck | AccessId::Bus)
    , /////////////////////////////////////////////////
    RoadElement(
        RevisionID{103, 1},
        geolib3::Polyline2{geolib3::PointsVector{{2, 0}, {3, 0}}}
    )
        .startZLevel(0)
        .startJunctionId(203)
        .endZLevel(0)
        .endJunctionId(204)
        .direction(RoadElement::Direction::Both)
        .accessId(AccessId::Car | AccessId::Truck | AccessId::Bus)
    , /////////////////////////////////////////////////
    RoadElement(
        RevisionID{104, 1},
        geolib3::Polyline2{geolib3::PointsVector{{2, 0}, {2, 1}}}
    )
        .startZLevel(0)
        .startJunctionId(203)
        .endZLevel(0)
        .endJunctionId(205)
        .direction(RoadElement::Direction::Both)
        .accessId(AccessId::Car | AccessId::Bus)
    , /////////////////////////////////////////////////
    RoadElement(
        RevisionID{105, 1},
        geolib3::Polyline2{geolib3::PointsVector{{2, 1}, {1, 1}}}
    )
        .startZLevel(0)
        .startJunctionId(205)
        .endZLevel(0)
        .endJunctionId(206)
        .direction(RoadElement::Direction::Both)
        .accessId(AccessId::Car | AccessId::Bus)
    , /////////////////////////////////////////////////
    RoadElement(
            RevisionID{106, 1},
            geolib3::Polyline2{geolib3::PointsVector{{3, 0}, {3, 1}}}
        )
            .startZLevel(0)
            .startJunctionId(207)
            .endJunctionId(204)
            .endZLevel(1)
            .direction(RoadElement::Direction::Backward)
            .accessId(AccessId::Car)
    , /////////////////////////////////////////////////
    RoadElement(
            RevisionID{107, 1},
            geolib3::Polyline2{geolib3::PointsVector{{3, 0}, {3, -1}}}
        )
            .startZLevel(0)
            .startJunctionId(208)
            .endZLevel(1)
            .endJunctionId(204)
            .direction(RoadElement::Direction::Forward)
            .accessId(AccessId::Car)
    , /////////////////////////////////////////////////
    RoadElement(
            RevisionID{108, 1},
            geolib3::Polyline2{geolib3::PointsVector{{3, 0}, {4, 0}}}
        )
            .startZLevel(0)
            .startJunctionId(204)
            .endZLevel(0)
            .endJunctionId(209)
            .direction(RoadElement::Direction::Both)
            .accessId(AccessId::Car | AccessId::Truck | AccessId::Bus)
    , /////////////////////////////////////////////////
    RoadElement(
            RevisionID{109, 1},
            geolib3::Polyline2{geolib3::PointsVector{{4, 0}, {5, 0}}}
        )
            .startZLevel(0)
            .startJunctionId(209)
            .endZLevel(0)
            .endJunctionId(210)
            .direction(RoadElement::Direction::Both)
            .accessId(AccessId::Car | AccessId::Bus)
};

const Conditions CONDITIONS {
    Condition(RevisionID{301, 1})
        .type(Condition::Type::Prohibited)
        .fromElementId(102)
        .viaJunctionId(203)
        .toElementIds({104, 105})
        .accessId(AccessId::Car)
    , /////////////////////////////////////////////////
    Condition(RevisionID{302, 1})
        .type(Condition::Type::Uturn)
        .fromElementId(101)
        .viaJunctionId(202)
        .toElementIds({101})
        .accessId(AccessId::Car)
    , /////////////////////////////////////////////////
    Condition(RevisionID{303, 1})
        .type(Condition::Type::Prohibited)
        .fromElementId(104)
        .viaJunctionId(203)
        .toElementIds({103})
        .accessId(AccessId::Car)
    , /////////////////////////////////////////////////
};

inline Graph makeTestGraph(AccessId accessId=AccessId::Car)
{
    return Graph(accessId, ROAD_ELEMENTS, CONDITIONS);
}

DirectedIds getAllPossibleDirectedId()
{
    DirectedIds directedIds;

    for (const auto& element: ROAD_ELEMENTS) {
        if (ymapsdf::rd::isSet(Direction::Forward, element.direction())) {
            directedIds.emplace_back(element.id(), Direction::Forward);
        }

        if (ymapsdf::rd::isSet(Direction::Backward, element.direction())) {
            directedIds.emplace_back(element.id(), Direction::Backward);
        }
    }

    return directedIds;
}

void checkTraceOfEndNodes(Graph graph, const Trace& startTrace, Traces expectedEndTraces)
{
    const NodeId startNodeId = graph.getNodeId(startTrace);

    Traces returnedEndTraces;
    for (const auto& edge: graph.edges(startNodeId)) {
        returnedEndTraces.push_back(graph.getTrace(edge.endNodeId()));
    }

    boost::sort(returnedEndTraces);
    boost::sort(expectedEndTraces);

    EXPECT_EQ(returnedEndTraces, expectedEndTraces);
}

TEST(graph, get_node_id)
{
    const DirectedIds allDirectedIds = getAllPossibleDirectedId();
    std::set<NodeId> nodeIds;

    auto graph = makeTestGraph();
    for (const auto& directedId: allDirectedIds) {
        const auto nodeId = graph.getNodeId(directedId);
        EXPECT_TRUE(nodeId);

        const auto [_, isNew] = nodeIds.insert(*nodeId);
        EXPECT_TRUE(isNew);

        Trace expected{directedId};
        EXPECT_EQ(graph.getTrace(*nodeId), expected);
    }
}

TEST(graph, no_edges)
{
    const Trace from{{105, Direction::Forward}};
    const Traces expected{};

    checkTraceOfEndNodes(makeTestGraph(), from, expected);
}

TEST(graph, check_node_incidence)
{
    {
        const Trace from{{103, Direction::Forward}};
        const Traces expected{
            Trace{{108, Direction::Forward}}
        };

        checkTraceOfEndNodes(makeTestGraph(), from, expected);
    }

    {
        const Trace from{{107, Direction::Forward}};
        const Traces expected{
            Trace{{106, Direction::Backward}}
        };

        checkTraceOfEndNodes(makeTestGraph(), from, expected);
    }
}

TEST(graph, u_turn)
{
    const Trace from{{101, Direction::Forward}};

    {   // Car
        const Traces expected{
            {{101, Direction::Backward}},
            {{102, Direction::Forward}},
        };

        checkTraceOfEndNodes(makeTestGraph(AccessId::Car), from, expected);
    }

    {   // Car and Truck
        const Traces expected{
            {{102, Direction::Forward}},
        };

        checkTraceOfEndNodes(makeTestGraph(AccessId::Car | AccessId::Truck), from, expected);
    }
}

TEST(graph, short_forbidden_condition)
{
    const Graph graph = makeTestGraph();

    const Trace one{{105, Direction::Backward}};
    const Trace two{{104, Direction::Backward}};

    {   // Condition entry
        const Traces expected{two};
        checkTraceOfEndNodes(makeTestGraph(), one, expected);
    }

    {   // Pass condition
        const Traces expected{
            {{102, Direction::Backward}},
        };

        checkTraceOfEndNodes(makeTestGraph(), two, expected);
        checkTraceOfEndNodes(makeTestGraph(AccessId::Bus | AccessId::Car), two, expected);
    }

    {   // Bus
        const Traces expected{
            {{102, Direction::Backward}},
            {{103, Direction::Forward}},
        };

        checkTraceOfEndNodes(makeTestGraph(AccessId::Bus), two, expected);
    }
}

TEST(graph, long_forbidden_condition)
{
    const Trace one{{101, Direction::Forward}};
    const Trace two{{102, Direction::Forward}};

    const Trace three{{102, Direction::Forward}, {104, Direction::Forward}};

    {   // Condition entry
        const Traces expected{
            {{101, Direction::Backward}},
            two,
        };
        checkTraceOfEndNodes(makeTestGraph(), one, expected);
    }

    {   // Walk in condition
        const Traces expected{
            three,
            {{103, Direction::Forward}},
        };
        checkTraceOfEndNodes(makeTestGraph(), two, expected);
    }

    {   // End of long condition
        checkTraceOfEndNodes(makeTestGraph(), three, {});
    }
}

TEST(graph, element_access_id)
{
    const Trace from{{108, Direction::Forward}};
    const Trace to{{109, Direction::Forward}};


    {   // Car
        const Traces expected{to};
        checkTraceOfEndNodes(makeTestGraph(AccessId::Car), from, expected);
    }

    {   // Truck
        const Traces expected{};
        checkTraceOfEndNodes(makeTestGraph(AccessId::Truck), from, expected);
    }

    {   // Car and Truck
        const Traces expected{};
        checkTraceOfEndNodes(makeTestGraph(AccessId::Car | AccessId::Truck), from, expected);
    }
}

} //namespace maps::mrc::graph::tests
