/** @file

  Unit Tests for HttpTransact

  @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 <string_view>
#include <vector>
#include <cstdio>

using namespace std::string_view_literals;

#include "tscore/Diags.h"
#include "tsutil/PostScript.h"

#include "proxy/http/HttpConfig.h"
#include "proxy/http/HttpTransact.h"
#include "proxy/http/remap/RemapProcessor.h"
#include "records/RecordsConfig.h"

#include <catch2/catch_test_macros.hpp>

TEST_CASE("HttpTransact", "[http]")
{
  url_init();
  mime_init();
  http_init();

  SECTION("RemapProcessor tolerates a missing remap table")
  {
    HttpTransact::State state;
    RemapProcessor      processor;

    CHECK_FALSE(processor.setup_for_remap(&state, nullptr));
    CHECK_FALSE(processor.finish_remap(&state, nullptr));
  }

  SECTION("HttpTransact::merge_response_header_with_cached_header")
  {
    SECTION("Basic")
    {
      HTTPHdr        hdr1;
      HTTPHdr        hdr2;
      ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
      ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header input1[] = {
        {"AAA", "111"},
        {"BBB", "222"},
        {"CCC", "333"},
      };
      struct header input2[] = {
        {"DDD", "444"},
        {"EEE", "555"},
        {"FFF", "666"}
      };

      hdr1.create(HTTPType::RESPONSE);
      for (auto &&entry : input1) {
        field = hdr1.field_create(entry.name);
        hdr1.field_attach(field);
        hdr1.field_value_set(field, entry.value.data(), entry.value.length());
      }

      hdr2.create(HTTPType::RESPONSE);
      for (auto &&entry : input2) {
        field = hdr2.field_create(entry.name);
        hdr2.field_attach(field);
        hdr2.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);

      CHECK(hdr1.fields_count() == 6);

      field = hdr1.field_find("AAA"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "111"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("BBB"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "222"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("CCC"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "333"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("DDD"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("EEE"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "555"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("FFF"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "666"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Have comon headers")
    {
      HTTPHdr        hdr1;
      HTTPHdr        hdr2;
      ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
      ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header input1[] = {
        {"AAA", "111"},
        {"BBB", "222"},
        {"CCC", "333"},
      };
      struct header input2[] = {
        {"DDD", "444"},
        {"BBB", "555"},
        {"FFF", "666"}
      };

      hdr1.create(HTTPType::RESPONSE);
      for (auto &&entry : input1) {
        field = hdr1.field_create(entry.name);
        hdr1.field_attach(field);
        hdr1.field_value_set(field, entry.value.data(), entry.value.length());
      }

      hdr2.create(HTTPType::RESPONSE);
      for (auto &&entry : input2) {
        field = hdr2.field_create(entry.name);
        hdr2.field_attach(field);
        hdr2.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);

      CHECK(hdr1.fields_count() == 5);

      field = hdr1.field_find("AAA"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "111"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("BBB"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "555"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("CCC"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "333"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("DDD"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("FFF"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "666"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Have dup headers")
    {
      HTTPHdr        hdr1;
      HTTPHdr        hdr2;
      ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
      ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header input1[] = {
        {"AAA", "111"},
        {"BBB", "222"},
        {"CCC", "333"},
      };
      struct header input2[] = {
        {"DDD", "444"},
        {"EEE", "555"},
        {"EEE", "666"}
      };

      hdr1.create(HTTPType::RESPONSE);
      for (auto &&entry : input1) {
        field = hdr1.field_create(entry.name);
        hdr1.field_attach(field);
        hdr1.field_value_set(field, entry.value.data(), entry.value.length());
      }

      hdr2.create(HTTPType::RESPONSE);
      for (auto &&entry : input2) {
        field = hdr2.field_create(entry.name);
        hdr2.field_attach(field);
        hdr2.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);

      CHECK(hdr1.fields_count() == 6);

      field = hdr1.field_find("AAA"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "111"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("BBB"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "222"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("CCC"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "333"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("DDD"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("EEE"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "555"sv);
      CHECK(field->has_dups() == true);
    }

    SECTION("Connection-named header from 304 is not merged")
    {
      HTTPHdr        cached_headers;
      HTTPHdr        response_headers;
      ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
      ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header cached[] = {
        {"AAA", "111"},
        {"BBB", "222"},
      };
      struct header response[] = {
        {"Connection", "X-Evil"  },
        {"X-Evil",     "injected"},
        {"CCC",        "333"     },
      };

      cached_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : cached) {
        field = cached_headers.field_create(entry.name);
        cached_headers.field_attach(field);
        cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      response_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : response) {
        field = response_headers.field_create(entry.name);
        response_headers.field_attach(field);
        response_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);

      CHECK(cached_headers.fields_count() == 3);

      field = cached_headers.field_find("Connection"sv);
      CHECK(field == nullptr);

      field = cached_headers.field_find("X-Evil"sv);
      CHECK(field == nullptr);

      field = cached_headers.field_find("CCC"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "333"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Multiple Connection tokens are all skipped")
    {
      HTTPHdr        cached_headers;
      HTTPHdr        response_headers;
      ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
      ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header cached[] = {
        {"AAA", "111"},
      };
      struct header response[] = {
        {"Connection", "X-Foo, X-Bar"},
        {"X-Foo",      "a"           },
        {"X-Bar",      "b"           },
        {"DDD",        "444"         },
      };

      cached_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : cached) {
        field = cached_headers.field_create(entry.name);
        cached_headers.field_attach(field);
        cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      response_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : response) {
        field = response_headers.field_create(entry.name);
        response_headers.field_attach(field);
        response_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);

      CHECK(cached_headers.fields_count() == 2);

      field = cached_headers.field_find("X-Foo"sv);
      CHECK(field == nullptr);

      field = cached_headers.field_find("X-Bar"sv);
      CHECK(field == nullptr);

      field = cached_headers.field_find("DDD"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Connection: TE keeps cached TE and merges normal headers")
    {
      HTTPHdr        cached_headers;
      HTTPHdr        response_headers;
      ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
      ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header cached[] = {
        {"AAA", "111"     },
        {"TE",  "trailers"},
      };
      struct header response[] = {
        {"Connection", "TE"      },
        {"TE",         "trailers"},
        {"BBB",        "222"     },
      };

      cached_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : cached) {
        field = cached_headers.field_create(entry.name);
        cached_headers.field_attach(field);
        cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      response_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : response) {
        field = response_headers.field_create(entry.name);
        response_headers.field_attach(field);
        response_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);

      CHECK(cached_headers.fields_count() == 3);

      field = cached_headers.field_find("TE"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "trailers"sv);
      CHECK(field->has_dups() == false);

      field = cached_headers.field_find("BBB"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "222"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Have dup headers 2")
    {
      HTTPHdr        hdr1;
      HTTPHdr        hdr2;
      ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
      ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header input1[] = {
        {"AAA", "111"},
        {"BBB", "222"},
        {"CCC", "333"},
      };
      struct header input2[] = {
        {"DDD", "444"},
        {"DDD", "555"},
        {"FFF", "666"}
      };

      hdr1.create(HTTPType::RESPONSE);
      for (auto &&entry : input1) {
        field = hdr1.field_create(entry.name);
        hdr1.field_attach(field);
        hdr1.field_value_set(field, entry.value.data(), entry.value.length());
      }

      hdr2.create(HTTPType::RESPONSE);
      for (auto &&entry : input2) {
        field = hdr2.field_create(entry.name);
        hdr2.field_attach(field);
        hdr2.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);

      CHECK(hdr1.fields_count() == 6);

      field = hdr1.field_find("AAA"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "111"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("BBB"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "222"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("CCC"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "333"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("DDD"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == true);

      field = hdr1.field_find("FFF"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "666"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Have common and dup headers")
    {
      HTTPHdr        hdr1;
      HTTPHdr        hdr2;
      ts::PostScript hdr1_defer([&]() -> void { hdr1.destroy(); });
      ts::PostScript hdr2_defer([&]() -> void { hdr2.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header input1[] = {
        {"AAA", "111"},
        {"BBB", "222"},
        {"CCC", "333"},
        {"DDD", "444"},
      };
      struct header input2[] = {
        {"AAA", "555"},
        {"BBB", "666"},
        {"BBB", "777"},
        {"CCC", "888"},
        {"EEE", "999"},
      };

      hdr1.create(HTTPType::RESPONSE);
      for (auto &&entry : input1) {
        field = hdr1.field_create(entry.name);
        hdr1.field_attach(field);
        hdr1.field_value_set(field, entry.value.data(), entry.value.length());
      }

      hdr2.create(HTTPType::RESPONSE);
      for (auto &&entry : input2) {
        field = hdr2.field_create(entry.name);
        hdr2.field_attach(field);
        hdr2.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&hdr1, &hdr2);

      CHECK(hdr1.fields_count() == 6);

      field = hdr1.field_find("AAA"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "555"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("BBB"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "666"sv);
      CHECK(field->has_dups() == true);

      ///////////// Dup //////////////////////////
      field = field->m_next_dup;
      str   = field->value_get();
      CHECK(str == "777"sv);
      CHECK(field->has_dups() == false);
      ///////////////////////////////////////

      field = hdr1.field_find("CCC"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "888"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("DDD"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == false);

      field = hdr1.field_find("EEE"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "999"sv);
      CHECK(field->has_dups() == false);
    }

    SECTION("Response has superset")
    {
      HTTPHdr        cached_headers;
      HTTPHdr        response_headers;
      ts::PostScript cached_headers_defer([&]() -> void { cached_headers.destroy(); });
      ts::PostScript response_headers_defer([&]() -> void { response_headers.destroy(); });

      MIMEField *field;

      struct header {
        std::string_view name;
        std::string_view value;
      };

      struct header cached[] = {
        {"Foo",   "111"},
        {"Fizz",  "555"},
        {"Bar",   "333"},
        {"Bop",   "666"},
        {"Bar",   "222"},
        {"X-Foo", "aaa"},
        {"Eat",   "444"},
      };
      // Response headers in a 304 should, in theory, match the cached headers, but, what if they don't?
      // The response headers should still be merged into the cached object properly given the existing logic.
      // In the following, the ordering is different from the cached headers, the Bar headers are missing, and two duplicate Zip
      // headers are not in the cached object.
      struct header response[] = {
        {"X-Foo", "aaa"},
        {"Zip",   "888"},
        {"Zip",   "999"},
        {"Eat",   "444"},
        {"Foo",   "111"},
        {"Fizz",  "555"},
        {"Bop",   "666"},
      };

      cached_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : cached) {
        field = cached_headers.field_create(entry.name);
        cached_headers.field_attach(field);
        cached_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      response_headers.create(HTTPType::RESPONSE);
      for (auto &&entry : response) {
        field = response_headers.field_create(entry.name);
        response_headers.field_attach(field);
        response_headers.field_value_set(field, entry.value.data(), entry.value.length());
      }

      HttpTransact::merge_response_header_with_cached_header(&cached_headers, &response_headers);

      CHECK(cached_headers.fields_count() == 9);
      CHECK(response_headers.fields_count() == 7);

      field = cached_headers.field_find("Foo"sv);
      REQUIRE(field != nullptr);
      auto str{field->value_get()};
      CHECK(str == "111"sv);
      CHECK(field->has_dups() == false);

      field = cached_headers.field_find("Fizz"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "555"sv);
      CHECK(field->has_dups() == false);

      field = cached_headers.field_find("Bop"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "666"sv);
      CHECK(field->has_dups() == false);

      field = cached_headers.field_find("X-Foo"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "aaa"sv);
      CHECK(field->has_dups() == false);

      field = cached_headers.field_find("Eat"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "444"sv);
      CHECK(field->has_dups() == false);

      field = cached_headers.field_find("Bar"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "333"sv);
      CHECK(field->has_dups() == true);

      ///////////// Dup //////////////////////////
      field = field->m_next_dup;
      str   = field->value_get();
      CHECK(str == "222"sv);
      CHECK(field->has_dups() == false);
      ///////////////////////////////////////

      field = cached_headers.field_find("Zip"sv);
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "888"sv);
      CHECK(field->has_dups() == true);

      ///////////// Dup //////////////////////////
      REQUIRE(field->m_next_dup != nullptr);
      field = field->m_next_dup;
      REQUIRE(field != nullptr);
      str = field->value_get();
      CHECK(str == "999"sv);
      CHECK(field->has_dups() == false);
      ///////////////////////////////////////
    }

    // Regression: repeated 304 revalidation must not grow the cached header
    // without bound (see merge_response_header_with_cached_header). The
    // yardstick is HdrHeap::marshal_length() -- the size that gates the cache
    // write -- NOT fields_count(): dead MIMEField slots grow the heap even
    // while the live field count stays constant.
    SECTION("Repeated revalidation does not grow the cached header heap")
    {
      struct header {
        std::string_view name;
        std::string_view value;
      };

      auto build = [](HTTPHdr &h, const std::vector<header> &fields) {
        h.create(HTTPType::RESPONSE);
        for (auto &&e : fields) {
          MIMEField *f = h.field_create(e.name);
          h.field_attach(f);
          h.field_value_set(f, e.value);
        }
      };

      // Merge `response` into a copy of `cached0` `iterations` times and return
      // the growth in the cached header's marshalled heap size. A correct merge
      // is idempotent, so the growth must be bounded (independent of iterations).
      auto merge_growth = [&](const std::vector<header> &cached0, const std::vector<header> &response, int iterations) -> int {
        HTTPHdr        cached;
        HTTPHdr        resp;
        ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
        ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
        build(cached, cached0);
        build(resp, response);
        // One warm-up merge so any one-time restructuring settles before measuring.
        HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
        int const before = cached.m_heap->marshal_length();
        for (int i = 0; i < iterations; ++i) {
          HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
        }
        return cached.m_heap->marshal_length() - before;
      };

      // No duplicated fields: pure in-place replacement, bounded.
      CHECK(merge_growth(
              {
                {"Date",    "d0"},
                {"Expires", "e0"},
                {"Etag",    "t0"}
      },
              {{"Date", "d1"}, {"Expires", "e1"}, {"Etag", "t1"}}, 4000) <= 2048);

      // A duplicated field (e.g. Cache-Control: max-age=60, public) ahead of
      // single-valued fields must not force those single-valued fields down an
      // appending path. Before the fix the sticky "dups_seen" flag did exactly
      // that and this grew ~132 bytes per merge (megabytes over a day).
      CHECK(merge_growth(
              {
                {"Cache-Control", "max-age=60"},
                {"Cache-Control", "public"    },
                {"Expires",       "e0"        },
                {"Etag",          "t0"        }
      },
              {{"Cache-Control", "max-age=60"}, {"Cache-Control", "public"}, {"Expires", "e0"}, {"Etag", "t0"}}, 4000) <= 2048);

      // Response carries a duplicated field the cached copy does not yet have.
      CHECK(merge_growth(
              {
                {"Date",    "d0"},
                {"Expires", "e0"},
                {"Etag",    "t0"}
      },
              {{"Date", "d0"}, {"Vary", "A"}, {"Vary", "B"}, {"Expires", "e0"}, {"Etag", "t0"}}, 4000) <= 2048);

      // A response value that changes length every merge (as Expires does on a
      // real 304) must still be bounded: value_set frees the old string (self-
      // coalesced) and reuses the slot.
      {
        HTTPHdr        cached;
        HTTPHdr        resp;
        ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
        ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
        build(cached, {
                        {"Server",        "ATS"       },
                        {"Cache-Control", "max-age=60"},
                        {"Date",          "d0"        },
                        {"Expires",       "e0"        },
                        {"Etag",          "t0"        }
        });
        resp.create(HTTPType::RESPONSE);
        MIMEField *rf = resp.field_create("Expires"sv);
        resp.field_attach(rf);
        resp.field_value_set(rf, "e0"sv);
        HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
        int const before = cached.m_heap->marshal_length();
        char      buf[64];
        for (int i = 0; i < 2000; ++i) {
          int n = std::snprintf(buf, sizeof(buf), "Sat, 18 Jul 2026 05:%02d:%02d GMT-%d", i % 60, (i * 7) % 60, i % 1000);
          resp.field_value_set(rf, std::string_view(buf, n));
          HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
        }
        MIMEField *exp = cached.field_find("Expires"sv);
        REQUIRE(exp != nullptr);
        CHECK(exp->has_dups() == false);
        CHECK(cached.m_heap->marshal_length() - before <= 4096);
      }

      // Correctness: merging a response that DROPS one of two duplicate values
      // must leave the cached header with exactly the response's values.
      {
        HTTPHdr        cached;
        HTTPHdr        resp;
        ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
        ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
        build(cached, {
                        {"Vary", "A" },
                        {"Vary", "B" },
                        {"Vary", "C" },
                        {"Etag", "t0"}
        });
        build(resp, {
                      {"Vary", "X" },
                      {"Vary", "Y" },
                      {"Etag", "t1"}
        });
        HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
        int              count = 0;
        std::string_view v0, v1;
        for (MIMEField *d = cached.field_find("Vary"sv); d != nullptr; d = d->m_next_dup) {
          if (count == 0) {
            v0 = d->value_get();
          } else if (count == 1) {
            v1 = d->value_get();
          }
          ++count;
        }
        CHECK(count == 2); // surplus cached "C" removed
        CHECK(v0 == "X"sv);
        CHECK(v1 == "Y"sv);
        MIMEField *etag = cached.field_find("Etag"sv);
        REQUIRE(etag != nullptr);
        CHECK(etag->value_get() == "t1"sv);
        CHECK(etag->has_dups() == false);
      }

      // The production caller sequence: merge_and_update_headers_for_cache_update
      // deletes a caching header only when the 304 OMITS it, then merges. This
      // exercises the conditional-delete + the cooked WKS headers
      // (Cache-Control/Expires/Age) end to end, over many revalidations with a
      // changing Expires. The cached header heap and the cooked freshness values
      // must stay correct and bounded.
      {
        HTTPHdr        cached;
        ts::PostScript cached_defer([&]() -> void { cached.destroy(); });
        build(cached, {
                        {"Server",        "ATS"       },
                        {"Cache-Control", "max-age=60"},
                        {"Date",          "d0"        },
                        {"Expires",       "e0"        },
                        {"Etag",          "t0"        }
        });

        int settled = 0;
        for (int i = 0; i < 2000; ++i) {
          HTTPHdr        resp;
          ts::PostScript resp_defer([&]() -> void { resp.destroy(); });
          resp.create(HTTPType::RESPONSE);
          MIMEField *f = resp.field_create("Cache-Control"sv);
          resp.field_attach(f);
          resp.field_value_set(f, "max-age=60"sv);
          char buf[64];
          int  n = std::snprintf(buf, sizeof(buf), "Sat, 18 Jul 2026 05:%02d:%02d GMT", i % 60, (i * 7) % 60);
          f      = resp.field_create("Expires"sv);
          resp.field_attach(f);
          resp.field_value_set(f, std::string_view(buf, n));

          // Mirror merge_and_update_headers_for_cache_update: delete a caching
          // header only if the response omits it, then merge.
          if (!resp.presence(MIME_PRESENCE_AGE)) {
            cached.field_delete(static_cast<std::string_view>(MIME_FIELD_AGE));
          }
          if (!resp.presence(MIME_PRESENCE_ETAG)) {
            cached.field_delete(static_cast<std::string_view>(MIME_FIELD_ETAG));
          }
          if (!resp.presence(MIME_PRESENCE_EXPIRES)) {
            cached.field_delete(static_cast<std::string_view>(MIME_FIELD_EXPIRES));
          }
          HttpTransact::merge_response_header_with_cached_header(&cached, &resp);
          if (i == 0) {
            settled = cached.m_heap->marshal_length();
          }
        }
        // Etag was omitted by every 304, so it must have been dropped from cache.
        CHECK(cached.field_find("Etag"sv) == nullptr);
        // Expires stays single-valued and the cooked max-age is intact.
        MIMEField *exp = cached.field_find("Expires"sv);
        REQUIRE(exp != nullptr);
        CHECK(exp->has_dups() == false);
        CHECK(cached.get_cooked_cc_max_age() == 60);
        // Heap stays bounded across 2000 revalidations.
        CHECK(cached.m_heap->marshal_length() - settled <= 4096);
      }
    }
  }

  SECTION("HttpTransact::strip_at_headers removes duplicate internal headers")
  {
    static constexpr char raw_request[] = "GET / HTTP/1.1\r\n"
                                          "Host: example.test\r\n"
                                          "@Ats-Internal: first\r\n"
                                          "X-Keep: ok\r\n"
                                          "@Ats-Internal: second\r\n"
                                          "@Another: third\r\n"
                                          "\r\n";

    HTTPHdr        hdr;
    ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); });
    HTTPParser     parser;

    if (http_rsb.client_request_at_headers_stripped == nullptr) {
      http_rsb.client_request_at_headers_stripped =
        Metrics::Counter::createPtr("proxy.process.http.client_request_at_headers_stripped");
    }
    if (http_rsb.origin_response_at_headers_stripped == nullptr) {
      http_rsb.origin_response_at_headers_stripped =
        Metrics::Counter::createPtr("proxy.process.http.origin_response_at_headers_stripped");
    }

    hdr.create(HTTPType::REQUEST);
    http_parser_init(&parser);

    auto       *start = raw_request;
    auto const *end   = raw_request + sizeof(raw_request) - 1;
    ParseResult err;

    while (true) {
      err = hdr.parse_req(&parser, &start, end, true);
      if (err != ParseResult::CONT) {
        break;
      }
    }

    REQUIRE(err == ParseResult::DONE);
    HttpTransact::strip_at_headers(hdr, HttpTransact::AtHeaderSource::CLIENT_REQUEST, 1);

    CHECK(hdr.field_find("@Ats-Internal"sv) == nullptr);
    CHECK(hdr.field_find("@Another"sv) == nullptr);
    CHECK(hdr.field_find("Host"sv) != nullptr);

    MIMEField *field = hdr.field_find("X-Keep"sv);
    REQUIRE(field != nullptr);
    CHECK(field->value_get() == "ok"sv);
  }
}
