/*
 * 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 "iceberg/util/retry_util.h"

#include <concepts>
#include <limits>
#include <vector>

#include <gtest/gtest.h>

#include "iceberg/result.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/retry.h"
#include "iceberg/util/retry_util_internal.h"

namespace iceberg {
namespace {

struct ResultReturningTask {
  Result<int> operator()() const { return 1; }
};

struct NonResultReturningTask {
  int operator()() const { return 1; }
};

using test::CommitFailedRetry;
using test::FakeRetryEnvironment;
using test::TransientIORetry;

static_assert(detail::RetryTask<ResultReturningTask>);
static_assert(!detail::RetryTask<NonResultReturningTask>);
static_assert(requires(RetryRunner<CommitFailedRetry> runner, ResultReturningTask task) {
  { runner.Run(task) } -> std::same_as<Result<int>>;
});
static_assert(retry::NoRetry::kMode == retry::RetryPolicyMode::kNoRetry);

}  // namespace

TEST(RetryRunnerTest, SuccessOnFirstAttempt) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 3,
                                                           .min_wait_ms = 1,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return 42;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 42);
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
}

TEST(RetryRunnerTest, RetryOnceThenSucceed) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 3,
                                                           .min_wait_ms = 1,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count == 1) {
                            return CommitFailed("transient failure");
                          }
                          return 42;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 42);
  EXPECT_EQ(call_count, 2);
  EXPECT_EQ(attempts, 2);
}

TEST(RetryRunnerTest, MaxAttemptsExhausted) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 2,
                                                           .min_wait_ms = 1,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return CommitFailed("always fails");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kCommitFailed));
  EXPECT_EQ(call_count, 3);
  EXPECT_EQ(attempts, 3);
}

TEST(RetryRunnerTest, OnlyRetryOnFilter) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 3,
                                                           .min_wait_ms = 1,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return ValidationFailed("schema conflict");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
}

TEST(RetryRunnerTest, OnlyRetryOnMatchingError) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 2,
                                                           .min_wait_ms = 1,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count <= 2) {
                            return CommitFailed("transient");
                          }
                          return 100;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 100);
  EXPECT_EQ(call_count, 3);
  EXPECT_EQ(attempts, 3);
}

TEST(RetryRunnerTest, StopRetryOnMatchingError) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<retry::StopRetryOn<ErrorKind::kCommitStateUnknown>>(
                    RetryConfig{.num_retries = 5,
                                .min_wait_ms = 1,
                                .max_wait_ms = 10,
                                .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return CommitStateUnknown("datacenter on fire");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kCommitStateUnknown));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
}

TEST(RetryRunnerTest, StopRetryOnNonMatchingErrorAllowsRetry) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<retry::StopRetryOn<ErrorKind::kCommitStateUnknown>>(
                    RetryConfig{.num_retries = 2,
                                .min_wait_ms = 1,
                                .max_wait_ms = 10,
                                .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count == 1) {
                            return CommitFailed("retryable");
                          }
                          return 88;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 88);
  EXPECT_EQ(call_count, 2);
  EXPECT_EQ(attempts, 2);
}

TEST(RetryRunnerTest, ZeroRetriesAllowsUnsetPolicyAndSkipsBackoffValidation) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<retry::NoRetry>(RetryConfig{.num_retries = 0,
                                                        .min_wait_ms = 0,
                                                        .max_wait_ms = 0,
                                                        .total_timeout_ms = 5000,
                                                        .scale_factor = 0.5})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return CommitFailed("fail");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kCommitFailed));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
}

TEST(RetryRunnerTest, NegativeRetriesFailsBeforeTaskRuns) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<retry::NoRetry>(RetryConfig{.num_retries = -1,
                                                        .min_wait_ms = 1,
                                                        .max_wait_ms = 10,
                                                        .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return 1;
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument));
  EXPECT_THAT(result, HasErrorMessage("num_retries must be non-negative"));
  EXPECT_EQ(call_count, 0);
  EXPECT_EQ(attempts, 0);
}

TEST(RetryRunnerTest, InvalidBackoffConfigFailsBeforeTaskRuns) {
  struct InvalidConfigCase {
    RetryConfig config;
    const char* expected_message;
  };

  const std::vector<InvalidConfigCase> test_cases = {
      {.config = RetryConfig{.num_retries = std::numeric_limits<int32_t>::max(),
                             .min_wait_ms = 1,
                             .max_wait_ms = 10,
                             .total_timeout_ms = 5000},
       .expected_message = "num_retries is too large"},
      {.config = RetryConfig{.num_retries = 1,
                             .min_wait_ms = 0,
                             .max_wait_ms = 10,
                             .total_timeout_ms = 5000},
       .expected_message = "min_wait_ms must be positive"},
      {.config = RetryConfig{.num_retries = 1,
                             .min_wait_ms = 1,
                             .max_wait_ms = 0,
                             .total_timeout_ms = 5000},
       .expected_message = "max_wait_ms must be positive"},
      {.config = RetryConfig{.num_retries = 1,
                             .min_wait_ms = 20,
                             .max_wait_ms = 10,
                             .total_timeout_ms = 5000},
       .expected_message = "max_wait_ms must be greater than or equal to min_wait_ms"},
      {.config = RetryConfig{.num_retries = 1,
                             .min_wait_ms = 1,
                             .max_wait_ms = 10,
                             .total_timeout_ms = 5000,
                             .scale_factor = 0.5},
       .expected_message = "scale_factor must be finite and at least 1.0"},
      {.config = RetryConfig{.num_retries = 1,
                             .min_wait_ms = 1,
                             .max_wait_ms = 10,
                             .total_timeout_ms = 5000,
                             .scale_factor = std::numeric_limits<double>::infinity()},
       .expected_message = "scale_factor must be finite and at least 1.0"},
  };

  for (const auto& test_case : test_cases) {
    int call_count = 0;
    int32_t attempts = 0;

    auto result = RetryRunner<CommitFailedRetry>(test_case.config)
                      .Run(
                          [&]() -> Result<int> {
                            ++call_count;
                            return 1;
                          },
                          &attempts);

    EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument))
        << test_case.expected_message;
    EXPECT_THAT(result, HasErrorMessage(test_case.expected_message));
    EXPECT_EQ(call_count, 0);
    EXPECT_EQ(attempts, 0);
  }
}

TEST(RetryRunnerTest, NoRetryWithRetries) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<retry::NoRetry>(RetryConfig{.num_retries = 1,
                                                        .min_wait_ms = 1,
                                                        .max_wait_ms = 10,
                                                        .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return CommitFailed("fail");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument));
  EXPECT_THAT(result,
              HasErrorMessage("Retry policy must be enabled when num_retries > 0"));
  EXPECT_EQ(call_count, 0);
  EXPECT_EQ(attempts, 0);
}

TEST(RetryRunnerTest, TotalTimeoutStopsBeforeStartingAnotherAttempt) {
  FakeRetryEnvironment fake_retry;
  ScopedRetryTestHooks scoped_hooks(fake_retry.hooks());
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 3,
                                                           .min_wait_ms = 20,
                                                           .max_wait_ms = 20,
                                                           .total_timeout_ms = 15})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count == 1) {
                            fake_retry.Advance(FakeRetryEnvironment::Duration(10));
                          }
                          return CommitFailed("retry budget exhausted");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kCommitFailed));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
  EXPECT_TRUE(fake_retry.sleep_durations().empty());
  EXPECT_EQ(fake_retry.observed_base_delays_ms(), std::vector<int32_t>({20}));
}

TEST(RetryRunnerTest, TotalTimeoutStopsWhenDelayEqualsRemainingBudget) {
  FakeRetryEnvironment fake_retry;
  ScopedRetryTestHooks scoped_hooks(fake_retry.hooks());
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 3,
                                                           .min_wait_ms = 10,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 20})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          fake_retry.Advance(FakeRetryEnvironment::Duration(10));
                          return CommitFailed("retry budget exhausted");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kCommitFailed));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
  EXPECT_TRUE(fake_retry.sleep_durations().empty());
  EXPECT_EQ(fake_retry.observed_base_delays_ms(), std::vector<int32_t>({10}));
}

TEST(RetryRunnerTest, NonPositiveTotalTimeoutDisablesDeadline) {
  FakeRetryEnvironment fake_retry;
  ScopedRetryTestHooks scoped_hooks(fake_retry.hooks());
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 2,
                                                           .min_wait_ms = 10,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 0})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          fake_retry.Advance(FakeRetryEnvironment::Duration(100));
                          if (call_count <= 2) {
                            return CommitFailed("transient");
                          }
                          return 123;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 123);
  EXPECT_EQ(call_count, 3);
  EXPECT_EQ(attempts, 3);
  EXPECT_EQ(fake_retry.sleep_durations(), std::vector<FakeRetryEnvironment::Duration>(
                                              {FakeRetryEnvironment::Duration(10),
                                               FakeRetryEnvironment::Duration(10)}));
  EXPECT_EQ(fake_retry.observed_base_delays_ms(), std::vector<int32_t>({10, 10}));
}

TEST(RetryRunnerTest, RetryDelayDoesNotExceedMaxWaitAfterJitter) {
  FakeRetryEnvironment fake_retry;
  fake_retry.SetJitterOffsetMs(100);
  ScopedRetryTestHooks scoped_hooks(fake_retry.hooks());
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<CommitFailedRetry>(RetryConfig{.num_retries = 1,
                                                           .min_wait_ms = 10,
                                                           .max_wait_ms = 10,
                                                           .total_timeout_ms = 0})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count == 1) {
                            return CommitFailed("transient");
                          }
                          return 321;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 321);
  EXPECT_EQ(call_count, 2);
  EXPECT_EQ(attempts, 2);
  EXPECT_EQ(fake_retry.sleep_durations(), std::vector<FakeRetryEnvironment::Duration>(
                                              {FakeRetryEnvironment::Duration(10)}));
  EXPECT_EQ(fake_retry.observed_base_delays_ms(), std::vector<int32_t>({10}));
}

TEST(RetryRunnerTest, MakeCommitRetryRunnerConfig) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = MakeCommitRetryRunner(2, 1, 10, 5000)
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          return ValidationFailed("not retryable");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
}

TEST(RetryRunnerTest, MakeCommitRetryRunnerRetriesCommitFailed) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = MakeCommitRetryRunner(3, 1, 10, 5000)
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count <= 2) {
                            return CommitFailed("transient");
                          }
                          return 99;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 99);
  EXPECT_EQ(call_count, 3);
  EXPECT_EQ(attempts, 3);
}

TEST(RetryRunnerTest, OnlyRetryOnMultipleErrorKinds) {
  int call_count = 0;
  int32_t attempts = 0;

  using CommitOrUnavailable =
      retry::RetryPolicy<retry::RetryPolicyMode::kOnlyRetryOn, ErrorKind::kCommitFailed,
                         ErrorKind::kServiceUnavailable>;

  auto result = RetryRunner<CommitOrUnavailable>(RetryConfig{.num_retries = 5,
                                                             .min_wait_ms = 1,
                                                             .max_wait_ms = 10,
                                                             .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Result<int> {
                          ++call_count;
                          if (call_count == 1) {
                            return CommitFailed("conflict");
                          }
                          if (call_count == 2) {
                            return ServiceUnavailable("server busy");
                          }
                          return 77;
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(*result, 77);
  EXPECT_EQ(call_count, 3);
  EXPECT_EQ(attempts, 3);
}

TEST(RetryRunnerTest, RetriesTransientIO) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<TransientIORetry>(RetryConfig{.num_retries = 3,
                                                          .min_wait_ms = 1,
                                                          .max_wait_ms = 10,
                                                          .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Status {
                          ++call_count;
                          if (call_count == 1) {
                            return IOError("read failed");
                          }
                          if (call_count == 2) {
                            return ServiceUnavailable("server busy");
                          }
                          return {};
                        },
                        &attempts);

  EXPECT_THAT(result, IsOk());
  EXPECT_EQ(call_count, 3);
  EXPECT_EQ(attempts, 3);
}

TEST(RetryRunnerTest, DoesNotRetryNotFound) {
  int call_count = 0;
  int32_t attempts = 0;

  auto result = RetryRunner<TransientIORetry>(RetryConfig{.num_retries = 3,
                                                          .min_wait_ms = 1,
                                                          .max_wait_ms = 10,
                                                          .total_timeout_ms = 5000})
                    .Run(
                        [&]() -> Status {
                          ++call_count;
                          return NotFound("missing file");
                        },
                        &attempts);

  EXPECT_THAT(result, IsError(ErrorKind::kNotFound));
  EXPECT_EQ(call_count, 1);
  EXPECT_EQ(attempts, 1);
}

}  // namespace iceberg
