#include <doctest/doctest.h>

#include <cstring>
#include <string>

#include "api_url_parser.hpp"

// Helper to convert string to mutable buffer for testing
std::string make_mutable_copy(const std::string &url) {
  return url; // Return copy that can be modified
}

TEST_CASE("ApiUrlParser routing") {
  SUBCASE("Static GET routes") {
    auto url = make_mutable_copy("/v1/version");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetVersion);

    url = make_mutable_copy("/v1/subscribe");
    result = ApiUrlParser::parse("GET", url.data(),
                                 static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetSubscribe);

    url = make_mutable_copy("/metrics");
    result = ApiUrlParser::parse("GET", url.data(),
                                 static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetMetrics);

    url = make_mutable_copy("/ok");
    result = ApiUrlParser::parse("GET", url.data(),
                                 static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetOk);
  }

  SUBCASE("Static POST routes") {
    auto url = make_mutable_copy("/v1/commit");
    RouteMatch match;
    auto result = ApiUrlParser::parse("POST", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::PostCommit);
  }

  SUBCASE("Not found") {
    auto url = make_mutable_copy("/unknown/route");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::NotFound);

    url = make_mutable_copy("/v1/version");
    result = ApiUrlParser::parse("DELETE", url.data(),
                                 static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::NotFound);
  }
}

TEST_CASE("ApiUrlParser with query strings") {
  SUBCASE("Simple query string") {
    auto url = make_mutable_copy("/v1/status?request_id=123");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "123");
  }

  SUBCASE("Multiple query parameters") {
    auto url = make_mutable_copy("/v1/status?request_id=abc&min_version=42");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "abc");
    REQUIRE(match.params[static_cast<int>(ApiParameterKey::MinVersion)]
                .has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::MinVersion)].value() ==
          "42");
  }

  SUBCASE("Unknown parameters are ignored") {
    auto url = make_mutable_copy("/v1/version?foo=bar&baz=quux");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetVersion);
    CHECK_FALSE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
  }
}

TEST_CASE("ApiUrlParser with URL parameters") {
  SUBCASE("PUT retention policy") {
    auto url = make_mutable_copy("/v1/retention/my-policy");
    RouteMatch match;
    auto result = ApiUrlParser::parse("PUT", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::PutRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          "my-policy");
  }

  SUBCASE("DELETE retention policy") {
    auto url = make_mutable_copy("/v1/retention/another-policy");
    RouteMatch match;
    auto result = ApiUrlParser::parse("DELETE", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::DeleteRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          "another-policy");
  }

  SUBCASE("GET retention policy") {
    auto url = make_mutable_copy("/v1/retention/get-this");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          "get-this");
  }

  SUBCASE("GET all retention policies (no ID)") {
    auto url = make_mutable_copy("/v1/retention");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);
    CHECK_FALSE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
  }
}

TEST_CASE("ApiUrlParser with URL and query parameters") {
  auto url = make_mutable_copy("/v1/retention/p1?request_id=abc123");
  RouteMatch match;
  auto result = ApiUrlParser::parse("DELETE", url.data(),
                                    static_cast<int>(url.size()), match);
  CHECK(result == ParseResult::Success);
  CHECK(match.route == HttpRoute::DeleteRetention);
  REQUIRE(
      match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
  CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
        "p1");
  REQUIRE(
      match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
  CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
        "abc123");
}

TEST_CASE("ApiUrlParser URL decoding") {
  SUBCASE("Path segment percent-decoding") {
    auto url = make_mutable_copy("/v1/retention/my%2Dpolicy");
    RouteMatch match;
    auto result = ApiUrlParser::parse("PUT", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::PutRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          "my-policy");
  }

  SUBCASE("Query parameter form decoding (+ to space)") {
    auto url = make_mutable_copy("/v1/status?request_id=hello+world");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "hello world");
  }

  SUBCASE("Query parameter percent-decoding") {
    auto url = make_mutable_copy("/v1/status?request_id=hello%20world");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "hello world");
  }

  SUBCASE("Base64-like sequences in query parameters") {
    auto url = make_mutable_copy("/v1/status?request_id=YWJj%3D");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "YWJj=");
  }

  SUBCASE("Mixed encoding in path and query") {
    auto url = make_mutable_copy(
        "/v1/retention/my%2Dpolicy?request_id=hello+world%21");
    RouteMatch match;
    auto result = ApiUrlParser::parse("DELETE", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::DeleteRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          "my-policy");
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "hello world!");
  }
}

TEST_CASE("ApiUrlParser malformed encoding") {
  SUBCASE("Incomplete percent sequence in path") {
    auto url = make_mutable_copy("/v1/retention/bad%2");
    RouteMatch match;
    auto result = ApiUrlParser::parse("PUT", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::MalformedEncoding);
  }

  SUBCASE("Invalid hex digits in path") {
    auto url = make_mutable_copy("/v1/retention/bad%ZZ");
    RouteMatch match;
    auto result = ApiUrlParser::parse("PUT", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::MalformedEncoding);
  }

  SUBCASE("Incomplete percent sequence in query") {
    auto url = make_mutable_copy("/v1/status?request_id=bad%2");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::MalformedEncoding);
  }

  SUBCASE("Invalid hex digits in query") {
    auto url = make_mutable_copy("/v1/status?request_id=bad%GG");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::MalformedEncoding);
  }

  SUBCASE("Percent at end of path") {
    auto url = make_mutable_copy("/v1/retention/bad%");
    RouteMatch match;
    auto result = ApiUrlParser::parse("PUT", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::MalformedEncoding);
  }

  SUBCASE("Percent at end of query") {
    auto url = make_mutable_copy("/v1/status?request_id=bad%");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::MalformedEncoding);
  }
}

TEST_CASE("ApiUrlParser edge cases and bugs") {
  SUBCASE("Bug: Path boundary error - /v1/retention/ with trailing slash") {
    // BUG: Code checks length > 13 but substrings at 14, causing off-by-one
    auto url = make_mutable_copy(
        "/v1/retention/"); // length 14, exactly the boundary case
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);
    // This should NOT set PolicyId since it's empty, but current code might
    // have issues
    CHECK_FALSE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
  }

  SUBCASE("Bug: Empty URL handling") {
    auto url = make_mutable_copy("");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::NotFound);
  }

  SUBCASE("Bug: Query-only URL") {
    auto url = make_mutable_copy("?request_id=123");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::NotFound);
    // Should still parse query parameters
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "123");
  }

  SUBCASE("Bug: Consecutive delimiters in query string") {
    auto url =
        make_mutable_copy("/v1/status?&&request_id=123&&min_version=42&&");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "123");
    REQUIRE(match.params[static_cast<int>(ApiParameterKey::MinVersion)]
                .has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::MinVersion)].value() ==
          "42");
  }

  SUBCASE("Bug: Parameter without value (should be skipped)") {
    auto url = make_mutable_copy("/v1/status?debug&request_id=123");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    // debug parameter should be ignored since it has no value
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "123");
  }

  SUBCASE("Bug: Empty parameter value") {
    auto url = make_mutable_copy("/v1/status?request_id=");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "");
  }

  SUBCASE("Edge: Exact length boundary for retention path") {
    // Test the exact boundary condition: "/v1/retention" is 13 chars
    auto url = make_mutable_copy("/v1/retention"); // exactly 13 characters
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);
    CHECK_FALSE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
  }

  SUBCASE("Edge: Minimum valid policy ID") {
    // Test one character after the boundary
    auto url =
        make_mutable_copy("/v1/retention/a"); // 15 chars total, policy_id = "a"
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          "a");
  }
}

TEST_CASE("ApiUrlParser specific bug reproduction") {
  SUBCASE("Reproduction: Path boundary math error") {
    // The bug: code checks length > 13 but substrings at 14
    // "/v1/retention" = 13 chars, "/v1/retention/" = 14 chars
    // This test should demonstrate undefined behavior or wrong results

    auto url = make_mutable_copy("/v1/retention/");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);

    // Expected behavior: should match GetRetention but NOT set PolicyId
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);

    // The bug: may incorrectly extract empty string or cause buffer read error
    // Let's see what actually happens
    if (match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value()) {
      auto policy_id =
          match.params[static_cast<int>(ApiParameterKey::PolicyId)].value();
      CHECK(policy_id.empty()); // Should be empty if set at all
    }
  }

  SUBCASE("Reproduction: Query parsing with edge cases") {
    // Test parameter parsing with multiple edge conditions
    auto url = make_mutable_copy(
        "/v1/status?=empty_key&no_value&request_id=&min_version=42&=");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);

    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);

    // Should handle empty values correctly
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "");

    REQUIRE(match.params[static_cast<int>(ApiParameterKey::MinVersion)]
                .has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::MinVersion)].value() ==
          "42");
  }

  SUBCASE("Reproduction: Very long input stress test") {
    // Test potential integer overflow or performance issues
    std::string long_policy_id(1000, 'x'); // 1000 character policy ID
    auto url = make_mutable_copy("/v1/retention/" + long_policy_id +
                                 "?request_id=" + std::string(500, 'y'));
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);

    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
          long_policy_id);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          std::string(500, 'y'));
  }

  SUBCASE("Reproduction: Zero-length edge case") {
    char empty_buffer[1] = {0};
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", empty_buffer, 0, match);

    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::NotFound);
  }

  SUBCASE("Reproduction: Null buffer edge case") {
    // This might cause undefined behavior if not handled properly
    RouteMatch match;
    char single_char = '/';
    auto result = ApiUrlParser::parse("GET", &single_char, 1, match);

    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::NotFound);
  }

  SUBCASE("BUG: Query parser pos increment overflow") {
    // BUG: pos += pair_end + 1 can go beyond buffer bounds
    // When pair_end == query_length - pos (no & found), pos becomes
    // query_length + 1
    auto url = make_mutable_copy("/v1/status?no_ampersand_at_end");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);

    // This should not crash or have undefined behavior
    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetStatus);
    // Parameter should be ignored since it's not a known key
  }

  SUBCASE("BUG: String view with potentially negative length cast") {
    // BUG: decoded_value_length is int but gets cast to size_t for string_view
    // If decode function returned negative (which it can), this could wrap
    // around

    // We can't easily trigger the decode function to return -1 through normal
    // parsing since that's caught earlier, but this tests the edge case
    // handling
    auto url = make_mutable_copy("/v1/status?request_id=normal_value");
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);

    CHECK(result == ParseResult::Success);
    REQUIRE(
        match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
    CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
          "normal_value");
  }

  SUBCASE("BUG: Array bounds - PolicyId path extraction edge case") {
    // Test the boundary condition more precisely
    // "/v1/retention" = 13 chars, checking length > 13, substr(14)
    auto url = make_mutable_copy("/v1/retention/"); // exactly 14 chars
    RouteMatch match;
    auto result = ApiUrlParser::parse("GET", url.data(),
                                      static_cast<int>(url.size()), match);

    CHECK(result == ParseResult::Success);
    CHECK(match.route == HttpRoute::GetRetention);

    // path.length() = 14, so > 13 is true
    // path.substr(14) should return empty string_view
    // The bug would be if this crashes or returns invalid data
    if (match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value()) {
      CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)]
                .value()
                .empty());
    }
  }
}
