#include "common.h"
#include "fixture.h"
#include "mocks.h"

#include <maps/wikimap/mapspro/services/mrc/browser/lib/configuration.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/graph_coverage_explorer.h>

namespace maps::mrc::browser::tests {
namespace {

const Meters FRONT_VISIBILITY_METERS{export_gen::COVERAGE_METERS_MAX_PATH};
const auto DEFAULT_TIMESTAMP = chrono::parseSqlDateTime("2020-01-01 12:00:00+03");

bool nearBy(
    const geolib3::Point2& lhs,
    const geolib3::Point2& rhs,
    Meters by = Meters{1})
{
    return Meters{geolib3::fastGeoDistance(lhs, rhs)} <= by;
}

bool nearBy(
    geolib3::Radians lhs,
    geolib3::Radians rhs,
    geolib3::Radians eps = geolib3::PI / 60)
{
    return std::abs((lhs - rhs).value()) < std::abs(eps.value());
}

bool nearBy(
    geolib3::Direction2 lhs,
    geolib3::Direction2 rhs,
    geolib3::Radians eps = geolib3::PI / 60)
{
    return nearBy(lhs.radians(), rhs.radians(), eps);
}

bool nearBy(
    db::Ray lhs,
    db::Ray rhs,
    Meters meters = Meters{1},
    geolib3::Radians radians = geolib3::PI / 60)
{
    return nearBy(lhs.pos, rhs.pos, meters) &&
           nearBy(lhs.direction, rhs.direction, radians);
}

std::tuple<geolib3::Polyline2, Meters> makePolyline(
    geolib3::PointsVector points)
{
    const geolib3::Polyline2 polyline(points);
    const Meters length{geolib3::geoLength(polyline)};
    return std::make_tuple(polyline, length);
}

std::optional<common::geometry::PolylinePosition> moveBy(
    const geolib3::Polyline2& polyline,
    const common::geometry::PolylinePosition& from,
    Meters by)
{
    ASSERT(from.segmentIdx() < polyline.segmentsNumber());

    auto current = from;
    const auto segment = polyline.segmentAt(current.segmentIdx());

    Meters currentSegmentLen = Meters{geoLength(segment)};
    Meters currentPosMeters =
        currentSegmentLen * current.segmentRelPosition();

    Meters toTravel = by;
    while (currentSegmentLen - currentPosMeters < toTravel) {
        if (polyline.segmentsNumber() <= current.segmentIdx() + 1) {
            return std::nullopt;
        }

        toTravel -= currentSegmentLen - currentPosMeters;

        current =
            common::geometry::PolylinePosition{current.segmentIdx() + 1, 0.0};

        currentSegmentLen =
            Meters{geoLength(polyline.segmentAt(current.segmentIdx()))};

        currentPosMeters = Meters{0};
    }

    return common::geometry::PolylinePosition{
        current.segmentIdx(),
        current.segmentRelPosition() + toTravel / currentSegmentLen};
}

std::optional<std::tuple<geolib3::Segment2, geolib3::Point2>>
segmentPointAt(const geolib3::Polyline2& polyline, Meters at)
{
    common::geometry::PolylinePosition start{0, 0.0};

    const auto pos = moveBy(polyline, start, at);
    if (!pos) {
        return std::nullopt;
    }

    const auto segment = polyline.segmentAt(pos->segmentIdx());
    const auto point = segment.pointByPosition(pos->segmentRelPosition());

    return std::make_tuple(segment, point);
}

enum class Side { Left, Right };

geolib3::Point2 sideShiftBy(
    const geolib3::Segment2& segment,
    const geolib3::Point2& point,
    Side side,
    Meters sideShift)
{
    const auto direction = geolib3::fastGeoDirection(segment);
    switch (side) {
        case Side::Left:
            return geolib3::fastGeoShift(
                point,
                sideShift.value() *
                    geolib3::rotateBy90(
                        direction.vector(), geolib3::Counterclockwise));
            break;
        case Side::Right:
            return geolib3::fastGeoShift(
                point,
                sideShift.value() *
                    geolib3::rotateBy90(
                        direction.vector(), geolib3::Clockwise));
        default:
            ASSERT(false);
    }
}

std::optional<geolib3::Point2> optGeoPointAt(
    const geolib3::Polyline2& polyline,
    Meters at,
    std::optional<Side> side = std::nullopt,
    Meters sideShift = Meters{3})
{
    const auto optSegmentPoint = segmentPointAt(polyline, at);
    if (!optSegmentPoint) {
        return std::nullopt;
    }

    const auto [segment, point] = *optSegmentPoint;

    if (side) {
        return sideShiftBy(segment, point, *side, sideShift);
    } else {
        return point;
    }
}

geolib3::Point2 geoPointAt(
    const geolib3::Polyline2& polyline,
    Meters at,
    std::optional<Side> side = std::nullopt,
    Meters sideShift = Meters{3})
{
    const auto point = optGeoPointAt(polyline, at, side, sideShift);
    ASSERT(point);
    return *point;
}

std::optional<db::Ray> optRayAt(
    const geolib3::Polyline2& polyline,
    Meters at,
    std::optional<Side> side = std::nullopt,
    Meters sideShift = Meters{3})
{
    const auto optSegmentPoint = segmentPointAt(polyline, at);
    if (!optSegmentPoint) {
        return std::nullopt;
    }

    const auto [segment, point] = *optSegmentPoint;
    const auto direction = geolib3::fastGeoDirection(segment);
    if (side) {
        return db::Ray{sideShiftBy(segment, point, *side, sideShift), direction};
    } else {
        return db::Ray{point, direction};
    }
}

db::Ray rayAt(
    const geolib3::Polyline2& polyline,
    Meters at = Meters{0},
    std::optional<Side> side = std::nullopt,
    Meters sideShift = Meters{3})
{
    const auto optRay = optRayAt(polyline, at, side, sideShift);
    ASSERT(optRay);
    return *optRay;
}

db::Features makeFeatures(const geolib3::Polyline2& polyline,
                          Meters distributionDistance,
                          Meters startFrom)
{
    ASSERT(Meters{1} <= distributionDistance);
    ASSERT(startFrom <= Meters{geolib3::geoLength(polyline)});

    db::Features features;

    static auto globalSourceId = 1;
    auto sourceId = std::to_string(++globalSourceId);
    auto current = moveBy(polyline, {0, 0.0}, startFrom);
    auto timestamp = chrono::parseSqlDateTime("2021-11-22 11:54:00+03");
    while (current) {
        const auto segment = polyline.segmentAt(current->segmentIdx());
        const auto position =
            segment.pointByPosition(current->segmentRelPosition());
        const auto heading =
            geolib3::Direction2(geolib3::convertGeodeticToMercator(segment))
                .heading();

        features
            .emplace_back(
                sourceId,
                position,
                heading,
                chrono::formatSqlDateTime(timestamp),
                mds::Key("group", "path"),
                db::Dataset::Agents)
            .setSize(1920, 1080)
            .setAutomaticShouldBePublished(true)
            .setIsPublished(true)
            .setCameraDeviation(db::CameraDeviation::Front)
            .setPrivacy(db::FeaturePrivacy::Public);

        timestamp += RIDE_BREAK_INTERVAL;

        current = moveBy(polyline, *current, distributionDistance);
    }

    return features;
}

const fb::TEdgeCoverage* testCoverageSelector(const fb::TEdge& coveredEdge)
{
    for (const auto& coverage: coveredEdge.coverages) {
        if (coverage.cameraDeviation == db::CameraDeviation::Front &&
            coverage.privacy <= db::FeaturePrivacy::Public) {
            return &coverage;
        }
    }
    return nullptr;
}

class TestContext : public NUnitTest::TBaseFixture {
private:
    class GraphCoverageExplorerFixture : public Fixture {
    public:
        GraphCoverageExplorerFixture(db::Features& features)
            : Fixture([&](pgpool3::Pool& pool) {
                auto txn = pool.masterWriteableTransaction();

                for (auto& feature: features) {
                    // Guarantee feature IDs persist from run to run
                    db::FeatureGateway{*txn}.insert(feature);
                }
                db::updateFeaturesTransaction(features, *txn);
                txn->commit();
                Playground::instance().makeCoverage();
            })
        { }

        const IGraph& graph()
        {
            if (!roadGraph_) {
                roadGraph_ = Configuration::instance()->roadGraph();
            }
            return *roadGraph_;
        }

        GraphCoverageExplorer explorer()
        {
            return GraphCoverageExplorer{graph(), testCoverageSelector};
        }

    private:
        std::shared_ptr<IGraph> roadGraph_;
    };

public:
    void addFeatures(const db::Features& features)
    {
        REQUIRE(
            !innerFixture_,
            "Cannot add more features after inner fixture instantiation");
        features_.insert(features_.end(), features.begin(), features.end());
    }

    void addFeatures(
        const geolib3::Polyline2& polyline,
        Meters distributionDistance = FRONT_VISIBILITY_METERS / 2,
        Meters startFrom = Meters{0})
    {
        addFeatures(makeFeatures(polyline, distributionDistance, startFrom));
    }

    GraphCoverageExplorer explorer()
    {
        return innerFixture().explorer();
    }

    const IGraph& graph() {
        return innerFixture().graph();
    }

    const db::Features& features() const {
        return features_;
    }

private:
    GraphCoverageExplorerFixture& innerFixture()
    {
        if (!innerFixture_) {
            innerFixture_.reset(new GraphCoverageExplorerFixture{features_});
        }
        return *innerFixture_;
    };

    db::Features features_;
    std::unique_ptr<GraphCoverageExplorerFixture> innerFixture_;
};

// test tools
} // namespace

Y_UNIT_TEST_SUITE_F(graph_coverage_explorer_tests, TestContext)
{
    Y_UNIT_TEST(find_by_point_single_backward_coverage)
    {
        // single coverage on the edge before of a snapped point.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length + 1);

        const geolib3::Point2 point = geoPointAt(polyline, length / 3);;
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline,  Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_point_single_forward_coverage)
    {
        // single coverage on the edge ahead of a snapped point.
         const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length, length - 1);

        const geolib3::Point2 point = geoPointAt(polyline, length / 1.5);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, length - 1);
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_point_no_close_coverage)
    {
        // no coverage within specified distance limit.
        const geolib3::Point2 point {37.612623, 55.746538};

        const auto node = explorer().find(point);
        EXPECT_FALSE(node);
    }

    Y_UNIT_TEST(find_by_point_closest_coverage_behind_same_edge)
    {
        // choose backward node as closest from a couple of edge nodes.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length - 1);

        const geolib3::Point2 point = geoPointAt(polyline, length / 3);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        EXPECT_TRUE(nearBy(polyline.pointAt(0), node->ray.pos));
        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_point_closest_coverage_ahead_same_edge)
    {
        // choose forward node as closest from a couple of edge nodes.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length - 1);

        const geolib3::Point2 point = geoPointAt(polyline, length / 1.5);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, length - 1);
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_point_coverage_starts_on_incoming_edge)
    {
        // pick backward coverage from an edge it begins from in case if a
        // feature covers two connected edges.
        const auto [polyline, length] = makePolyline(
            {{37.615697, 55.742119},
             {37.61573403, 55.74205591},
             {37.615772, 55.741991}});
        addFeatures(polyline, length + 1);

        const geolib3::Point2 point = geoPointAt(polyline, length / 1.5);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_point_select_right_edge)
    {
        // pick a node on the right side of road according to the local
        // traffic direction.
        const auto [toNorth, length] = makePolyline(
            {{37.569551, 55.791435}, {37.569574, 55.791569}});
        auto toSouth = toNorth; toSouth.reverse();

        addFeatures(toNorth, length + 1);
        addFeatures(toSouth, length + 1);

        const geolib3::Point2 point = geoPointAt(toNorth, length / 2, Side::Right);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(toNorth, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_point_select_left_edge)
    {
        // pick a node on the left side of road according to the local
        // traffic direction.
        const auto [toNorth, length] = makePolyline(
            {{37.569551, 55.791435}, {37.569574, 55.791569}});
        auto toSouth = toNorth; toSouth.reverse();

        addFeatures(toNorth, length + 1);
        addFeatures(toSouth, length + 1);

        const geolib3::Point2 point = geoPointAt(toSouth, length / 2, Side::Right);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(toSouth, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_single_backward_coverage_one_way)
    {
        // single coverage on the edge before of a snapped ray.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length + 1);

        const db::Ray ray = rayAt(polyline, length / 3);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_single_forward_coverage_one_way)
    {
        // single coverage on the edge ahead of a snapped ray.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length, length - 1);

        const db::Ray ray = rayAt(polyline, length / 1.5);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, length - 1);
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_single_backward_coverage_two_ways)
    {
        // pick a node before of a snapped ray on the right side of road according
        // to the ray direction and ignoring the local traffic direction.
        const auto [toNorth, length] = makePolyline(
            {{37.569551, 55.791435}, {37.569574, 55.791569}});
        auto toSouth = toNorth; toSouth.reverse();

        addFeatures(toNorth, length + 1);
        addFeatures(toSouth, length + 1);

        const db::Ray ray = rayAt(toNorth, length / 3, Side::Left);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(toNorth, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_single_forward_coverage_two_ways)
    {
        // pick a node ahead of a snapped ray on the right side of road according
        // to the ray direction and ignoring the local traffic direction.
        const auto [toNorth, length] = makePolyline(
            {{37.569551, 55.791435}, {37.569574, 55.791569}});
        auto toSouth = toNorth; toSouth.reverse();

        addFeatures(toNorth, length, length - 1);
        addFeatures(toSouth, length, length - 1);

        const db::Ray ray = rayAt(toNorth, length / 1.5, Side::Left);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(toNorth, length - 1);
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_no_close_coverage)
    {
        // no coverage within specified distance limit.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});

        const db::Ray ray = rayAt(polyline, length / 2);
        const auto node = explorer().find(ray);
        EXPECT_FALSE(node);
    }

    Y_UNIT_TEST(find_by_ray_closest_coverage_behind_same_edge)
    {
        // choose backward node as closest from a couple of edge nodes.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length - 1);

        const db::Ray ray = rayAt(polyline, length / 3);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_closest_coverage_ahead_same_edge)
    {
        // choose forward node as closest from a couple of edge nodes.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length - 1);

        const db::Ray ray = rayAt(polyline, length / 1.5);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, length - 1);
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_ray_closest_coverage_incoming_edge)
    {
        // pick backward coverage from an edge it begins from in case if a
        // feature covers two connected edges.
        const auto [polyline, length] = makePolyline(
            {{37.615697, 55.742119},
             {37.61573403, 55.74205591},
             {37.615772, 55.741991}});
        addFeatures(polyline, length + 1);

        const db::Ray ray = rayAt(polyline, length / 1.5);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
    }

    Y_UNIT_TEST(find_by_feature_success)
    {
        // find a node which corresponds to a given feature.
        const auto [polyline, length] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        addFeatures(polyline, length + 1);

        ASSERT_EQ(features().size(), 1u);
        const auto node = explorer().find(features().front());
        ASSERT_TRUE(node);
        const auto expectedRay = db::getRay(features().front());
        EXPECT_TRUE(nearBy(expectedRay, node->ray));
        EXPECT_EQ(features().front().id(), node->featureId);
    }

    Y_UNIT_TEST(find_by_feature_fail)
    {
        // fail to find a node which corresponds to a given feature because it
        // doesn't cover any graph edge.
        const auto [polyline, length] = makePolyline(
            {{37.428156, 55.796386}, {37.428156, 55.796417}});
        addFeatures(polyline);

        ASSERT_EQ(features().size(), 1u);
        const auto node = explorer().find(features().front());
        EXPECT_FALSE(node);
    }

    Y_UNIT_TEST(uturn_on_topological_reverse_edge)
    {
        // find uturn on topological reverse edge.
        const auto [toNorth, length] = makePolyline(
           {{37.569551, 55.791435}, {37.569574, 55.791569}});
        auto toSouth = toNorth; toSouth.reverse();

        addFeatures(toNorth, length, length / 2);
        addFeatures(toSouth, length, length / 2);

        const db::Ray ray = rayAt(toNorth, length / 2);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        const auto expectedNorthRay = rayAt(toNorth, length / 2);
        EXPECT_TRUE(nearBy(expectedNorthRay, node->ray));

        const auto uturn = explorer().uturn(*node);
        ASSERT_TRUE(uturn);
        const auto expectedSouthRay = rayAt(toSouth, length / 2);
        EXPECT_TRUE(nearBy(expectedSouthRay, uturn->ray));
    }

    Y_UNIT_TEST(uturn_on_geometrical_reverse_edge)
    {
        // find uturn on geometrical reverse edge if there is no topological one.
        const auto [toNorth, toNorthLength] = makePolyline(
            {{37.612587, 55.746565}, {37.612486, 55.746687}});
        const auto [toSouth, toSouthLength] = makePolyline(
            {{37.612221, 55.746641}, {37.612322, 55.746519}});

        addFeatures(toNorth, toNorthLength + 1, toNorthLength / 2);
        addFeatures(toSouth, toNorthLength + 1, toSouthLength / 2);

        const geolib3::Point2 point = geoPointAt(toNorth, toNorthLength / 2);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto uturn = explorer().uturn(*node);
        ASSERT_TRUE(uturn);
        const auto expectedSouthRay = rayAt(toSouth, toSouthLength / 2);
        EXPECT_TRUE(nearBy(expectedSouthRay, uturn->ray));
    }

    Y_UNIT_TEST(uturn_no_reverse_coverage)
    {
        // fail to find uturn
        const auto [toNorth, length] = makePolyline(
           {{37.569551, 55.791435}, {37.569574, 55.791569}});
        auto toSouth = toNorth; toSouth.reverse();

        addFeatures(toNorth, length, length / 2);

        const db::Ray ray = rayAt(toNorth, length / 2);
        const auto node = explorer().find(ray);
        ASSERT_TRUE(node);
        EXPECT_TRUE(nearBy(geolib3::findMiddle(toNorth), node->ray.pos));

        const auto uturn = explorer().uturn(*node);
        EXPECT_FALSE(uturn);
    }

    Y_UNIT_TEST(backward_absent)
    {
        // no backward connections.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        addFeatures(polyline, length, length - 1);

        const geolib3::Point2 point = geoPointAt(polyline, length - 1);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        EXPECT_TRUE(backwards.empty());
    }

    Y_UNIT_TEST(backward_not_connected)
    {
        // no backward adjacent connections.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        addFeatures(polyline, MAX_CONNECTIVITY_DISTANCE + 5);

        const geolib3::Point2 point =
            geoPointAt(polyline, MAX_CONNECTIVITY_DISTANCE + 5);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        EXPECT_TRUE(backwards.empty());
    }

    Y_UNIT_TEST(backward_on_tip_of_same_edge)
    {
        // pick a single backward edge connection from the same edge.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        addFeatures(polyline);

        const geolib3::Point2 point =
            geoPointAt(polyline, FRONT_VISIBILITY_METERS / 2);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        ASSERT_EQ(backwards.size(), 1u);
        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, backwards.front().ray));
    }

    Y_UNIT_TEST(backward_in_the_middle_of_same_edge)
    {
        // pick a single backward edge connection from the same edge.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        ASSERT_GT(length, FRONT_VISIBILITY_METERS * 3);
        addFeatures(polyline, FRONT_VISIBILITY_METERS / 2);

        const geolib3::Point2 point = geoPointAt(polyline, FRONT_VISIBILITY_METERS);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        ASSERT_EQ(backwards.size(), 1u);
        const auto expectedRay = rayAt(polyline, FRONT_VISIBILITY_METERS / 2);
        EXPECT_TRUE(nearBy(expectedRay, backwards.front().ray));
    }

    Y_UNIT_TEST(backward_from_incoming_edge)
    {
        // pick a single backward connection from a single incoming edge.
        const auto [polyline, length] = makePolyline(
            {{37.615697, 55.742119},
             {37.61573403, 55.74205591},
             {37.615772, 55.741991}});
        addFeatures(polyline, length / 3);

        const geolib3::Point2 point = geoPointAt(polyline, length / 3 * 2);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        ASSERT_EQ(backwards.size(), 1u);
        const auto expectedRay = rayAt(polyline, length / 3);
        EXPECT_TRUE(nearBy(expectedRay, backwards.front().ray));
    }

    Y_UNIT_TEST(backward_multiple_connections)
    {
        // pick multiple backward connections from multiple incoming edges.
        const auto [polyline1, length1] = makePolyline(
            {{37.607779, 55.747362},
             {37.607670, 55.747408},
             {37.607587, 55.747440}});
        const auto [polyline2, length2] = makePolyline(
            {{37.607537, 55.747296},
             {37.607670, 55.747408},
             {37.607800, 55.747520}});
        const auto [polyline3, length3] = makePolyline(
            {{37.607679, 55.747567},
             {37.607548, 55.747456},
             {37.607423, 55.747349}});

        addFeatures(polyline1, length1 + 1);
        addFeatures(polyline2, length2 / 3);
        addFeatures(polyline3, length3 / 3);
        ASSERT_EQ(features().size(), 7u);

        const geolib3::Point2 point = geoPointAt(polyline3, length3 * 2. / 3.);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        auto backwards = explorer().backward(*node);
        ASSERT_EQ(backwards.size(), 3u);

        std::sort(
            backwards.begin(),
            backwards.end(),
            [](const auto& lhs, const auto& rhs) {
                return lhs.featureId < rhs.featureId;
            });

        ASSERT_EQ(features().at(0).id(), backwards.at(0).featureId);
        ASSERT_EQ(features().at(2).id(), backwards.at(1).featureId);
        ASSERT_EQ(features().at(5).id(), backwards.at(2).featureId);

        const auto expectedRay1 = db::getRay(features().at(0));
        EXPECT_TRUE(nearBy(expectedRay1, backwards.at(0).ray));

        const auto expectedRay2 = db::getRay(features().at(2));
        EXPECT_TRUE(nearBy(expectedRay2, backwards.at(1).ray));

        const auto expectedRay3 = db::getRay(features().at(5));
        EXPECT_TRUE(nearBy(expectedRay3, backwards.at(2).ray));

        // Open question:
        // Feature with idx = 2 is on geometrical reverse edge (an edge that
        // is not reverse in the graph topology but it is on the same street
        // in opposite direction and it is also possible to move from this point
        // to the original node. So it is not yet clear how to handle this case
        // correctly. E.g. a user is able filter out uturn node but feature ID.
    };

    Y_UNIT_TEST(backward_skip_empty_edges_single_connection)
    {
        // Skip an edge in between wich doesn't have coverage starting from it
        // but there is a backward connnection within MAX_CONNECTIVITY_DISTANCE limit.
        const auto [polyline, length] = makePolyline(
            {{37.612025, 55.747263},
             {37.611674, 55.747675}});
        ASSERT_GT(length, MAX_CONNECTIVITY_DISTANCE - 1);

        addFeatures(polyline, MAX_CONNECTIVITY_DISTANCE - 1);
        ASSERT_EQ(features().size(), 2u);

        const auto node = explorer().find(features().back().geodeticPos());
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        ASSERT_EQ(backwards.size(), 1u);

        const auto expectedRay = rayAt(polyline, Meters{0});
        EXPECT_TRUE(nearBy(expectedRay, backwards.front().ray));
    }

    Y_UNIT_TEST(backward_skip_empty_edges_multiple_connections) {
        // Skip an edge in between wich doesn't have coverage starting from it
        // but there are backward connnections within MAX_CONNECTIVITY_DISTANCE limit.
        const auto [polyline1, length1] = makePolyline(
            {{37.608138, 55.747211},
             {37.607899, 55.747311}});
        const auto [polyline2, length2] = makePolyline(
            {{37.607425, 55.747200},
             {37.607602, 55.747354}});
        const auto [polyline3, length3] = makePolyline(
            {{37.607532, 55.747438},
             {37.607352, 55.747292}});

        addFeatures(polyline1, length1 + 1);
        addFeatures(polyline2, length2 + 1);
        addFeatures(polyline3, length3 + 1);
        EXPECT_EQ(features().size(), 3u);

        const auto node = explorer().find(features().back().geodeticPos());
        ASSERT_TRUE(node);

        const auto backwards = explorer().backward(*node);
        ASSERT_EQ(backwards.size(), 2u);

        const auto expectedRay1 = rayAt(polyline1);
        const auto expectedRay2 = rayAt(polyline2);
        const bool nearCase1 = nearBy(expectedRay1, backwards.front().ray) &&
                               nearBy(expectedRay2, backwards.back().ray);
        const bool nearCase2 = nearBy(expectedRay1, backwards.back().ray) &&
                               nearBy(expectedRay2, backwards.front().ray);
        EXPECT_TRUE(nearCase1 || nearCase2);
    }

    Y_UNIT_TEST(forward_absent)
    {
        // no forward connections.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        addFeatures(polyline, length - 1);

        const geolib3::Point2 point = geoPointAt(polyline, length - 1);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto forwards = explorer().forward(*node);
        EXPECT_TRUE(forwards.empty());
    }

    Y_UNIT_TEST(forward_not_connected)
    {
        // no forward adjacent connections.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        addFeatures(polyline, MAX_CONNECTIVITY_DISTANCE + 5);

        const geolib3::Point2 point =
            geoPointAt(polyline, Meters{0});
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto forwards = explorer().forward(*node);
        EXPECT_TRUE(forwards.empty());
    }

    Y_UNIT_TEST(forward_on_tip_of_same_edge)
    {
        // pick a single forward edge connection from the same edge.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        ASSERT_GT(length, FRONT_VISIBILITY_METERS * 2);
        addFeatures(
            polyline, FRONT_VISIBILITY_METERS / 2.,
            length - FRONT_VISIBILITY_METERS * 3. / 4.);

        const geolib3::Point2 point = geoPointAt(
            polyline, length - FRONT_VISIBILITY_METERS * 3. / 4.);

        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto forwards = explorer().forward(*node);
        ASSERT_EQ(forwards.size(), 1u);
        const auto expectedRay = rayAt(
            polyline, length - FRONT_VISIBILITY_METERS * 1. / 4.);
        EXPECT_TRUE(nearBy(expectedRay, forwards.front().ray));
    }

    Y_UNIT_TEST(forward_in_the_middle_of_same_edge)
    {
        // pick a single forward edge connection from the same edge.
        const auto [polyline, length] = makePolyline(
            {{37.612757, 55.746365}, {37.611988, 55.747307}});
        ASSERT_GT(length, FRONT_VISIBILITY_METERS * 3);
        addFeatures(polyline, FRONT_VISIBILITY_METERS / 2);

        const geolib3::Point2 point = geoPointAt(polyline, FRONT_VISIBILITY_METERS / 2);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto forwards = explorer().forward(*node);
        ASSERT_EQ(forwards.size(), 1u);
        const auto expectedRay = rayAt(polyline, FRONT_VISIBILITY_METERS);
        EXPECT_TRUE(nearBy(expectedRay, forwards.front().ray));
    }

    Y_UNIT_TEST(forward_from_outgoing_edge)
    {
        // pick next forward node from somewhere in the middle of outgoing
        // edge
        const auto [polyline, length] = makePolyline(
            {{37.615697, 55.742119},
             {37.61573403, 55.74205591},
             {37.615772, 55.741991}});
        addFeatures(polyline, length - 1);

        const geolib3::Point2 point = geoPointAt(polyline, length / 3);
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        const auto forward = explorer().forward(*node);
        ASSERT_EQ(forward.size(), 1u);
        EXPECT_TRUE(nearBy(polyline.pointAt(2), forward.front().ray.pos, Meters{2}));
    }

    Y_UNIT_TEST(forward_multiple_connections)
    {
        // pick multiple forward connections from multiple outgoing edges.
        const auto [polyline1, length1] = makePolyline(
            {{37.607779, 55.747362},
             {37.607670, 55.747408},
             {37.607587, 55.747440}});
        const auto [polyline2, length2] = makePolyline(
            {{37.607537, 55.747296},
             {37.607670, 55.747408},
             {37.607800, 55.747520}});
        const auto [polyline3, length3] = makePolyline(
            {{37.607679, 55.747567},
             {37.607548, 55.747456},
             {37.607423, 55.747349}});

        addFeatures(polyline1, length1 + 1);
        addFeatures(polyline2, length2 / 3);
        addFeatures(polyline3, length3 / 3);
        ASSERT_EQ(features().size(), 7u);

        const geolib3::Point2 point = geoPointAt(polyline1, Meters{0});
        const auto node = explorer().find(point);
        ASSERT_TRUE(node);

        auto forwards = explorer().forward(*node);
        ASSERT_EQ(forwards.size(), 2u);

        if (forwards.back().featureId != features().back().id()) {
            std::swap(forwards.front(), forwards.back());
        }

        ASSERT_EQ(features().at(3).id(), forwards.front().featureId);
        ASSERT_EQ(features().at(6).id(), forwards.back().featureId);

        const auto expectedRay1 = db::getRay(features().at(3));
        EXPECT_TRUE(nearBy(expectedRay1, forwards.front().ray));

        const auto expectedRay2 = db::getRay(features().at(6));
        EXPECT_TRUE(nearBy(expectedRay2, forwards.back().ray));
    };

    Y_UNIT_TEST(forward_skip_empty_edges_single_connection) {
        // Skip an edge in between wich doesn't have coverage starting from it
        // but there is a forward connnection within MAX_CONNECTIVITY_DISTANCE limit.
        const auto [polyline, length] = makePolyline(
            {{37.612025, 55.747263},
             {37.611674, 55.747675}});

        addFeatures(polyline, MAX_CONNECTIVITY_DISTANCE - 1);
        ASSERT_EQ(features().size(), 2u);

        const auto node = explorer().find(features().front().geodeticPos());
        ASSERT_TRUE(node);

        const auto forwards = explorer().forward(*node);
        ASSERT_EQ(forwards.size(), 1u);

        const auto expectedRay = rayAt(polyline, MAX_CONNECTIVITY_DISTANCE - 1);
        EXPECT_TRUE(nearBy(expectedRay, forwards.front().ray));
    }

    Y_UNIT_TEST(forward_skip_empty_edges_multiple_connections) {
        // Skip an edge in between wich doesn't have coverage starting from it
        // but there are forward connnections within MAX_CONNECTIVITY_DISTANCE limit.
        const auto [polyline1, length1] = makePolyline(
            {{37.608138, 55.747211},
             {37.607899, 55.747311}});
        const auto [polyline2, length2] = makePolyline(
            {{37.607789, 55.747505},
             {37.608060, 55.747737}});
        const auto [polyline3, length3] = makePolyline(
            {{37.607532, 55.747438},
             {37.607352, 55.747292}});

        addFeatures(polyline1, length1 + 1);
        addFeatures(polyline2, length2 + 1);
        addFeatures(polyline3, length3 + 1);
        EXPECT_EQ(features().size(), 3u);

        const auto node = explorer().find(features().front().geodeticPos());
        ASSERT_TRUE(node);

        const auto forwards = explorer().forward(*node);
        ASSERT_EQ(forwards.size(), 2u);

        const auto expectedRay1 = rayAt(polyline2);
        const auto expectedRay2 = rayAt(polyline3);
        const bool nearCase1 = nearBy(expectedRay1, forwards.front().ray) &&
                               nearBy(expectedRay2, forwards.back().ray);
        const bool nearCase2 = nearBy(expectedRay1, forwards.back().ray) &&
                               nearBy(expectedRay2, forwards.front().ray);
        EXPECT_TRUE(nearCase1 || nearCase2);
    }
} // Y_UNIT_TEST_SUITE(graph_coverage_explorer_tests)
} // namespace maps::mrc::browser::tests
