// 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.group;

import static com.google.common.base.Preconditions.checkState;

import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.exceptions.NoSuchGroupException;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.GroupUuid;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupCreation;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
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;
import org.eclipse.jgit.lib.PersonIdent;

/**
 * The implementation of {@code GroupOperations}.
 *
 * <p>There is only one implementation of {@code GroupOperations}. Nevertheless, we keep the
 * separation between interface and implementation to enhance clarity.
 */
public class GroupOperationsImpl implements GroupOperations {
  private final Groups groups;
  private final GroupsUpdate groupsUpdate;
  private final Sequences seq;
  private final PersonIdent serverIdent;

  @Inject
  public GroupOperationsImpl(
      Groups groups,
      @ServerInitiated GroupsUpdate groupsUpdate,
      Sequences seq,
      @GerritPersonIdent PersonIdent serverIdent) {
    this.groups = groups;
    this.groupsUpdate = groupsUpdate;
    this.seq = seq;
    this.serverIdent = serverIdent;
  }

  @Override
  public PerGroupOperations group(AccountGroup.UUID groupUuid) {
    return new PerGroupOperationsImpl(groupUuid);
  }

  @Override
  public TestGroupCreation.Builder newGroup() {
    return TestGroupCreation.builder(this::createNewGroup);
  }

  private AccountGroup.UUID createNewGroup(TestGroupCreation groupCreation)
      throws ConfigInvalidException, IOException {
    InternalGroupCreation internalGroupCreation = toInternalGroupCreation(groupCreation);
    InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupCreation);
    InternalGroup internalGroup =
        groupsUpdate.createGroup(internalGroupCreation, internalGroupUpdate);
    return internalGroup.getGroupUUID();
  }

  private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation) {
    AccountGroup.Id groupId = AccountGroup.id(seq.nextGroupId());
    String groupName = groupCreation.name().orElse("group-with-id-" + groupId.get());
    AccountGroup.UUID groupUuid = GroupUuid.make(groupName, serverIdent);
    AccountGroup.NameKey nameKey = AccountGroup.nameKey(groupName);
    return InternalGroupCreation.builder()
        .setId(groupId)
        .setGroupUUID(groupUuid)
        .setNameKey(nameKey)
        .build();
  }

  private static InternalGroupUpdate toInternalGroupUpdate(TestGroupCreation groupCreation) {
    InternalGroupUpdate.Builder builder = InternalGroupUpdate.builder();
    groupCreation.description().ifPresent(builder::setDescription);
    groupCreation.ownerGroupUuid().ifPresent(builder::setOwnerGroupUUID);
    groupCreation.visibleToAll().ifPresent(builder::setVisibleToAll);
    builder.setMemberModification(originalMembers -> groupCreation.members());
    builder.setSubgroupModification(originalSubgroups -> groupCreation.subgroups());
    return builder.build();
  }

  private class PerGroupOperationsImpl implements PerGroupOperations {
    private final AccountGroup.UUID groupUuid;

    PerGroupOperationsImpl(AccountGroup.UUID groupUuid) {
      this.groupUuid = groupUuid;
    }

    @Override
    public boolean exists() {
      return getGroup(groupUuid).isPresent();
    }

    @Override
    public TestGroup get() {
      Optional<InternalGroup> group = getGroup(groupUuid);
      checkState(group.isPresent(), "Tried to get non-existing test group");
      return toTestGroup(group.get());
    }

    private Optional<InternalGroup> getGroup(AccountGroup.UUID groupUuid) {
      try {
        return groups.getGroup(groupUuid);
      } catch (IOException | ConfigInvalidException e) {
        throw new IllegalStateException(e);
      }
    }

    private TestGroup toTestGroup(InternalGroup internalGroup) {
      return TestGroup.builder()
          .groupUuid(internalGroup.getGroupUUID())
          .groupId(internalGroup.getId())
          .nameKey(internalGroup.getNameKey())
          .description(Optional.ofNullable(internalGroup.getDescription()))
          .ownerGroupUuid(internalGroup.getOwnerGroupUUID())
          .visibleToAll(internalGroup.isVisibleToAll())
          .createdOn(internalGroup.getCreatedOn())
          .members(internalGroup.getMembers())
          .subgroups(internalGroup.getSubgroups())
          .build();
    }

    @Override
    public TestGroupUpdate.Builder forUpdate() {
      return TestGroupUpdate.builder(this::updateGroup);
    }

    private void updateGroup(TestGroupUpdate groupUpdate)
        throws DuplicateKeyException, NoSuchGroupException, ConfigInvalidException, IOException {
      InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupUpdate);
      groupsUpdate.updateGroup(groupUuid, internalGroupUpdate);
    }

    private InternalGroupUpdate toInternalGroupUpdate(TestGroupUpdate groupUpdate) {
      InternalGroupUpdate.Builder builder = InternalGroupUpdate.builder();
      groupUpdate.name().map(AccountGroup::nameKey).ifPresent(builder::setName);
      groupUpdate.description().ifPresent(builder::setDescription);
      groupUpdate.ownerGroupUuid().ifPresent(builder::setOwnerGroupUUID);
      groupUpdate.visibleToAll().ifPresent(builder::setVisibleToAll);
      builder.setMemberModification(groupUpdate.memberModification()::apply);
      builder.setSubgroupModification(groupUpdate.subgroupModification()::apply);
      return builder.build();
    }
  }
}
