//
// Copyright 2025 gRPC authors.
//
// Licensed 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 "src/core/credentials/call/regional_access_boundary_fetcher.h"

#include <grpc/support/alloc.h>
#include <grpc/support/string_util.h>
#include <grpc/support/time.h>

#include <memory>
#include <string>
#include <utility>

#include "src/core/call/metadata_batch.h"
#include "src/core/credentials/call/call_credentials.h"
#include "src/core/lib/iomgr/timer_manager.h"
#include "src/core/lib/promise/arena_promise.h"
#include "src/core/lib/resource_quota/arena.h"
#include "src/core/util/http_client/httpcli.h"
#include "src/core/util/ref_counted_ptr.h"
#include "src/core/util/sync.h"
#include "src/core/util/wait_for_single_owner.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h"
#include "test/core/test_util/test_config.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/log/log_entry.h"
#include "absl/log/log_sink.h"
#include "absl/log/log_sink_registry.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/match.h"
#include "absl/strings/string_view.h"

namespace grpc_core {

class RegionalAccessBoundaryFetcherTest : public ::testing::Test {
 protected:
  using RegionalAccessBoundary =
      RegionalAccessBoundaryFetcher::RegionalAccessBoundary;
  static constexpr Duration kRegionalAccessBoundarySoftCacheGraceDuration =
      Duration::Hours(1);
  bool has_cache(RegionalAccessBoundaryFetcher* fetcher) {
    MutexLock lock(&fetcher->cache_mu_);
    return fetcher->cache_.has_value();
  }
  bool has_cache() { return has_cache(fetcher_.get()); }

  std::string cached_encoded_locations() {
    MutexLock lock(&fetcher_->cache_mu_);
    return std::string(fetcher_->cache_->encoded_locations.as_string_view());
  }
  void set_cache(RegionalAccessBoundary cache) {
    MutexLock lock(&fetcher_->cache_mu_);
    fetcher_->cache_ = std::move(cache);
  }

  bool fetch_in_flight(RegionalAccessBoundaryFetcher* fetcher) {
    MutexLock lock(&fetcher->cache_mu_);
    return fetcher->pending_request_ != nullptr;
  }
  bool fetch_in_flight() { return fetch_in_flight(fetcher_.get()); }

  Timestamp next_fetch_time() {
    MutexLock lock(&fetcher_->cache_mu_);
    return fetcher_->next_fetch_time_;
  }

  WeakRefCountedPtr<RegionalAccessBoundaryFetcher> WeakFetcher() {
    return fetcher_->WeakRef();
  }

  bool is_shutdown(RegionalAccessBoundaryFetcher* fetcher) {
    MutexLock lock(&fetcher->cache_mu_);
    return fetcher->shutdown_;
  }

  bool check_pending_request_is_null(RegionalAccessBoundaryFetcher* fetcher) {
    MutexLock lock(&fetcher->cache_mu_);
    return fetcher->pending_request_ == nullptr;
  }

  void SetUp() override {
    fuzzing_event_engine_ =
        std::make_shared<grpc_event_engine::experimental::FuzzingEventEngine>(
            grpc_event_engine::experimental::FuzzingEventEngine::Options(),
            fuzzing_event_engine::Actions());
    grpc_timer_manager_set_start_threaded(false);
    grpc_init();
    fetcher_ = RegionalAccessBoundaryFetcher::Create(
        "https://googleapis.com", fuzzing_event_engine_,
        BackOff::Options()
            .set_initial_backoff(Duration::Seconds(1))
            .set_multiplier(1.1)
            .set_jitter(0.1)
            .set_max_backoff(Duration::Seconds(10)));
    arena_ = SimpleArenaAllocator()->MakeArena();
    metadata_ = arena_->MakePooled<ClientMetadata>();
    metadata_->Set(HttpAuthorityMetadata(),
                   Slice::FromStaticString("googleapis.com"));
    HttpRequest::SetOverride(nullptr, nullptr, nullptr);
  }

  void TearDown() override {
    HttpRequest::SetOverride(nullptr, nullptr, nullptr);
    {
      ExecCtx exec_ctx;
      fetcher_.reset();
    }
    fuzzing_event_engine_->FuzzingDone();
    fuzzing_event_engine_->TickUntilIdle();
    fuzzing_event_engine_->UnsetGlobalHooks();
    WaitForSingleOwner(std::move(fuzzing_event_engine_));
    grpc_shutdown();
  }

  RefCountedPtr<RegionalAccessBoundaryFetcher> fetcher_;
  RefCountedPtr<Arena> arena_;
  ClientMetadataHandle metadata_;
  std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>
      fuzzing_event_engine_;
};

namespace {

grpc_closure* g_stalled_on_done = nullptr;
grpc_http_response* g_stalled_response = nullptr;

int httpcli_get_stalled(const grpc_http_request* request, const URI& uri,
                        Timestamp deadline, grpc_closure* on_done,
                        grpc_http_response* response);

TEST_F(RegionalAccessBoundaryFetcherTest, CacheMissTriggersFetch) {
  ExecCtx exec_ctx;
  g_stalled_on_done = nullptr;
  g_stalled_response = nullptr;
  HttpRequest::SetOverride(httpcli_get_stalled, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  // Clean up the stalled request to avoid leaks and timeouts in shutdown.
  if (g_stalled_on_done != nullptr) {
    ExecCtx::Run(DEBUG_LOCATION, g_stalled_on_done,
                 absl::CancelledError("orphaned"));
    ExecCtx::Get()->Flush();
  }
}

TEST_F(RegionalAccessBoundaryFetcherTest, CacheHitDoesNotTriggerFetch) {
  ExecCtx exec_ctx;
  set_cache(RegionalAccessBoundary{Slice::FromStaticString("us-west1"),
                                   Timestamp::Now() + Duration::Seconds(7200)});
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_FALSE(fetch_in_flight());
  std::string buffer;
  std::optional<absl::string_view> value =
      metadata_->GetStringValue("x-allowed-locations", &buffer);
  EXPECT_THAT(value, ::testing::Optional(absl::string_view("us-west1")));
}

TEST_F(RegionalAccessBoundaryFetcherTest, ExpiredCacheTriggersFetch) {
  ExecCtx exec_ctx;
  g_stalled_on_done = nullptr;
  g_stalled_response = nullptr;
  HttpRequest::SetOverride(httpcli_get_stalled, nullptr, nullptr);
  set_cache(RegionalAccessBoundary{Slice::FromStaticString("us-west1"),
                                   Timestamp::Now() + Duration::Seconds(100)});
  fuzzing_event_engine_->TickForDuration(Duration::Seconds(101));
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  // Clean up the stalled request to avoid leaks and timeouts in shutdown.
  if (g_stalled_on_done != nullptr) {
    ExecCtx::Run(DEBUG_LOCATION, g_stalled_on_done,
                 absl::CancelledError("orphaned"));
    ExecCtx::Get()->Flush();
  }
}

TEST_F(RegionalAccessBoundaryFetcherTest, InvalidUriParsing) {
  auto fetcher = RegionalAccessBoundaryFetcher::Create(
      "invalid_uri_!@#$", fuzzing_event_engine_,
      BackOff::Options()
          .set_initial_backoff(Duration::Milliseconds(1))
          .set_multiplier(1.1)
          .set_jitter(0.1)
          .set_max_backoff(Duration::Milliseconds(10)));
  EXPECT_EQ(fetcher, nullptr);
}

TEST_F(RegionalAccessBoundaryFetcherTest, RegionalEndpointIgnored) {
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Set(HttpAuthorityMetadata(),
                 Slice::FromStaticString("rep.googleapis.com"));
  fetcher_->Fetch("", *metadata_);
  EXPECT_FALSE(fetch_in_flight());
  metadata_->Set(HttpAuthorityMetadata(),
                 Slice::FromStaticString("foo.rep.googleapis.com"));
  fetcher_->Fetch("", *metadata_);
  EXPECT_FALSE(fetch_in_flight());
}

grpc_http_response http_response(int status, const char* body) {
  grpc_http_response response;
  response = {};
  response.status = status;
  response.body = gpr_strdup(const_cast<char*>(body));
  response.body_length = strlen(body);
  return response;
}

int httpcli_get_valid_json(const grpc_http_request* /*request*/,
                           const URI& /*uri*/, Timestamp /*deadline*/,
                           grpc_closure* on_done,
                           grpc_http_response* response) {
  *response = http_response(
      200,
      "{\"encodedLocations\": \"us-west1\", \"locations\": [\"us-west1\"]}");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(RegionalAccessBoundaryFetcherTest, GoogleApisEndpointAllowed) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_valid_json, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Set(HttpAuthorityMetadata(),
                 Slice::FromStaticString("googleapis.com"));
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
}

TEST_F(RegionalAccessBoundaryFetcherTest, GoogleApisEndpointWithPortAllowed) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_valid_json, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Set(HttpAuthorityMetadata(),
                 Slice::FromStaticString("googleapis.com:443"));
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
}

TEST_F(RegionalAccessBoundaryFetcherTest, SubdomainGoogleApisEndpointAllowed) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_valid_json, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Set(HttpAuthorityMetadata(),
                 Slice::FromStaticString("pubsub.googleapis.com"));
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
}

int httpcli_get_malformed_json(const grpc_http_request* /*request*/,
                               const URI& /*uri*/, Timestamp /*deadline*/,
                               grpc_closure* on_done,
                               grpc_http_response* response) {
  *response = http_response(200, "{\"encodedLocations\"");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

int httpcli_get_missing_fields_json(const grpc_http_request* /*request*/,
                                    const URI& /*uri*/, Timestamp /*deadline*/,
                                    grpc_closure* on_done,
                                    grpc_http_response* response) {
  *response = http_response(200, "{\"locations\": [\"us-west1\"]}");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

int httpcli_get_valid_json_with_non_string_locations(
    const grpc_http_request* /*request*/, const URI& /*uri*/,
    Timestamp /*deadline*/, grpc_closure* on_done,
    grpc_http_response* response) {
  *response =
      http_response(200,
                    "{\"encodedLocations\": \"us-west1\", \"locations\": "
                    "[\"us-west1\", 123, \"us-east1\"]}");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(RegionalAccessBoundaryFetcherTest, ValidJsonResponse) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_valid_json, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_TRUE(has_cache());
  EXPECT_EQ(cached_encoded_locations(), "us-west1");
}

TEST_F(RegionalAccessBoundaryFetcherTest, MalformedJsonResponse) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_malformed_json, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_FALSE(has_cache());
  EXPECT_GT(next_fetch_time(), Timestamp::Now());
}

TEST_F(RegionalAccessBoundaryFetcherTest, ValidJsonMissingFields) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_missing_fields_json, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_FALSE(has_cache());
  EXPECT_GT(next_fetch_time(), Timestamp::Now());
}

TEST_F(RegionalAccessBoundaryFetcherTest, ValidJsonWithNonStringLocations) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_valid_json_with_non_string_locations,
                           nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_TRUE(has_cache());
  EXPECT_EQ(cached_encoded_locations(), "us-west1");
}

int g_mock_get_count = 0;

int httpcli_get_500(const grpc_http_request* /*request*/, const URI& /*uri*/,
                    Timestamp /*deadline*/, grpc_closure* on_done,
                    grpc_http_response* response) {
  g_mock_get_count++;
  *response = http_response(500, "");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(RegionalAccessBoundaryFetcherTest, RetryableHttpErrors) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  HttpRequest::SetOverride(httpcli_get_500, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  // After failure, fetch should no longer be in flight (request is reset during
  // backoff).
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_EQ(g_mock_get_count, 1);
  // We can advance time and retry.
  fuzzing_event_engine_->TickForDuration(
      Duration::Seconds(1) * 1.5 +
      Duration::Milliseconds(100));  // Initial backoff (1s) + jitter + buffer
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_EQ(g_mock_get_count, 2);
  fetcher_.reset();
}

int httpcli_get_network_error(const grpc_http_request* /*request*/,
                              const URI& /*uri*/, Timestamp /*deadline*/,
                              grpc_closure* on_done,
                              grpc_http_response* /*response*/) {
  ExecCtx::Run(DEBUG_LOCATION, on_done,
               absl::UnavailableError("Connection refused"));
  return 1;
}

class TestLogSink : public absl::LogSink {
 public:
  void Send(const absl::LogEntry& entry) override {
    messages.push_back(std::string(entry.text_message()));
  }
  std::vector<std::string> messages;
};

TEST_F(RegionalAccessBoundaryFetcherTest, NetworkErrorLogsResponseCorrectly) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_network_error, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });

  TestLogSink log_sink;
  absl::AddLogSink(&log_sink);

  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_FALSE(has_cache());

  absl::RemoveLogSink(&log_sink);

  bool found_log = false;
  for (const auto& msg : log_sink.messages) {
    if (absl::StrContains(msg,
                          "Regional access boundary request will be retried")) {
      found_log = true;
      EXPECT_THAT(msg,
                  ::testing::HasSubstr(
                      "failing with error: UNAVAILABLE:Connection refused"));
      EXPECT_THAT(msg, ::testing::HasSubstr("HTTP Status: 0"));
      EXPECT_THAT(msg, ::testing::Not(::testing::HasSubstr("Body:")));
    }
  }
  EXPECT_TRUE(found_log) << "Expected log message not found";
}

TEST_F(RegionalAccessBoundaryFetcherTest, RetryClearsPendingRequest) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  HttpRequest::SetOverride(httpcli_get_500, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  // Request should be cleared (reset) during backoff.
  EXPECT_FALSE(fetch_in_flight());
  // Advance time past backoff to allow retry.
  fuzzing_event_engine_->TickForDuration(Duration::Seconds(10));
  // Trigger retry.
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_EQ(g_mock_get_count, 2);
}

TEST_F(RegionalAccessBoundaryFetcherTest, CancelPendingFetch) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_500, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();

  auto fetcher = WeakFetcher();
  fetcher_.reset();
  EXPECT_FALSE(fetch_in_flight(fetcher.operator->()));
}

int httpcli_get_stalled(const grpc_http_request* /*request*/,
                        const URI& /*uri*/, Timestamp /*deadline*/,
                        grpc_closure* on_done, grpc_http_response* response) {
  g_mock_get_count++;
  g_stalled_on_done = on_done;
  g_stalled_response = response;
  // Do not call ExecCtx::Run here. The request is now "in flight" and stalled.
  return 1;
}

TEST_F(RegionalAccessBoundaryFetcherTest,
       CancelPendingFetchWithInFlightRequest) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  g_stalled_on_done = nullptr;
  g_stalled_response = nullptr;
  HttpRequest::SetOverride(httpcli_get_stalled, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  // Cancel immediately while the HTTP request is still in flight (but stalled).
  auto fetcher = WeakFetcher();
  fetcher_.reset();
  // The fetcher should have cleared the pending request internally. We can
  // verify by executing the stashed on_done closure explicitly.
  if (g_stalled_on_done != nullptr) {
    ExecCtx::Run(DEBUG_LOCATION, g_stalled_on_done,
                 absl::CancelledError("orphaned"));
  }
  ExecCtx::Get()->Flush();
  // The fetch shouldn't be in flight anymore since it was cleanly aborted.
  EXPECT_FALSE(fetch_in_flight(fetcher.operator->()));
  EXPECT_FALSE(fetch_in_flight(fetcher.operator->()));
}

TEST_F(RegionalAccessBoundaryFetcherTest, ResponseAfterShutdownIgnored) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  g_stalled_on_done = nullptr;
  g_stalled_response = nullptr;
  HttpRequest::SetOverride(httpcli_get_stalled, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  auto fetcher = WeakFetcher();
  RegionalAccessBoundaryFetcher* fetcher_raw = fetcher.operator->();
  // Orphan the fetcher (simulating shutdown)
  fetcher_.reset();
  // Verify fetcher is shutdown but memory is valid
  EXPECT_TRUE(is_shutdown(fetcher_raw));
  EXPECT_TRUE(check_pending_request_is_null(fetcher_raw));
  // Simulate a successful response arriving AFTER shutdown
  if (g_stalled_response != nullptr) {
    *g_stalled_response = http_response(
        200,
        "{\"encodedLocations\": \"us-west1\", \"locations\": [\"us-west1\"]}");
  }
  if (g_stalled_on_done != nullptr) {
    ExecCtx::Run(DEBUG_LOCATION, g_stalled_on_done, absl::OkStatus());
  }
  ExecCtx::Get()->Flush();
  // Verify cache was NOT updated (it requires inspecting the zombie object)
  EXPECT_FALSE(has_cache(fetcher_raw));
}

TEST_F(RegionalAccessBoundaryFetcherTest, BackoffResetsOnSuccess) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  // 1. Force a 500 error to trigger backoff.
  HttpRequest::SetOverride(httpcli_get_500, nullptr, nullptr);
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  // Verify backoff is active (next fetch time is in future)
  Timestamp after_first_fail = Timestamp::Now();
  EXPECT_GT(next_fetch_time(), after_first_fail);
  // 2. Advance time past backoff.
  fuzzing_event_engine_->TickForDuration(Duration::Seconds(2));
  // 3. Force a success.
  HttpRequest::SetOverride(httpcli_get_valid_json, nullptr, nullptr);
  auto metadata_success = arena_->MakePooled<ClientMetadata>();
  metadata_success->Set(HttpAuthorityMetadata(),
                        Slice::FromStaticString("googleapis.com"));
  metadata_success->Append("authorization",
                           Slice::FromStaticString("Bearer token"),
                           [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_success);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_TRUE(has_cache());
  // 4. Invalidate cache to force new fetch.
  set_cache(RegionalAccessBoundary{Slice::FromStaticString(""),
                                   Timestamp::InfPast()});
  // 5. Force another 500 error.
  HttpRequest::SetOverride(httpcli_get_500, nullptr, nullptr);
  auto metadata_fail = arena_->MakePooled<ClientMetadata>();
  metadata_fail->Set(HttpAuthorityMetadata(),
                     Slice::FromStaticString("googleapis.com"));
  metadata_fail->Append("authorization",
                        Slice::FromStaticString("Bearer token"),
                        [](absl::string_view, const Slice&) { abort(); });
  fetcher_->Fetch("", *metadata_fail);
  EXPECT_TRUE(fetch_in_flight());
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  // 6. Verify backoff was reset.
  // The next fetch time should be approximately initial_backoff from now.
  // If it wasn't reset, it would be much larger (exponentially increased).
  // Initial backoff is 1ms. W/ jitter & multiplier it's small.
  // We can just verify it is > now and <= now + max_expected_initial_backoff.
  Timestamp now = Timestamp::Now();
  Timestamp next = next_fetch_time();
  EXPECT_GT(next, now);
  // Initial backoff is 1s (set in SetUp). W/ jitter (0.1) it's [0.9s, 1.1s].
  EXPECT_LT(next - now, Duration::Milliseconds(1200));
  EXPECT_GT(next - now, Duration::Milliseconds(800));
}

TEST_F(RegionalAccessBoundaryFetcherTest, CacheSoftExpirationTriggersRefresh) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  HttpRequest::SetOverride(httpcli_get_valid_json, nullptr, nullptr);
  Timestamp now = Timestamp::Now();
  Timestamp soft_expired_timestamp =
      now + (kRegionalAccessBoundarySoftCacheGraceDuration / 2);
  set_cache(RegionalAccessBoundary{Slice::FromStaticString("us-east1"),
                                   soft_expired_timestamp});
  // Verify our mock cache setup is correct
  EXPECT_TRUE(has_cache());
  EXPECT_FALSE(fetch_in_flight());
  auto metadata_soft_expired = arena_->MakePooled<ClientMetadata>();
  metadata_soft_expired->Append(
      "authorization", Slice::FromStaticString("Bearer token"),
      [](absl::string_view, const Slice&) { abort(); });
  metadata_soft_expired->Set(HttpAuthorityMetadata(),
                             Slice::FromStaticString("googleapis.com"));
  fetcher_->Fetch("https://googleapis.com", *metadata_soft_expired);
  // We should still get the cached location
  EXPECT_EQ(cached_encoded_locations(), "us-east1");
  // AND a background refresh should have been triggered
  EXPECT_TRUE(fetch_in_flight());
  // Flush the exec context to allow the background fetch to complete
  ExecCtx::Get()->Flush();
  EXPECT_FALSE(fetch_in_flight());
  EXPECT_EQ(cached_encoded_locations(), "us-west1");
}

class EmailFetcherTest : public ::testing::Test {
 protected:
  void SetUp() override {
    fuzzing_event_engine_ =
        std::make_shared<grpc_event_engine::experimental::FuzzingEventEngine>(
            grpc_event_engine::experimental::FuzzingEventEngine::Options(),
            fuzzing_event_engine::Actions());
    grpc_timer_manager_set_start_threaded(false);
    grpc_init();
    email_fetcher_ = MakeRefCounted<EmailFetcher>(fuzzing_event_engine_);
    arena_ = SimpleArenaAllocator()->MakeArena();
    metadata_ = arena_->MakePooled<ClientMetadata>();
    HttpRequest::SetOverride(nullptr, nullptr, nullptr);
  }

  void TearDown() override {
    HttpRequest::SetOverride(nullptr, nullptr, nullptr);
    {
      ExecCtx exec_ctx;
      email_fetcher_.reset();
    }
    fuzzing_event_engine_->FuzzingDone();
    fuzzing_event_engine_->TickUntilIdle();
    fuzzing_event_engine_->UnsetGlobalHooks();
    WaitForSingleOwner(std::move(fuzzing_event_engine_));
    grpc_shutdown();
  }

  RefCountedPtr<EmailFetcher> email_fetcher_;
  RefCountedPtr<Arena> arena_;
  ClientMetadataHandle metadata_;
  std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>
      fuzzing_event_engine_;
};

int httpcli_get_email_success(const grpc_http_request* /*request*/,
                              const URI& uri, Timestamp /*deadline*/,
                              grpc_closure* on_done,
                              grpc_http_response* response) {
  if (uri.path() ==
      "/computeMetadata/v1/instance/service-accounts/default/email") {
    *response = http_response(200, "foo@bar.com");
  } else {
    // RAB fetch
    *response = http_response(
        200,
        "{\"encodedLocations\": \"us-west1\", \"locations\": [\"us-west1\"]}");
  }
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(EmailFetcherTest, FetchesEmailAndThenRab) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_email_success, nullptr, nullptr);
  // Start email fetch
  email_fetcher_->StartEmailFetch();
  // Fetch should trigger RAB fetch if email fetch succeeded
  // We need to wait for email fetch to complete.
  // Since we use ExecCtx::Run in mock, flushing ExecCtx should handle it.
  ExecCtx::Get()->Flush();
  // Now call Fetch. It should allow RAB fetch to proceed.
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Append(":authority", Slice::FromStaticString("foo.googleapis.com"),
                    [](absl::string_view, const Slice&) { abort(); });
  email_fetcher_->Fetch("token", *metadata_);
  // RAB fetch should happen (async).
  ExecCtx::Get()->Flush();
  // First fetch won't have metadata (cache miss).
  std::string buffer;
  std::optional<absl::string_view> value =
      metadata_->GetStringValue("x-allowed-locations", &buffer);
  EXPECT_FALSE(value.has_value());
  // Verify cache is populated by fetching again.
  metadata_ = arena_->MakePooled<ClientMetadata>();
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Append(":authority", Slice::FromStaticString("foo.googleapis.com"),
                    [](absl::string_view, const Slice&) { abort(); });
  email_fetcher_->Fetch("token", *metadata_);
  std::string buffer2;
  std::optional<absl::string_view> value2 =
      metadata_->GetStringValue("x-allowed-locations", &buffer2);
  EXPECT_THAT(value2, ::testing::Optional(absl::string_view("us-west1")));
}

int httpcli_get_email_failure(const grpc_http_request* /*request*/,
                              const URI& /*uri*/, Timestamp /*deadline*/,
                              grpc_closure* on_done,
                              grpc_http_response* response) {
  *response = http_response(404, "Not Found");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(EmailFetcherTest, EmailFetchFailureSkipsRab) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_email_failure, nullptr, nullptr);
  email_fetcher_->StartEmailFetch();
  ExecCtx::Get()->Flush();
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  email_fetcher_->Fetch("token", *metadata_);
  ExecCtx::Get()->Flush();
  std::string buffer;
  std::optional<absl::string_view> value =
      metadata_->GetStringValue("x-allowed-locations", &buffer);
  EXPECT_FALSE(value.has_value());
}

int httpcli_get_email_failure_counted(const grpc_http_request* /*request*/,
                                      const URI& /*uri*/,
                                      Timestamp /*deadline*/,
                                      grpc_closure* on_done,
                                      grpc_http_response* response) {
  g_mock_get_count++;
  *response = http_response(404, "Not Found");
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(EmailFetcherTest, EmailFetchBackoffRespected) {
  ExecCtx exec_ctx;
  g_mock_get_count = 0;
  HttpRequest::SetOverride(httpcli_get_email_failure_counted, nullptr, nullptr);
  // 1. First failure triggers backoff.
  email_fetcher_->StartEmailFetch();
  ExecCtx::Get()->Flush();
  EXPECT_EQ(g_mock_get_count, 1);
  // 2. Immediate second attempt should be skipped due to backoff.
  email_fetcher_->StartEmailFetch();
  ExecCtx::Get()->Flush();
  EXPECT_EQ(g_mock_get_count, 1);
  // 3. Advance time past initial backoff (1s with jitters, say 2s to be safe).
  fuzzing_event_engine_->TickForDuration(Duration::Seconds(20));
  // 4. Retry should proceed.
  email_fetcher_->StartEmailFetch();
  ExecCtx::Get()->Flush();
  EXPECT_EQ(g_mock_get_count, 2);
}

TEST_F(EmailFetcherTest, EarlyDestructionDoesNotCrash) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_stalled, nullptr, nullptr);
  email_fetcher_->StartEmailFetch();
  // Do NOT flush ExecCtx. We want the request to be in flight.
  // Now destroy the fetcher.
  email_fetcher_.reset();
  // Clean up the stalled request to avoid leaks and timeouts in shutdown.
  if (g_stalled_on_done != nullptr) {
    ExecCtx::Run(DEBUG_LOCATION, g_stalled_on_done,
                 absl::CancelledError("orphaned"));
    ExecCtx::Get()->Flush();
  }
}

int httpcli_get_spiffe_id(const grpc_http_request* /*request*/, const URI& uri,
                          Timestamp /*deadline*/, grpc_closure* on_done,
                          grpc_http_response* response) {
  if (uri.path() ==
      "/computeMetadata/v1/instance/service-accounts/default/email") {
    *response = http_response(
        200, "spiffe://my-project.svc.id.goog/ns/default/sa/default");
  }
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(EmailFetcherTest, InvalidEmailSkipsRab) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_spiffe_id, nullptr, nullptr);
  email_fetcher_->StartEmailFetch();
  ExecCtx::Get()->Flush();
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Append(":authority", Slice::FromStaticString("foo.googleapis.com"),
                    [](absl::string_view, const Slice&) { abort(); });
  email_fetcher_->Fetch("token", *metadata_);
  ExecCtx::Get()->Flush();
  std::string buffer;
  std::optional<absl::string_view> value =
      metadata_->GetStringValue("x-allowed-locations", &buffer);
  EXPECT_FALSE(value.has_value());
}

int httpcli_get_email_with_whitespace(const grpc_http_request* /*request*/,
                                      const URI& uri, Timestamp /*deadline*/,
                                      grpc_closure* on_done,
                                      grpc_http_response* response) {
  if (uri.path() ==
      "/computeMetadata/v1/instance/service-accounts/default/email") {
    *response = http_response(200, "  foo@bar.com\n");
  } else {
    // RAB fetch
    *response = http_response(
        200,
        "{\"encodedLocations\": \"us-west1\", \"locations\": [\"us-west1\"]}");
  }
  ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
  return 1;
}

TEST_F(EmailFetcherTest, EmailWithWhitespaceTrimmedAndSucceeds) {
  ExecCtx exec_ctx;
  HttpRequest::SetOverride(httpcli_get_email_with_whitespace, nullptr, nullptr);
  email_fetcher_->StartEmailFetch();
  ExecCtx::Get()->Flush();
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Append(":authority", Slice::FromStaticString("foo.googleapis.com"),
                    [](absl::string_view, const Slice&) { abort(); });
  email_fetcher_->Fetch("token", *metadata_);
  ExecCtx::Get()->Flush();
  // Cache miss on first fetch.
  std::string buffer;
  std::optional<absl::string_view> value =
      metadata_->GetStringValue("x-allowed-locations", &buffer);
  EXPECT_FALSE(value.has_value());

  // Verify cache is populated by fetching again (means the trimmed URL worked).
  metadata_ = arena_->MakePooled<ClientMetadata>();
  metadata_->Append("authorization", Slice::FromStaticString("Bearer token"),
                    [](absl::string_view, const Slice&) { abort(); });
  metadata_->Append(":authority", Slice::FromStaticString("foo.googleapis.com"),
                    [](absl::string_view, const Slice&) { abort(); });
  email_fetcher_->Fetch("token", *metadata_);
  std::string buffer2;
  std::optional<absl::string_view> value2 =
      metadata_->GetStringValue("x-allowed-locations", &buffer2);
  EXPECT_THAT(value2, ::testing::Optional(absl::string_view("us-west1")));
}

TEST(IsValidEmailTest, Valid) {
  EXPECT_TRUE(EmailFetcher::IsValidEmail("foo@bar.com"));
  EXPECT_TRUE(EmailFetcher::IsValidEmail("foo@bar.com."));
  EXPECT_TRUE(EmailFetcher::IsValidEmail("foo@.bar.com"));
  EXPECT_TRUE(EmailFetcher::IsValidEmail("foo@bar..com"));
}

TEST(IsValidEmailTest, Invalid) {
  EXPECT_FALSE(EmailFetcher::IsValidEmail("foo@bar"));
  EXPECT_FALSE(EmailFetcher::IsValidEmail("foo@bar@baz.com"));
  EXPECT_FALSE(EmailFetcher::IsValidEmail("@bar.com"));
  EXPECT_FALSE(EmailFetcher::IsValidEmail("foo@bar."));
}

}  // namespace

}  // namespace grpc_core

int main(int argc, char** argv) {
  testing::InitGoogleTest(&argc, argv);
  grpc::testing::TestEnvironment env(&argc, argv);
  return RUN_ALL_TESTS();
}