// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "google_apis/gaia/google_service_auth_error.h"

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

#include "base/check_op.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "net/base/net_errors.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"

#if BUILDFLAG(IS_ANDROID)
#include "google_apis/gaia/android/jni_headers/GoogleServiceAuthError_jni.h"
#endif  // BUILDFLAG(IS_ANDROID)

namespace {
const char* InvalidCredentialsReasonToString(
    GoogleServiceAuthError::InvalidGaiaCredentialsReason reason) {
  using InvalidGaiaCredentialsReason =
      GoogleServiceAuthError::InvalidGaiaCredentialsReason;
  switch (reason) {
    case InvalidGaiaCredentialsReason::UNKNOWN:
      return "unknown";
    case InvalidGaiaCredentialsReason::CREDENTIALS_REJECTED_BY_SERVER:
      return "credentials rejected by server";
    case InvalidGaiaCredentialsReason::CREDENTIALS_REJECTED_BY_CLIENT:
      return "credentials rejected by client";
    case InvalidGaiaCredentialsReason::CREDENTIALS_MISSING:
      return "credentials missing";
    case InvalidGaiaCredentialsReason::NUM_REASONS:
      NOTREACHED();
  }
}

const char* ScopeLimitedUnrecoverableErrorReasonToString(
    GoogleServiceAuthError::ScopeLimitedUnrecoverableErrorReason reason) {
  using enum GoogleServiceAuthError::ScopeLimitedUnrecoverableErrorReason;
  switch (reason) {
    case kInvalidGrantRaptError:
      return "invalid grant rapt error";
    case kInvalidScope:
      return "invalid scope";
    case kRestrictedClient:
      return "restricted client";
    case kAdminPolicyEnforced:
      return "admin policy enforced";
    case kRemoteConsentResolutionRequired:
      return "remote consent resolution required";
    case kAccessDenied:
      return "access denied";
  }
  NOTREACHED();
}

bool IsTransientError(GoogleServiceAuthError::State state) {
  switch (state) {
    // These are failures that are likely to succeed if tried again.
    case GoogleServiceAuthError::CONNECTION_FAILED:
    case GoogleServiceAuthError::SERVICE_UNAVAILABLE:
    case GoogleServiceAuthError::REQUEST_CANCELED:
    case GoogleServiceAuthError::CHALLENGE_RESPONSE_REQUIRED:
      return true;
    // Everything else will have the same result.
    default:
      return false;
  }
}
}  // namespace

bool operator==(const GoogleServiceAuthError&,
                const GoogleServiceAuthError&) = default;

GoogleServiceAuthError::GoogleServiceAuthError() : details_(None()) {}

GoogleServiceAuthError::GoogleServiceAuthError(
    const GoogleServiceAuthError& other) = default;

GoogleServiceAuthError& GoogleServiceAuthError::operator=(
    const GoogleServiceAuthError& other) = default;

GoogleServiceAuthError::~GoogleServiceAuthError() = default;

// static
GoogleServiceAuthError GoogleServiceAuthError::FromConnectionError(int error) {
  return GoogleServiceAuthError(Details(
      ConnectionFailed{.network_error = static_cast<net::Error>(error)}));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
    InvalidGaiaCredentialsReason reason) {
  return GoogleServiceAuthError(
      Details(InvalidGaiaCredentials{.reason = reason}));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::FromServiceUnavailable(
    const std::string& error_message) {
  return GoogleServiceAuthError(
      Details(ServiceUnavailable{.error_message = error_message}));
}

// static
GoogleServiceAuthError
GoogleServiceAuthError::FromScopeLimitedUnrecoverableErrorReason(
    ScopeLimitedUnrecoverableErrorReason reason) {
  return GoogleServiceAuthError(
      Details(ScopeLimitedUnrecoverableError{.reason = reason}));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::FromServiceError(
    const std::string& error_message) {
  return GoogleServiceAuthError(
      Details(ServiceError{.error_message = error_message}));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::FromUnexpectedServiceResponse(
    const std::string& error_message) {
  return GoogleServiceAuthError(
      Details(UnexpectedServiceResponse{.error_message = error_message}));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::FromTokenBindingChallenge(
    const std::string& challenge) {
  return GoogleServiceAuthError(
      Details(ChallengeResponseRequired{.token_binding_challenge = challenge}));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::AuthErrorNone() {
  return GoogleServiceAuthError(Details(None()));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::FromDeviceManagementError(
    std::unique_ptr<gaia::DeviceManagementErrorDetails> details) {
  CHECK(details);
  return GoogleServiceAuthError(DeviceManagementError(std::move(details)));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::CreateAccountNotFound() {
  return GoogleServiceAuthError(Details(AccountNotFound()));
}

// static
GoogleServiceAuthError GoogleServiceAuthError::CreateRequestCanceled() {
  return GoogleServiceAuthError(Details(RequestCanceled()));
}

// static
bool GoogleServiceAuthError::IsValid(State state) {
  switch (state) {
    case NONE:
    case INVALID_GAIA_CREDENTIALS:
    case ACCOUNT_NOT_FOUND:
    case CONNECTION_FAILED:
    case SERVICE_UNAVAILABLE:
    case REQUEST_CANCELED:
    case UNEXPECTED_SERVICE_RESPONSE:
    case SERVICE_ERROR:
    case SCOPE_LIMITED_UNRECOVERABLE_ERROR:
    case CHALLENGE_RESPONSE_REQUIRED:
    case DEVICE_MANAGEMENT_ERROR:
      return true;
    case NUM_STATES:
      return false;
  }

  return false;
}

GoogleServiceAuthError::State GoogleServiceAuthError::state() const {
  return std::visit(
      absl::Overload{
          [](const None&) { return NONE; },
          [](const InvalidGaiaCredentials&) {
            return INVALID_GAIA_CREDENTIALS;
          },
          [](const AccountNotFound&) { return ACCOUNT_NOT_FOUND; },
          [](const ConnectionFailed&) { return CONNECTION_FAILED; },
          [](const ServiceUnavailable&) { return SERVICE_UNAVAILABLE; },
          [](const RequestCanceled&) { return REQUEST_CANCELED; },
          [](const UnexpectedServiceResponse&) {
            return UNEXPECTED_SERVICE_RESPONSE;
          },
          [](const ServiceError&) { return SERVICE_ERROR; },
          [](const ScopeLimitedUnrecoverableError&) {
            return SCOPE_LIMITED_UNRECOVERABLE_ERROR;
          },
          [](const ChallengeResponseRequired&) {
            return CHALLENGE_RESPONSE_REQUIRED;
          },
          [](const DeviceManagementError& e) {
            return DEVICE_MANAGEMENT_ERROR;
          },
      },
      details_);
}

const std::string& GoogleServiceAuthError::error_message() const {
  return std::visit(
      absl::Overload{[](const ServiceUnavailable& d) -> const std::string& {
                       return d.error_message;
                     },
                     [](const UnexpectedServiceResponse& d)
                         -> const std::string& { return d.error_message; },
                     [](const ServiceError& d) -> const std::string& {
                       return d.error_message;
                     },
                     [](const auto&) -> const std::string& {
                       return base::EmptyString();
                     }},
      details_);
}

net::Error GoogleServiceAuthError::GetNetworkError() const {
  const auto* connection_failed = std::get_if<ConnectionFailed>(&details_);
  CHECK(connection_failed);
  return connection_failed->network_error;
}

const std::string& GoogleServiceAuthError::GetTokenBindingChallenge() const {
  const auto* challenge_response_required =
      std::get_if<ChallengeResponseRequired>(&details_);
  CHECK(challenge_response_required);
  return challenge_response_required->token_binding_challenge;
}

GoogleServiceAuthError::InvalidGaiaCredentialsReason
GoogleServiceAuthError::GetInvalidGaiaCredentialsReason() const {
  const auto* invalid_gaia_credentials =
      std::get_if<InvalidGaiaCredentials>(&details_);
  CHECK(invalid_gaia_credentials);
  return invalid_gaia_credentials->reason;
}

GoogleServiceAuthError::ScopeLimitedUnrecoverableErrorReason
GoogleServiceAuthError::GetScopeLimitedUnrecoverableErrorReason() const {
  const auto* scope_limited_unrecoverable_error =
      std::get_if<ScopeLimitedUnrecoverableError>(&details_);
  CHECK(scope_limited_unrecoverable_error);
  return scope_limited_unrecoverable_error->reason;
}

std::string GoogleServiceAuthError::ToString() const {
  return std::visit(
      absl::Overload{
          [](const None&) { return std::string(); },
          [](const InvalidGaiaCredentials& invalid_gaia_credentials) {
            return base::StringPrintf("Invalid credentials (%s).",
                                      InvalidCredentialsReasonToString(
                                          invalid_gaia_credentials.reason));
          },
          [](const AccountNotFound&) { return std::string("Not authorized."); },
          [](const ConnectionFailed& connection_failed) {
            return base::StringPrintf("Connection failed (%d).",
                                      connection_failed.network_error);
          },
          [](const ServiceUnavailable& service_unavailable) {
            return base::StringPrintf(
                "Service unavailable; try again later (%s).",
                service_unavailable.error_message);
          },
          [](const RequestCanceled&) {
            return std::string("Request canceled.");
          },
          [](const UnexpectedServiceResponse& unexpected_service_response) {
            return base::StringPrintf(
                "Unexpected service response (%s)",
                unexpected_service_response.error_message.c_str());
          },
          [](const ServiceError& service_error) {
            return base::StringPrintf("Service responded with error: '%s'",
                                      service_error.error_message.c_str());
          },
          [](const ScopeLimitedUnrecoverableError&
                 scope_limited_unrecoverable_error) {
            return base::StringPrintf(
                "OAuth scope error (%s).",
                ScopeLimitedUnrecoverableErrorReasonToString(
                    scope_limited_unrecoverable_error.reason));
          },
          [](const ChallengeResponseRequired&) {
            return std::string(
                "Service responded with a token binding challenge.");
          },
          [](const DeviceManagementError& e) {
            return base::StringPrintf(
                "Device management error (is_user_actionable: %s)",
                e.details->IsUserActionable() ? "true" : "false");
          },
      },
      details_);
}

bool GoogleServiceAuthError::IsPersistentError() const {
  if (state() == GoogleServiceAuthError::NONE) {
    return false;
  }
  return !IsTransientError();
}

bool GoogleServiceAuthError::IsScopePersistentError() const {
  return state() == GoogleServiceAuthError::SCOPE_LIMITED_UNRECOVERABLE_ERROR;
}

bool GoogleServiceAuthError::IsTransientError() const {
  return ::IsTransientError(state());
}

bool GoogleServiceAuthError::IsDeviceManagementErrorUserActionable() const {
  const auto* dmerror = std::get_if<DeviceManagementError>(&details_);
  CHECK(dmerror);
  return dmerror->details->IsUserActionable();
}

const gaia::DeviceManagementErrorDetails&
GoogleServiceAuthError::GetDeviceManagementErrorDetails() const {
  const auto* dmerror = std::get_if<DeviceManagementError>(&details_);
  CHECK(dmerror);
  return *dmerror->details;
}

GoogleServiceAuthError::GoogleServiceAuthError(Details details)
    : details_(std::move(details)) {}

#if BUILDFLAG(IS_ANDROID)
// static
GoogleServiceAuthError GoogleServiceAuthError::FromJavaObject(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_auth_error) {
  CHECK(j_auth_error);
  using GSAE = GoogleServiceAuthError;

  GSAE::State state = static_cast<GSAE::State>(
      Java_GoogleServiceAuthError_getState(env, j_auth_error));
  switch (state) {
    case GSAE::NONE:
      return GSAE::AuthErrorNone();
    case GSAE::SCOPE_LIMITED_UNRECOVERABLE_ERROR:
      // Android doesn't provide reasons for this type of errors and only
      // creates them for enterprise policy enforced scopes. So we hardcode the
      // value here.
      return GSAE::FromScopeLimitedUnrecoverableErrorReason(
          GSAE::ScopeLimitedUnrecoverableErrorReason::kAdminPolicyEnforced);
    case GSAE::INVALID_GAIA_CREDENTIALS:
      return GSAE::FromInvalidGaiaCredentialsReason(
          GSAE::InvalidGaiaCredentialsReason::UNKNOWN);
    case GSAE::CONNECTION_FAILED:
      return GSAE::FromConnectionError(net::ERR_FAILED);
    case GSAE::REQUEST_CANCELED:
      return GSAE::CreateRequestCanceled();
    case GSAE::ACCOUNT_NOT_FOUND:
      return GSAE::CreateAccountNotFound();
    default:
      NOTREACHED();
  }
}

jni_zero::ScopedJavaLocalRef<jobject> GoogleServiceAuthError::ToJavaObject(
    JNIEnv* env) const {
  return Java_GoogleServiceAuthError_Constructor(env, state());
}

static bool JNI_GoogleServiceAuthError_IsTransientError(JNIEnv* env,
                                                        int32_t state) {
  return IsTransientError(static_cast<GoogleServiceAuthError::State>(state));
}
#endif  // BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_ANDROID)
DEFINE_JNI(GoogleServiceAuthError)
#endif

GoogleServiceAuthError::DeviceManagementError::DeviceManagementError(
    std::unique_ptr<gaia::DeviceManagementErrorDetails> detail)
    : details(std::move(detail)) {
  CHECK(details);
}

GoogleServiceAuthError::DeviceManagementError::~DeviceManagementError() =
    default;

GoogleServiceAuthError::DeviceManagementError::DeviceManagementError(
    const DeviceManagementError& other) {
  details = other.details->Clone();
  CHECK(details);
}

GoogleServiceAuthError::DeviceManagementError&
GoogleServiceAuthError::DeviceManagementError::operator=(
    const DeviceManagementError& other) {
  if (this != &other) {
    details = other.details->Clone();
    CHECK(details);
  }
  return *this;
}

bool GoogleServiceAuthError::DeviceManagementError::operator==(
    const DeviceManagementError& other) const {
  CHECK(details && other.details);
  return details->Equals(*other.details);
}

GoogleServiceAuthError::DeviceManagementError::DeviceManagementError(
    DeviceManagementError&& other) noexcept = default;

GoogleServiceAuthError::DeviceManagementError&
GoogleServiceAuthError::DeviceManagementError::operator=(
    DeviceManagementError&& other) noexcept = default;
