// Copyright (c) 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "quiche/blind_sign_auth/cached_blind_sign_auth.h"

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

#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "quiche/blind_sign_auth/blind_sign_auth_interface.h"
#include "quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h"
#include "quiche/common/platform/api/quiche_mutex.h"
#include "quiche/common/platform/api/quiche_test.h"
#include "quiche/common/test_tools/quiche_test_utils.h"

namespace quiche {
namespace test {
namespace {

using ::testing::_;
using ::testing::InvokeArgument;
using ::testing::Unused;

class CachedBlindSignAuthTest : public QuicheTest {
 protected:
  void SetUp() override {
    cached_blind_sign_auth_ =
        std::make_unique<CachedBlindSignAuth>(&mock_blind_sign_auth_interface_);
  }

  void TearDown() override {
    fake_tokens_.clear();
    cached_blind_sign_auth_.reset();
  }

 public:
  std::vector<BlindSignToken> MakeFakeTokens(int num_tokens) {
    std::vector<BlindSignToken> fake_tokens;
    for (int i = 0; i < kBlindSignAuthRequestMaxTokens; i++) {
      fake_tokens.push_back(BlindSignToken{absl::StrCat("token:", i),
                                           absl::Now() + absl::Hours(1)});
    }
    return fake_tokens;
  }

  std::vector<BlindSignToken> MakeExpiredTokens(int num_tokens) {
    std::vector<BlindSignToken> fake_tokens;
    for (int i = 0; i < kBlindSignAuthRequestMaxTokens; i++) {
      fake_tokens.push_back(BlindSignToken{absl::StrCat("token:", i),
                                           absl::Now() - absl::Hours(1)});
    }
    return fake_tokens;
  }

  MockBlindSignAuthInterface mock_blind_sign_auth_interface_;
  std::unique_ptr<CachedBlindSignAuth> cached_blind_sign_auth_;
  std::string oauth_token_ = "oauth_token";
  std::vector<BlindSignToken> fake_tokens_;
};

TEST_F(CachedBlindSignAuthTest, TestGetTokensOneCallSuccessful) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(1)
      .WillOnce(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeFakeTokens(num_tokens);
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = 5;
  QuicheNotification done;
  SignedTokenCallback callback =
      [num_tokens, &done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        for (int i = 0; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
        }
        done.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA, std::move(callback));
  done.WaitForNotification();
}

TEST_F(CachedBlindSignAuthTest, TestGetTokensMultipleRemoteCallsSuccessful) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(2)
      .WillRepeatedly(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeFakeTokens(num_tokens);
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = kBlindSignAuthRequestMaxTokens - 1;
  QuicheNotification first;
  SignedTokenCallback first_callback =
      [num_tokens, &first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        for (int i = 0; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
        }
        first.Notify();
      };

  cached_blind_sign_auth_->GetTokens(
      oauth_token_, num_tokens, ProxyLayer::kProxyA, std::move(first_callback));
  first.WaitForNotification();

  QuicheNotification second;
  SignedTokenCallback second_callback =
      [num_tokens, &second](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        EXPECT_EQ(tokens->at(0).token,
                  absl::StrCat("token:", kBlindSignAuthRequestMaxTokens - 1));
        for (int i = 1; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i - 1));
        }
        second.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA,
                                     std::move(second_callback));
  second.WaitForNotification();
}

TEST_F(CachedBlindSignAuthTest, TestGetTokensSecondRequestFilledFromCache) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(1)
      .WillOnce(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeFakeTokens(num_tokens);
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = kBlindSignAuthRequestMaxTokens / 2;
  QuicheNotification first;
  SignedTokenCallback first_callback =
      [num_tokens, &first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        for (int i = 0; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
        }
        first.Notify();
      };

  cached_blind_sign_auth_->GetTokens(
      oauth_token_, num_tokens, ProxyLayer::kProxyA, std::move(first_callback));
  first.WaitForNotification();

  QuicheNotification second;
  SignedTokenCallback second_callback =
      [num_tokens, &second](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        for (int i = 0; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token,
                    absl::StrCat("token:", i + num_tokens));
        }
        second.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA,
                                     std::move(second_callback));
  second.WaitForNotification();
}

TEST_F(CachedBlindSignAuthTest, TestGetTokensThirdRequestRefillsCache) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(2)
      .WillRepeatedly(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeFakeTokens(num_tokens);
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = kBlindSignAuthRequestMaxTokens / 2;
  QuicheNotification first;
  SignedTokenCallback first_callback =
      [num_tokens, &first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        for (int i = 0; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
        }
        first.Notify();
      };

  cached_blind_sign_auth_->GetTokens(
      oauth_token_, num_tokens, ProxyLayer::kProxyA, std::move(first_callback));
  first.WaitForNotification();

  QuicheNotification second;
  SignedTokenCallback second_callback =
      [num_tokens, &second](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(num_tokens, tokens->size());
        for (int i = 0; i < num_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token,
                    absl::StrCat("token:", i + num_tokens));
        }
        second.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA,
                                     std::move(second_callback));
  second.WaitForNotification();

  QuicheNotification third;
  int third_request_tokens = 10;
  SignedTokenCallback third_callback =
      [third_request_tokens,
       &third](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(third_request_tokens, tokens->size());
        for (int i = 0; i < third_request_tokens; i++) {
          EXPECT_EQ(tokens->at(i).token, absl::StrCat("token:", i));
        }
        third.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, third_request_tokens,
                                     ProxyLayer::kProxyA,
                                     std::move(third_callback));
  third.WaitForNotification();
}

TEST_F(CachedBlindSignAuthTest, TestGetTokensRequestTooLarge) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(0);

  int num_tokens = kBlindSignAuthRequestMaxTokens + 1;
  SignedTokenCallback callback =
      [](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
        EXPECT_THAT(
            tokens.status().message(),
            absl::StrFormat("Number of tokens requested exceeds maximum: %d",
                            kBlindSignAuthRequestMaxTokens));
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA, std::move(callback));
}

TEST_F(CachedBlindSignAuthTest, TestGetTokensRequestNegative) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(0);

  int num_tokens = -1;
  SignedTokenCallback callback =
      [num_tokens](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
        EXPECT_THAT(tokens.status().message(),
                    absl::StrFormat("Negative number of tokens requested: %d",
                                    num_tokens));
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA, std::move(callback));
}

TEST_F(CachedBlindSignAuthTest, TestHandleGetTokensResponseErrorHandling) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(2)
      .WillOnce(
          [](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            std::move(callback)(absl::InternalError("AuthAndSign failed"));
          })
      .WillOnce(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeFakeTokens(num_tokens);
            fake_tokens_.pop_back();
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = kBlindSignAuthRequestMaxTokens;
  QuicheNotification first;
  SignedTokenCallback first_callback =
      [&first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal);
        EXPECT_THAT(tokens.status().message(), "AuthAndSign failed");
        first.Notify();
      };

  cached_blind_sign_auth_->GetTokens(
      oauth_token_, num_tokens, ProxyLayer::kProxyA, std::move(first_callback));
  first.WaitForNotification();

  QuicheNotification second;
  SignedTokenCallback second_callback =
      [&second](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(),
                    absl::StatusCode::kResourceExhausted);
        second.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA,
                                     std::move(second_callback));
  second.WaitForNotification();
}

TEST_F(CachedBlindSignAuthTest, TestGetTokensZeroTokensRequested) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(0);

  int num_tokens = 0;
  SignedTokenCallback callback =
      [](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        QUICHE_EXPECT_OK(tokens);
        EXPECT_EQ(tokens->size(), 0);
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA, std::move(callback));
}

TEST_F(CachedBlindSignAuthTest, TestExpiredTokensArePruned) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(1)
      .WillOnce(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeExpiredTokens(num_tokens);
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = kBlindSignAuthRequestMaxTokens;
  QuicheNotification first;
  SignedTokenCallback first_callback =
      [&first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(),
                    absl::StatusCode::kResourceExhausted);
        first.Notify();
      };

  cached_blind_sign_auth_->GetTokens(
      oauth_token_, num_tokens, ProxyLayer::kProxyA, std::move(first_callback));
  first.WaitForNotification();
}

TEST_F(CachedBlindSignAuthTest, TestClearCacheRemovesTokens) {
  EXPECT_CALL(mock_blind_sign_auth_interface_,
              GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _, _))
      .Times(2)
      .WillRepeatedly(
          [this](Unused, int num_tokens, Unused, SignedTokenCallback callback) {
            fake_tokens_ = MakeExpiredTokens(num_tokens);
            std::move(callback)(absl::MakeSpan(fake_tokens_));
          });

  int num_tokens = kBlindSignAuthRequestMaxTokens / 2;
  QuicheNotification first;
  SignedTokenCallback first_callback =
      [&first](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(),
                    absl::StatusCode::kResourceExhausted);
        first.Notify();
      };

  cached_blind_sign_auth_->GetTokens(
      oauth_token_, num_tokens, ProxyLayer::kProxyA, std::move(first_callback));
  first.WaitForNotification();

  cached_blind_sign_auth_->ClearCache();

  QuicheNotification second;
  SignedTokenCallback second_callback =
      [&second](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
        EXPECT_THAT(tokens.status().code(),
                    absl::StatusCode::kResourceExhausted);
        second.Notify();
      };

  cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens,
                                     ProxyLayer::kProxyA,
                                     std::move(second_callback));
  second.WaitForNotification();
}

}  // namespace
}  // namespace test
}  // namespace quiche
