/** @file

  Unit tests for LogAccess.

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#include <catch2/catch_test_macros.hpp>

#include "proxy/NonHttpSmLogData.h"
#include "proxy/logging/LogAccess.h"
#include "proxy/logging/TransactionLogData.h"
#include "tscore/ink_align.h"
#include "tscore/ink_inet.h"

#include <string_view>
#include <vector>

using namespace std::literals;

extern int cmd_disable_pfreelist;

namespace
{
void
initialize_headers_once()
{
  static bool initialized = false;

  if (!initialized) {
    cmd_disable_pfreelist = true;
    url_init();
    mime_init();
    http_init();
    initialized = true;
  }
}

void
add_header_field(HTTPHdr &hdr, std::string_view name, std::string_view value)
{
  MIMEField *field = hdr.field_create(name);
  REQUIRE(field != nullptr);
  field->value_set(hdr.m_heap, hdr.m_mime, value);
  hdr.field_attach(field);
}

std::string
synthesize_target(std::string_view method, std::string_view scheme, std::string_view authority, std::string_view path)
{
  if (method == static_cast<std::string_view>(HTTP_METHOD_CONNECT)) {
    if (!authority.empty()) {
      return std::string(authority);
    }
    if (!path.empty()) {
      return std::string(path);
    }
    return {};
  }

  if (!scheme.empty() && !authority.empty()) {
    std::string url;
    url.reserve(scheme.size() + authority.size() + path.size() + 4);
    url.append(scheme);
    url.append("://");
    url.append(authority);
    if (!path.empty()) {
      url.append(path);
    } else {
      url.push_back('/');
    }
    return url;
  }

  if (!path.empty()) {
    return std::string(path);
  }

  return authority.empty() ? std::string{} : std::string(authority);
}

void
set_socket_address(IpEndpoint &ep, std::string_view text)
{
  REQUIRE(0 == ats_ip_pton(text, &ep.sa));
}

void
populate_non_http_sm_data(NonHttpSmLogData &data, std::string_view method, std::string_view scheme, std::string_view authority,
                          std::string_view path)
{
  initialize_headers_once();

  auto *heap = new_HdrHeap(HdrHeap::DEFAULT_SIZE + 64);
  data.owned_client_request.create(HTTPType::REQUEST, HTTP_2_0, heap);
  data.owned_client_request.method_set(method);
  data.m_client_connection_is_ssl = true;
  data.m_log_code                 = SquidLogCode::ERR_INVALID_REQ;
  data.m_hit_miss_code            = SQUID_MISS_NONE;
  data.m_hier_code                = SquidHierarchyCode::NONE;
  data.m_server_transact_count    = 0;
  data.owned_client_protocol_str  = "http/2";
  data.owned_method.assign(method.data(), method.size());
  data.owned_scheme.assign(scheme.data(), scheme.size());
  data.owned_authority.assign(authority.data(), authority.size());
  data.owned_path.assign(path.data(), path.size());
  data.owned_url = synthesize_target(method, scheme, authority, path);
  set_socket_address(data.owned_client_addr, "192.0.2.10:4321"sv);
  ats_ip_copy(&data.owned_client_src_addr.sa, &data.owned_client_addr.sa);
  data.m_client_port = ats_ip_port_host_order(&data.owned_client_addr.sa);

  add_header_field(data.owned_client_request, PSEUDO_HEADER_METHOD, method);
  if (!scheme.empty()) {
    add_header_field(data.owned_client_request, PSEUDO_HEADER_SCHEME, scheme);
  }
  if (!authority.empty()) {
    add_header_field(data.owned_client_request, PSEUDO_HEADER_AUTHORITY, authority);
  }
  if (!path.empty()) {
    add_header_field(data.owned_client_request, PSEUDO_HEADER_PATH, path);
  }
  add_header_field(data.owned_client_request, static_cast<std::string_view>(MIME_FIELD_USER_AGENT), "TikTok/1.0");

  data.owned_milestones[TS_MILESTONE_SM_START]            = ink_hrtime_from_msec(10);
  data.owned_milestones[TS_MILESTONE_UA_BEGIN]            = ink_hrtime_from_msec(10);
  data.owned_milestones[TS_MILESTONE_UA_FIRST_READ]       = ink_hrtime_from_msec(12);
  data.owned_milestones[TS_MILESTONE_UA_READ_HEADER_DONE] = ink_hrtime_from_msec(14);
  data.owned_milestones[TS_MILESTONE_SM_FINISH]           = ink_hrtime_from_msec(15);
}

template <typename Marshal>
std::string
marshal_string(Marshal marshal)
{
  const int         len = marshal(nullptr);
  std::vector<char> buffer(len);
  marshal(buffer.data());
  return std::string(buffer.data());
}

template <typename Marshal>
int64_t
marshal_int_value(Marshal marshal)
{
  std::vector<char> buffer(INK_MIN_ALIGN * 2);
  marshal(buffer.data());
  char *ptr = buffer.data();
  return LogAccess::unmarshal_int(&ptr);
}
} // namespace

TEST_CASE("LogAccess non-HttpSM CONNECT fields", "[LogAccess]")
{
  NonHttpSmLogData data;
  populate_non_http_sm_data(data, "CONNECT", ""sv, "example.com:443", ""sv);
  TransactionLogData log_data(data);
  LogAccess          access(log_data);

  access.init();

  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_http_method(buf); }) == "CONNECT");
  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_protocol_version(buf); }) == "http/2");
  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_url(buf); }) == "example.com:443");
  CHECK(marshal_int_value([&](char *buf) { return access.marshal_cache_result_code(buf); }) ==
        static_cast<int64_t>(SquidLogCode::ERR_INVALID_REQ));
  CHECK(marshal_int_value([&](char *buf) { return access.marshal_server_transact_count(buf); }) == 0);

  char user_agent[] = "User-Agent";
  CHECK(marshal_string([&](char *buf) { return access.marshal_http_header_field(LogField::CQH, user_agent, buf); }) ==
        "TikTok/1.0");
}

TEST_CASE("LogAccess malformed CONNECT without authority falls back to path", "[LogAccess]")
{
  NonHttpSmLogData data;
  populate_non_http_sm_data(data, "CONNECT", "https"sv, ""sv, "/"sv);
  TransactionLogData log_data(data);
  LogAccess          access(log_data);

  access.init();

  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_url(buf); }) == "/");
  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_url_canon(buf); }) == "/");
  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_url_path(buf); }) == "/");
  CHECK(marshal_string([&](char *buf) { return access.marshal_client_req_url_scheme(buf); }) == "https");
  CHECK(marshal_int_value([&](char *buf) { return access.marshal_transfer_time_ms(buf); }) == 5);
}

TEST_CASE("LogAccess non-HttpSM client host port is null-safe", "[LogAccess]")
{
  NonHttpSmLogData data;
  populate_non_http_sm_data(data, "GET", "https"sv, "example.com", "/client-port"sv);
  TransactionLogData log_data(data);
  LogAccess          access(log_data);

  access.init();

  CHECK(access.marshal_client_host_port(nullptr) == INK_MIN_ALIGN);
  CHECK(marshal_int_value([&](char *buf) { return access.marshal_client_host_port(buf); }) == 4321);
}

TEST_CASE("LogAccess unmarshal_http_version keeps the minor version", "[LogAccess]")
{
  auto render = [](int64_t major, int64_t minor) -> std::string {
    char marshalled[2 * INK_MIN_ALIGN];
    LogAccess::marshal_int(marshalled, major);
    LogAccess::marshal_int(marshalled + INK_MIN_ALIGN, minor);

    char  dest[64] = {};
    char *src      = marshalled;
    int   len      = LogAccess::unmarshal_http_version(&src, dest, sizeof(dest));
    REQUIRE(len > 0);
    return std::string(dest, len);
  };

  CHECK(render(1, 1) == "HTTP/1.1");
  CHECK(render(1, 0) == "HTTP/1.0");
  // known bug of `.0` suffix for HTTP/2 and HTTP/3
  CHECK(render(2, 0) == "HTTP/2.0");
  CHECK(render(3, 0) == "HTTP/3.0");
}
