/**
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This file is part of osm2pgsql (https://osm2pgsql.org/).
 *
 * Copyright (C) 2006-2026 by the osm2pgsql developer community.
 * For a full list of authors see the git log.
 */

#include <catch.hpp>

#include "common-buffer.hpp"

#include "geom-from-osm.hpp"
#include "geom-functions.hpp"
#include "geom-output.hpp"
#include "geom.hpp"

#include <array>

TEST_CASE("geom::linestring_t", "[NoDB]")
{
    geom::linestring_t ls1;

    REQUIRE(ls1.empty());
    ls1.emplace_back(17, 42);
    ls1.emplace_back(-3, 22);
    REQUIRE(ls1.size() == 2);

    auto it = ls1.cbegin();
    REQUIRE(it != ls1.cend());
    REQUIRE(it->x() == 17);
    ++it;
    REQUIRE(it != ls1.cend());
    REQUIRE(it->y() == 22);
    ++it;
    REQUIRE(it == ls1.cend());

    REQUIRE(ls1.num_geometries() == 1);
}

TEST_CASE("remove duplicate points in linestring", "[NoDB]")
{
    geom::linestring_t ls{{1, 1}, {1, 2}, {1, 2}, {2, 2}};
    REQUIRE(ls.size() == 4);
    ls.remove_duplicates();
    REQUIRE(ls.size() == 3);
    REQUIRE(ls == geom::linestring_t{{1, 1}, {1, 2}, {2, 2}});
}

TEST_CASE("line geometry", "[NoDB]")
{
    geom::geometry_t const geom{geom::linestring_t{{1, 1}, {2, 2}}};

    REQUIRE(geom.n_points() == 2);
    REQUIRE(dimension(geom) == 1);
    REQUIRE(num_geometries(geom) == 1);
    REQUIRE(area(geom) == Approx(0.0));
    REQUIRE(spherical_area(geom) == Approx(0.0));
    REQUIRE(length(geom) == Approx(1.41421));
    REQUIRE(spherical_length(geom) == Approx(156876.14940188668).epsilon(0.0000001));
    REQUIRE(geometry_type(geom) == "LINESTRING");
    REQUIRE(centroid(geom) == geom::geometry_t{geom::point_t{1.5, 1.5}});
    REQUIRE(geometry_n(geom, 1) == geom);
}

TEST_CASE("reverse line geometry", "[NoDB]")
{
    geom::geometry_t const geom{geom::linestring_t{{1, 1}, {2, 2}}};

    auto reversed = geom::reverse(geom);
    REQUIRE(num_geometries(reversed) == 1);
    REQUIRE(geometry_type(reversed) == "LINESTRING");

    auto const &line = reversed.get<geom::linestring_t>();
    REQUIRE(line.size() == 2);
    REQUIRE(line == geom::linestring_t{{2, 2}, {1, 1}});
}

TEST_CASE("create_linestring from OSM data", "[NoDB]")
{
    test_buffer_t buffer;
    buffer.add_way("w20 Nn1x1y1,n2x2y2");

    auto const geom =
        geom::create_linestring(buffer.buffer().get<osmium::Way>(0));

    REQUIRE(geom.is_linestring());
    REQUIRE(geometry_type(geom) == "LINESTRING");
    REQUIRE(dimension(geom) == 1);
    REQUIRE(num_geometries(geom) == 1);
    REQUIRE(area(geom) == Approx(0.0));
    REQUIRE(spherical_area(geom) == Approx(0.0));
    REQUIRE(length(geom) == Approx(1.41421));
    REQUIRE(spherical_length(geom) == Approx(156876.14940188668).epsilon(0.0000001));
    REQUIRE(geom.get<geom::linestring_t>() ==
            geom::linestring_t{{1, 1}, {2, 2}});
    REQUIRE(centroid(geom) == geom::geometry_t{geom::point_t{1.5, 1.5}});
}

TEST_CASE("create_linestring from OSM data without locations", "[NoDB]")
{
    test_buffer_t buffer;
    buffer.add_way("w20 Nn1,n2");

    auto const geom =
        geom::create_linestring(buffer.buffer().get<osmium::Way>(0));

    REQUIRE(geom.is_null());
}

TEST_CASE("create_linestring from invalid OSM data", "[NoDB]")
{
    test_buffer_t buffer;
    buffer.add_way("w20 Nn1x1y1");

    auto const geom =
        geom::create_linestring(buffer.buffer().get<osmium::Way>(0));

    REQUIRE(geom.is_null());
}

TEST_CASE("create_point from OSM way data", "[NoDB]")
{
    test_buffer_t buffer;
    buffer.add_way("w20 Nn1x1y1,n2x2y2");

    auto const &nodes = buffer.buffer().get<osmium::Way>(0).nodes();
    geom::geometry_t geom;
    geom::create_point(&geom, nodes[0].location());

    REQUIRE(geom.is_point());
    REQUIRE(geometry_type(geom) == "POINT");
    REQUIRE(dimension(geom) == 0);
    REQUIRE(num_geometries(geom) == 1);
    REQUIRE(geom.get<geom::point_t>() == geom::point_t{1, 1});
}

TEST_CASE("create_point from OSM data without locations", "[NoDB]")
{
    test_buffer_t buffer;
    buffer.add_way("w20 Nn1,n2");

    auto const &nodes = buffer.buffer().get<osmium::Way>(0).nodes();
    geom::geometry_t geom;
    geom::create_point(&geom, nodes[0].location());

    REQUIRE(geom.is_null());
}

TEST_CASE("create_point from way with single node", "[NoDB]")
{
    test_buffer_t buffer;
    buffer.add_way("w20 Nn1x1y1");

    auto const &nodes = buffer.buffer().get<osmium::Way>(0).nodes();
    geom::geometry_t geom;
    geom::create_point(&geom, nodes[0].location());

    REQUIRE(geom.is_point());
    REQUIRE(geometry_type(geom) == "POINT");
    REQUIRE(dimension(geom) == 0);
    REQUIRE(num_geometries(geom) == 1);
    REQUIRE(geom.get<geom::point_t>() == geom::point_t{1, 1});
}

TEST_CASE("geom::segmentize w/o split", "[NoDB]")
{
    geom::linestring_t const expected{{0, 0}, {1, 2}, {2, 2}};
    geom::linestring_t line = expected;

    auto const geom = geom::segmentize(geom::geometry_t{std::move(line)}, 10.0);

    REQUIRE(geom.is_multilinestring());
    REQUIRE(num_geometries(geom) == 1);
    auto const &ml = geom.get<geom::multilinestring_t>();
    REQUIRE(ml.num_geometries() == 1);
    REQUIRE(ml[0] == expected);
}

TEST_CASE("geom::segmentize with split 0.5", "[NoDB]")
{
    geom::linestring_t line{{0, 0}, {1, 0}};

    std::array<geom::linestring_t, 2> const expected{
        geom::linestring_t{{0, 0}, {0.5, 0}},
        geom::linestring_t{{0.5, 0}, {1, 0}}};

    auto const geom = geom::segmentize(geom::geometry_t{std::move(line)}, 0.5);

    REQUIRE(geom.is_multilinestring());
    auto const &ml = geom.get<geom::multilinestring_t>();
    REQUIRE(ml.num_geometries() == 2);
    REQUIRE(ml[0] == expected[0]);
    REQUIRE(ml[1] == expected[1]);
}

TEST_CASE("geom::segmentize with split 0.4", "[NoDB]")
{
    geom::linestring_t line{{0, 0}, {1, 0}};

    std::array<geom::linestring_t, 3> const expected{
        geom::linestring_t{{0, 0}, {0.4, 0}},
        geom::linestring_t{{0.4, 0}, {0.8, 0}},
        geom::linestring_t{{0.8, 0}, {1, 0}}};

    auto const geom = geom::segmentize(geom::geometry_t{std::move(line)}, 0.4);

    REQUIRE(geom.is_multilinestring());
    auto const &ml = geom.get<geom::multilinestring_t>();
    REQUIRE(ml.num_geometries() == 3);
    REQUIRE(ml[0] == expected[0]);
    REQUIRE(ml[1] == expected[1]);
    REQUIRE(ml[2] == expected[2]);
}

TEST_CASE("geom::segmentize with split 1.0 at start", "[NoDB]")
{
    geom::linestring_t line{{0, 0}, {2, 0}, {3, 0}, {4, 0}};

    std::array<geom::linestring_t, 4> const expected{
        geom::linestring_t{{0, 0}, {1, 0}}, geom::linestring_t{{1, 0}, {2, 0}},
        geom::linestring_t{{2, 0}, {3, 0}}, geom::linestring_t{{3, 0}, {4, 0}}};

    auto const geom = geom::segmentize(geom::geometry_t{std::move(line)}, 1.0);

    REQUIRE(geom.is_multilinestring());
    auto const &ml = geom.get<geom::multilinestring_t>();
    REQUIRE(ml.num_geometries() == 4);
    REQUIRE(ml[0] == expected[0]);
    REQUIRE(ml[1] == expected[1]);
    REQUIRE(ml[2] == expected[2]);
    REQUIRE(ml[3] == expected[3]);
}

TEST_CASE("geom::segmentize with split 1.0 in middle", "[NoDB]")
{
    geom::linestring_t line{{0, 0}, {1, 0}, {3, 0}, {4, 0}};

    std::array<geom::linestring_t, 4> const expected{
        geom::linestring_t{{0, 0}, {1, 0}}, geom::linestring_t{{1, 0}, {2, 0}},
        geom::linestring_t{{2, 0}, {3, 0}}, geom::linestring_t{{3, 0}, {4, 0}}};

    auto const geom = geom::segmentize(geom::geometry_t{std::move(line)}, 1.0);

    REQUIRE(geom.is_multilinestring());
    auto const &ml = geom.get<geom::multilinestring_t>();
    REQUIRE(ml.num_geometries() == 4);
    REQUIRE(ml[0] == expected[0]);
    REQUIRE(ml[1] == expected[1]);
    REQUIRE(ml[2] == expected[2]);
    REQUIRE(ml[3] == expected[3]);
}

TEST_CASE("geom::segmentize with split 1.0 at end", "[NoDB]")
{
    geom::linestring_t line{{0, 0}, {1, 0}, {2, 0}, {4, 0}};

    std::array<geom::linestring_t, 4> const expected{
        geom::linestring_t{{0, 0}, {1, 0}}, geom::linestring_t{{1, 0}, {2, 0}},
        geom::linestring_t{{2, 0}, {3, 0}}, geom::linestring_t{{3, 0}, {4, 0}}};

    auto const geom = geom::segmentize(geom::geometry_t{std::move(line)}, 1.0);

    REQUIRE(geom.is_multilinestring());
    auto const &ml = geom.get<geom::multilinestring_t>();
    REQUIRE(ml.num_geometries() == 4);
    REQUIRE(ml[0] == expected[0]);
    REQUIRE(ml[1] == expected[1]);
    REQUIRE(ml[2] == expected[2]);
    REQUIRE(ml[3] == expected[3]);
}

TEST_CASE("geom::simplify", "[NoDB]")
{
    geom::geometry_t const input{
        geom::linestring_t{{0, 0}, {1, 1}, {2, 0}, {3, 1}, {4, 0}, {5, 1}}};

    SECTION("small tolerance leaves linestring as is")
    {
        auto const geom = geom::simplify(input, 0.5);

        REQUIRE(geom.is_linestring());
        auto const &l = geom.get<geom::linestring_t>();
        REQUIRE(l.size() == 6);
        REQUIRE(l == input.get<geom::linestring_t>());
    }

    SECTION("large tolerance simplifies linestring")
    {
        auto const geom = geom::simplify(input, 10.0);

        REQUIRE(geom.is_linestring());
        auto const &l = geom.get<geom::linestring_t>();
        REQUIRE(l.size() == 2);
        REQUIRE(l[0] == input.get<geom::linestring_t>()[0]);
        REQUIRE(l[1] == input.get<geom::linestring_t>()[5]);
    }
}

TEST_CASE("geom::simplify of a loop", "[NoDB]")
{
    geom::geometry_t const input{
        geom::linestring_t{{0, 0}, {0, 1}, {1, 1}, {1, 0}, {0.1, 0.1}, {0, 0}}};

    SECTION("small tolerance leaves linestring as is")
    {
        auto const geom = geom::simplify(input, 0.01);

        REQUIRE(geom.is_linestring());
        auto const &l = geom.get<geom::linestring_t>();
        REQUIRE(l.size() == 6);
        REQUIRE(l == input.get<geom::linestring_t>());
    }

    SECTION("medium tolerance simplifies linestring")
    {
        auto const geom = geom::simplify(input, 0.5);

        REQUIRE(geom.is_linestring());
        auto const &l = geom.get<geom::linestring_t>();
        REQUIRE(l.size() == 5);

        auto const &il = input.get<geom::linestring_t>();
        REQUIRE(l[0] == il[0]);
        REQUIRE(l[1] == il[1]);
        REQUIRE(l[2] == il[2]);
        REQUIRE(l[3] == il[3]);
        REQUIRE(l[4] == il[5]);
    }

    SECTION("large tolerance breaks linestring, null geometry is returned")
    {
        auto const geom = geom::simplify(input, 10.0);

        REQUIRE(geom.is_null());
    }
}

TEST_CASE("geom::simplify of straight line", "[NoDB]")
{
    geom::geometry_t const input{geom::linestring_t{{1, 1}, {1, 2}, {1, 3}}};

    SECTION("small tolerance simplifies linestring")
    {
        auto const geom = geom::simplify(input, 0.5);

        REQUIRE(geom.is_linestring());
        auto const &l = geom.get<geom::linestring_t>();
        REQUIRE(l.size() == 2);
        REQUIRE(l[0] == input.get<geom::linestring_t>()[0]);
        REQUIRE(l[1] == input.get<geom::linestring_t>()[2]);
    }

    SECTION("large tolerance also simplifies linestring")
    {
        auto const geom = geom::simplify(input, 10.0);

        REQUIRE(geom.is_linestring());
        auto const &l = geom.get<geom::linestring_t>();
        REQUIRE(l.size() == 2);
        REQUIRE(l[0] == input.get<geom::linestring_t>()[0]);
        REQUIRE(l[1] == input.get<geom::linestring_t>()[2]);
    }
}

TEST_CASE("long line length - equator", "[NoDB]")
{
    geom::geometry_t const geom{geom::linestring_t{{0, 0}, {180, 0}}};
    REQUIRE(length(geom) == Approx(180.0));
    REQUIRE(spherical_length(geom) == Approx(20003931.458625447).epsilon(0.0000001));
}

TEST_CASE("long line length - to pole", "[NoDB]")
{
    geom::geometry_t const geom{geom::linestring_t{{0, -90}, {0, 90}}};
    REQUIRE(length(geom) == Approx(180.0));
    REQUIRE(spherical_length(geom) == Approx(20003931.458625447).epsilon(0.0000001));
}

TEST_CASE("line length - more points", "[NoDB]")
{
    geom::geometry_t const geom{geom::linestring_t{{20, 19.8}, {20.1, 19.8}, {20.2, 19.9}}};
    REQUIRE(length(geom) == Approx(0.2414213562373079));
    REQUIRE(spherical_length(geom) == Approx(25718.175297824535).epsilon(0.0000001));
}
