// Copyright (C) 2018 The Android Open Source Project
//
// 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.

package com.google.gerrit.acceptance.testsuite.account;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.InternalAccountUpdate;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.notedb.Sequences;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;

/**
 * The implementation of {@code AccountOperations}.
 *
 * <p>There is only one implementation of {@code AccountOperations}. Nevertheless, we keep the
 * separation between interface and implementation to enhance clarity.
 */
public class AccountOperationsImpl implements AccountOperations {
  private final Accounts accounts;
  private final AccountsUpdate accountsUpdate;
  private final Sequences seq;

  @Inject
  public AccountOperationsImpl(
      Accounts accounts, @ServerInitiated AccountsUpdate accountsUpdate, Sequences seq) {
    this.accounts = accounts;
    this.accountsUpdate = accountsUpdate;
    this.seq = seq;
  }

  @Override
  public PerAccountOperations account(Account.Id accountId) {
    return new PerAccountOperationsImpl(accountId);
  }

  @Override
  public TestAccountCreation.Builder newAccount() {
    return TestAccountCreation.builder(this::createAccount);
  }

  private Account.Id createAccount(TestAccountCreation accountCreation) throws Exception {
    AccountsUpdate.AccountUpdater accountUpdater =
        (accountState, updateBuilder) ->
            fillBuilder(updateBuilder, accountCreation, accountState.account().id());
    AccountState createdAccount = createAccount(accountUpdater);
    return createdAccount.account().id();
  }

  private AccountState createAccount(AccountsUpdate.AccountUpdater accountUpdater)
      throws IOException, ConfigInvalidException {
    Account.Id accountId = Account.id(seq.nextAccountId());
    return accountsUpdate.insert("Create Test Account", accountId, accountUpdater);
  }

  private static void fillBuilder(
      InternalAccountUpdate.Builder builder,
      TestAccountCreation accountCreation,
      Account.Id accountId) {
    accountCreation.fullname().ifPresent(builder::setFullName);
    accountCreation.preferredEmail().ifPresent(e -> setPreferredEmail(builder, accountId, e));
    String httpPassword = accountCreation.httpPassword().orElse(null);
    accountCreation.username().ifPresent(u -> setUsername(builder, accountId, u, httpPassword));
    accountCreation.status().ifPresent(builder::setStatus);
    accountCreation.active().ifPresent(builder::setActive);
    accountCreation
        .secondaryEmails()
        .forEach(
            secondaryEmail ->
                builder.addExternalId(ExternalId.createEmail(accountId, secondaryEmail)));
  }

  private static InternalAccountUpdate.Builder setPreferredEmail(
      InternalAccountUpdate.Builder builder, Account.Id accountId, String preferredEmail) {
    return builder
        .setPreferredEmail(preferredEmail)
        .addExternalId(ExternalId.createEmail(accountId, preferredEmail));
  }

  private static InternalAccountUpdate.Builder setUsername(
      InternalAccountUpdate.Builder builder,
      Account.Id accountId,
      String username,
      String httpPassword) {
    return builder.addExternalId(ExternalId.createUsername(username, accountId, httpPassword));
  }

  private class PerAccountOperationsImpl implements PerAccountOperations {
    private final Account.Id accountId;

    PerAccountOperationsImpl(Account.Id accountId) {
      this.accountId = accountId;
    }

    @Override
    public boolean exists() {
      return getAccountState(accountId).isPresent();
    }

    @Override
    public TestAccount get() {
      AccountState account =
          getAccountState(accountId)
              .orElseThrow(
                  () -> new IllegalStateException("Tried to get non-existing test account"));
      return toTestAccount(account);
    }

    private Optional<AccountState> getAccountState(Account.Id accountId) {
      try {
        return accounts.get(accountId);
      } catch (IOException | ConfigInvalidException e) {
        throw new IllegalStateException(e);
      }
    }

    private TestAccount toTestAccount(AccountState accountState) {
      Account account = accountState.account();
      return TestAccount.builder()
          .accountId(account.id())
          .preferredEmail(Optional.ofNullable(account.preferredEmail()))
          .fullname(Optional.ofNullable(account.fullName()))
          .username(accountState.userName())
          .active(accountState.account().isActive())
          .emails(ExternalId.getEmails(accountState.externalIds()).collect(toImmutableSet()))
          .build();
    }

    @Override
    public TestAccountUpdate.Builder forUpdate() {
      return TestAccountUpdate.builder(this::updateAccount);
    }

    private void updateAccount(TestAccountUpdate accountUpdate)
        throws IOException, ConfigInvalidException {
      AccountsUpdate.AccountUpdater accountUpdater =
          (accountState, updateBuilder) -> fillBuilder(updateBuilder, accountUpdate, accountState);
      Optional<AccountState> updatedAccount = updateAccount(accountUpdater);
      checkState(updatedAccount.isPresent(), "Tried to update non-existing test account");
    }

    private Optional<AccountState> updateAccount(AccountsUpdate.AccountUpdater accountUpdater)
        throws IOException, ConfigInvalidException {
      return accountsUpdate.update("Update Test Account", accountId, accountUpdater);
    }

    private void fillBuilder(
        InternalAccountUpdate.Builder builder,
        TestAccountUpdate accountUpdate,
        AccountState accountState) {
      accountUpdate.fullname().ifPresent(builder::setFullName);
      accountUpdate.preferredEmail().ifPresent(e -> setPreferredEmail(builder, accountId, e));
      String httpPassword = accountUpdate.httpPassword().orElse(null);
      accountUpdate.username().ifPresent(u -> setUsername(builder, accountId, u, httpPassword));
      accountUpdate.status().ifPresent(builder::setStatus);
      accountUpdate.active().ifPresent(builder::setActive);

      ImmutableSet<String> secondaryEmails = getSecondaryEmails(accountUpdate, accountState);
      ImmutableSet<String> newSecondaryEmails =
          ImmutableSet.copyOf(accountUpdate.secondaryEmailsModification().apply(secondaryEmails));
      if (!secondaryEmails.equals(newSecondaryEmails)) {
        setSecondaryEmails(builder, accountUpdate, accountState, newSecondaryEmails);
      }
    }

    private ImmutableSet<String> getSecondaryEmails(
        TestAccountUpdate accountUpdate, AccountState accountState) {
      ImmutableSet<String> allEmails =
          ExternalId.getEmails(accountState.externalIds()).collect(toImmutableSet());
      if (accountUpdate.preferredEmail().isPresent()) {
        return ImmutableSet.copyOf(
            Sets.difference(allEmails, ImmutableSet.of(accountUpdate.preferredEmail().get())));
      } else if (accountState.account().preferredEmail() != null) {
        return ImmutableSet.copyOf(
            Sets.difference(allEmails, ImmutableSet.of(accountState.account().preferredEmail())));
      }
      return allEmails;
    }

    private void setSecondaryEmails(
        InternalAccountUpdate.Builder builder,
        TestAccountUpdate accountUpdate,
        AccountState accountState,
        ImmutableSet<String> newSecondaryEmails) {
      // delete all external IDs of SCHEME_MAILTO scheme, then add back SCHEME_MAILTO external IDs
      // for the new secondary emails and the preferred email
      builder.deleteExternalIds(
          accountState.externalIds().stream()
              .filter(e -> e.isScheme(ExternalId.SCHEME_MAILTO))
              .collect(toImmutableSet()));
      builder.addExternalIds(
          newSecondaryEmails.stream()
              .map(secondaryEmail -> ExternalId.createEmail(accountId, secondaryEmail))
              .collect(toImmutableSet()));
      if (accountUpdate.preferredEmail().isPresent()) {
        builder.addExternalId(
            ExternalId.createEmail(accountId, accountUpdate.preferredEmail().get()));
      } else if (accountState.account().preferredEmail() != null) {
        builder.addExternalId(
            ExternalId.createEmail(accountId, accountState.account().preferredEmail()));
      }
    }

    @Override
    public TestAccountInvalidation.Builder forInvalidation() {
      return TestAccountInvalidation.builder(this::invalidateAccount);
    }

    private void invalidateAccount(TestAccountInvalidation testAccountInvalidation)
        throws Exception {
      Optional<AccountState> accountState = getAccountState(accountId);
      checkState(accountState.isPresent(), "Tried to invalidate a non-existing test account");

      if (testAccountInvalidation.preferredEmailWithoutExternalId().isPresent()) {
        updateAccount(
            (account, updateBuilder) ->
                updateBuilder.setPreferredEmail(
                    testAccountInvalidation.preferredEmailWithoutExternalId().get()));
      }
    }
  }
}
