// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.tools;

import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.getHistoryEntriesOfType;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getRegistryLockByRevisionId;
import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCode;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.batch.RelockDomainAction;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.RegistryLock;
import google.registry.model.host.Host;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.SqlHelper;
import google.registry.util.StringGenerator.Alphabets;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

/** Unit tests for {@link google.registry.tools.DomainLockUtils}. */
public final class DomainLockUtilsTest {

  private static final String DOMAIN_NAME = "example.tld";
  private static final String POC_ID = "marla.singer@example.com";

  private static final User NON_ADMIN_USER =
      new User.Builder()
          .setEmailAddress("user@theregistrar.com")
          .setUserRoles(
              new UserRoles.Builder()
                  .setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
                  .build())
          .build();

  private static final User ADMIN_USER =
      new User.Builder()
          .setEmailAddress("admin@theregistrar.com")
          .setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
          .build();

  private final FakeClock clock = new FakeClock(Instant.now());
  private DomainLockUtils domainLockUtils;
  private CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(clock);

  @RegisterExtension
  final JpaIntegrationTestExtension jpa =
      new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();

  private Domain domain;

  @BeforeEach
  void setup() {
    createTlds("tld", "net");
    Host host = persistActiveHost("ns1.example.net");
    domain = persistResource(DatabaseHelper.newDomain(DOMAIN_NAME, host));

    domainLockUtils =
        new DomainLockUtils(
            new DeterministicStringGenerator(Alphabets.BASE_58),
            "adminreg",
            cloudTasksHelper.getTestCloudTasksUtils());
  }

  @Test
  void testSuccess_createLock() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    assertNoDomainChanges();
    assertThat(lock.getLockCompletionTime().isPresent()).isFalse();
  }

  @Test
  void testSuccess_createUnlock() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock lock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    assertThat(lock.getUnlockCompletionTime().isPresent()).isFalse();
  }

  @Test
  void testSuccess_createUnlock_adminUnlockingAdmin() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", null, true);
    RegistryLock lock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
    assertThat(lock.getUnlockCompletionTime().isPresent()).isFalse();
  }

  @Test
  void testSuccess_createLock_previousLockExpired() {
    domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    clock.advanceBy(Duration.ofDays(1));
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    verifyProperlyLockedDomain(false);
  }

  @Test
  void testSuccess_createUnlock_previousUnlockRequestExpired() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.saveNewRegistryUnlockRequest(
        DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    clock.advanceBy(Duration.ofDays(1));
    RegistryLock unlockRequest =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    domainLockUtils.verifyVerificationCode(unlockRequest.getVerificationCode(), NON_ADMIN_USER);
    assertThat(loadByEntity(domain).getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES);
  }

  @Test
  void testSuccess_applyLockDomain() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    verifyProperlyLockedDomain(false);
  }

  @Test
  void testSuccess_applyUnlockDomain() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock unlock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), NON_ADMIN_USER);
    verifyProperlyUnlockedDomain(false);
  }

  @Test
  void testSuccess_applyAdminLock_onlyHistoryEntry() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
    verifyProperlyLockedDomain(true);
  }

  @Test
  void testSuccess_applyAdminUnlock_onlyHistoryEntry() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
    RegistryLock unlock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
    domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), ADMIN_USER);
    verifyProperlyUnlockedDomain(true);
  }

  @Test
  void testSuccess_administrativelyLock_nonAdmin() {
    domainLockUtils.administrativelyApplyLock(
        DOMAIN_NAME, "TheRegistrar", "Marla.Singer@crr.com", false);
    verifyProperlyLockedDomain(false);
  }

  @Test
  void testSuccess_administrativelyLock_admin() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", null, true);
    verifyProperlyLockedDomain(true);
  }

  @Test
  void testSuccess_administrativelyUnlock_nonAdmin() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    domainLockUtils.administrativelyApplyUnlock(
        DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    verifyProperlyUnlockedDomain(false);
  }

  @Test
  void testSuccess_administrativelyUnlock_admin() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
    domainLockUtils.administrativelyApplyUnlock(
        DOMAIN_NAME, "TheRegistrar", true, Optional.empty());
    verifyProperlyUnlockedDomain(true);
  }

  @Test
  void testSuccess_regularLock_relockSet() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock oldLock =
        domainLockUtils.administrativelyApplyUnlock(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    RegistryLock newLock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    newLock = domainLockUtils.verifyVerificationCode(newLock.getVerificationCode(), NON_ADMIN_USER);
    assertThat(
            getRegistryLockByRevisionId(oldLock.getRevisionId()).get().getRelock().getRevisionId())
        .isEqualTo(newLock.getRevisionId());
  }

  @Test
  void testSuccess_administrativelyLock_relockSet() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock oldLock =
        domainLockUtils.administrativelyApplyUnlock(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    RegistryLock newLock =
        domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    assertThat(
            getRegistryLockByRevisionId(oldLock.getRevisionId()).get().getRelock().getRevisionId())
        .isEqualTo(newLock.getRevisionId());
  }

  @Test
  void testSuccess_createUnlock_relockDuration() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock lock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.of(Duration.ofDays(1)));
    assertThat(lock.getRelockDuration()).hasValue(Duration.ofDays(1));
  }

  @Test
  void testSuccess_unlock_relockSubmitted() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock lock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.of(Duration.ofHours(6)));
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    cloudTasksHelper.assertTasksEnqueued(
        QUEUE_ASYNC_ACTIONS,
        new TaskMatcher()
            .path(RelockDomainAction.PATH)
            .method(HttpMethod.POST)
            .service("backend")
            .param(
                RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
                String.valueOf(lock.getRevisionId()))
            .param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, "0")
            .scheduleTime(clock.now().plus(lock.getRelockDuration().get())));
  }

  @Test
  void testSuccess_adminCanLockLockedDomain_withNoSavedLock() {
    // in the case of inconsistencies / errors, admins should have the ability to override
    // whatever statuses exist on the domain
    persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
    RegistryLock resultLock =
        domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, true);
    verifyProperlyLockedDomain(true);
    assertThat(resultLock.getLockCompletionTime()).hasValue(clock.now());
  }

  @Test
  void testSuccess_adminCanLockUnlockedDomain_withSavedLock() {
    // in the case of inconsistencies / errors, admins should have the ability to override
    // what the RegistryLock table says
    SqlHelper.saveRegistryLock(
        new RegistryLock.Builder()
            .setLockCompletionTime(clock.now())
            .setDomainName(DOMAIN_NAME)
            .setVerificationCode("hi")
            .setRegistrarId("TheRegistrar")
            .setRepoId(domain.getRepoId())
            .isSuperuser(false)
            .setRegistryLockEmail(POC_ID)
            .build());
    clock.advanceOneMilli();
    RegistryLock resultLock =
        domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, true);
    verifyProperlyLockedDomain(true);
    assertThat(resultLock.getLockCompletionTime()).hasValue(clock.now());
  }

  @Test
  void testFailure_createUnlock_alreadyPendingUnlock() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    domainLockUtils.saveNewRegistryUnlockRequest(
        DOMAIN_NAME, "TheRegistrar", false, Optional.empty());

    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.saveNewRegistryUnlockRequest(
                    DOMAIN_NAME, "TheRegistrar", false, Optional.empty()));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("A pending unlock action already exists for example.tld");
  }

  @Test
  void testFailure_createUnlock_nonAdminUnlockingAdmin() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER);
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.saveNewRegistryUnlockRequest(
                    DOMAIN_NAME, "TheRegistrar", false, Optional.empty()));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Non-admin user cannot unlock admin-locked domain example.tld");
  }

  @Test
  void testFailure_createLock_unknownDomain() {
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.saveNewRegistryLockRequest(
                    "asdf.tld", "TheRegistrar", POC_ID, false));
    assertThat(thrown).hasMessageThat().isEqualTo("Domain doesn't exist");
  }

  @Test
  void testFailure_createLock_alreadyPendingLock() {
    domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.saveNewRegistryLockRequest(
                    DOMAIN_NAME, "TheRegistrar", POC_ID, false));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("A pending or completed lock action already exists for example.tld");
  }

  @Test
  void testFailure_createLock_alreadyLocked() {
    persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.saveNewRegistryLockRequest(
                    DOMAIN_NAME, "TheRegistrar", POC_ID, false));
    assertThat(thrown).hasMessageThat().isEqualTo("Domain example.tld is already locked");
  }

  @Test
  void testFailure_createUnlock_alreadyUnlocked() {
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.saveNewRegistryUnlockRequest(
                    DOMAIN_NAME, "TheRegistrar", false, Optional.empty()));
    assertThat(thrown).hasMessageThat().isEqualTo("Domain example.tld is already unlocked");
  }

  @Test
  void testFailure_applyLock_alreadyApplied() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    domain = loadByEntity(domain);
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Lock/unlock with code 123456789ABCDEFGHJKLMNPQRSTUVWXY is already completed");
    assertNoDomainChanges();
  }

  @Test
  void testFailure_applyLock_expired() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    clock.advanceBy(Duration.ofDays(1));
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () -> domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), ADMIN_USER));
    assertThat(thrown).hasMessageThat().isEqualTo("The pending lock has expired; please try again");
    assertNoDomainChanges();
  }

  @Test
  void testFailure_applyLock_nonAdmin_applyAdminLock() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", null, true);
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER));
    assertThat(thrown).hasMessageThat().isEqualTo("Non-admin user cannot complete admin lock");
    assertNoDomainChanges();
  }

  @Test
  void testFailure_applyLock_noPermission() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    User userWithoutPermission =
        new User.Builder()
            .setEmailAddress("unauthorized@example.com")
            .setUserRoles(new UserRoles.Builder().build())
            .build();
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(
                    lock.getVerificationCode(), userWithoutPermission));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            "User unauthorized@example.com does not have registry lock permission on registrar"
                + " TheRegistrar");
    assertNoDomainChanges();
  }

  @Test
  void testFailure_applyUnlock_noPermission() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock unlock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    User userWithoutPermission =
        new User.Builder()
            .setEmailAddress("unauthorized@example.com")
            .setUserRoles(new UserRoles.Builder().build())
            .build();
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(
                    unlock.getVerificationCode(), userWithoutPermission));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            "User unauthorized@example.com does not have registry lock permission on registrar"
                + " TheRegistrar");
  }

  @Test
  void testFailure_applyUnlock_alreadyUnlocked() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    domainLockUtils.verifyVerificationCode(lock.getVerificationCode(), NON_ADMIN_USER);
    RegistryLock unlock =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    domainLockUtils.verifyVerificationCode(unlock.getVerificationCode(), NON_ADMIN_USER);

    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(
                    unlock.getVerificationCode(), NON_ADMIN_USER));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Lock/unlock with code Zabcdefghijkmnopqrstuvwxyz123456 is already completed");
    assertNoDomainChanges();
  }

  @Test
  void testFailure_applyUnlock_supersededByNewerLock() {
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    RegistryLock unlockRequest =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", false, Optional.empty());
    clock.advanceOneMilli();
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", null, true);

    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(
                    unlockRequest.getVerificationCode(), NON_ADMIN_USER));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            "The pending unlock on example.tld has been superseded; please request a new unlock");
  }

  @Test
  void testFailure_applyUnlock_supersededByInterleavedUnlockAndLock() {
    // Lock the domain initially
    domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, "TheRegistrar", null, true);

    // 1. Unlock A requested
    clock.advanceOneMilli();
    RegistryLock unlockA =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", true, Optional.empty());

    // 2. Unlock B requested
    clock.advanceOneMilli();
    RegistryLock unlockB =
        domainLockUtils.saveNewRegistryUnlockRequest(
            DOMAIN_NAME, "TheRegistrar", true, Optional.empty());

    // 3. Unlock B completed
    clock.advanceOneMilli();
    domainLockUtils.verifyVerificationCode(unlockB.getVerificationCode(), ADMIN_USER);

    // 4. Lock C requested
    clock.advanceOneMilli();
    RegistryLock lockC =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, true);

    // 5. Lock C applied
    clock.advanceOneMilli();
    domainLockUtils.verifyVerificationCode(lockC.getVerificationCode(), ADMIN_USER);

    // 6. Unlock A completion attempt should fail since it has been superseded -- the second unlock
    // request rewrote the verification code
    clock.advanceOneMilli();
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                domainLockUtils.verifyVerificationCode(unlockA.getVerificationCode(), ADMIN_USER));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            String.format("Invalid verification code \"%s\"", unlockA.getVerificationCode()));
  }

  @Test
  void testFailure_applyLock_alreadyLocked() {
    RegistryLock lock =
        domainLockUtils.saveNewRegistryLockRequest(DOMAIN_NAME, "TheRegistrar", POC_ID, false);
    String verificationCode = lock.getVerificationCode();
    // reload to pick up modification times, etc
    lock = getRegistryLockByVerificationCode(verificationCode).get();
    domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () -> domainLockUtils.verifyVerificationCode(verificationCode, NON_ADMIN_USER));
    assertThat(thrown).hasMessageThat().isEqualTo("Domain example.tld is already locked");

    // Failure during the lock acquisition portion shouldn't affect the SQL object
    RegistryLock afterAction = getRegistryLockByVerificationCode(lock.getVerificationCode()).get();
    assertThat(afterAction).isEqualTo(lock);
    assertNoDomainChanges();
  }

  @Test
  void testEnqueueRelock() {
    RegistryLock lock =
        saveRegistryLock(
            new RegistryLock.Builder()
                .setLockCompletionTime(clock.now())
                .setUnlockRequestTime(clock.now())
                .setUnlockCompletionTime(clock.now())
                .isSuperuser(false)
                .setDomainName("example.tld")
                .setRepoId("repoId")
                .setRelockDuration(Duration.ofHours(6))
                .setRegistrarId("TheRegistrar")
                .setRegistryLockEmail("someone@example.com")
                .setVerificationCode("hi")
                .build());
    domainLockUtils.enqueueDomainRelock(lock.getRelockDuration().get(), lock.getRevisionId(), 0);
    cloudTasksHelper.assertTasksEnqueued(
        QUEUE_ASYNC_ACTIONS,
        new CloudTasksHelper.TaskMatcher()
            .path(RelockDomainAction.PATH)
            .method(HttpMethod.POST)
            .service("backend")
            .param(
                RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
                String.valueOf(lock.getRevisionId()))
            .param(RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM, "0")
            .scheduleTime(clock.now().plus(lock.getRelockDuration().get())));
  }

  @MockitoSettings(strictness = Strictness.LENIENT)
  @Test
  void testFailure_enqueueRelock_noDuration() {
    RegistryLock lockWithoutDuration =
        saveRegistryLock(
            new RegistryLock.Builder()
                .isSuperuser(false)
                .setDomainName("example.tld")
                .setRepoId("repoId")
                .setRegistrarId("TheRegistrar")
                .setRegistryLockEmail("someone@example.com")
                .setVerificationCode("hi")
                .build());
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () -> domainLockUtils.enqueueDomainRelock(lockWithoutDuration));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            String.format(
                "Lock with ID %s not configured for relock", lockWithoutDuration.getRevisionId()));
  }

  private void verifyProperlyLockedDomain(boolean isAdmin) {
    assertThat(loadByEntity(domain).getStatusValues())
        .containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
    DomainHistory historyEntry =
        getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE, DomainHistory.class);
    assertThat(historyEntry.getRequestedByRegistrar()).isEqualTo(!isAdmin);
    assertThat(historyEntry.getBySuperuser()).isEqualTo(isAdmin);
    assertThat(historyEntry.getReason())
        .isEqualTo("Lock of a domain through a RegistryLock operation");
    if (isAdmin) {
      assertNoBillingEvents();
    } else {
      assertBillingEvent(historyEntry);
    }
  }

  private void verifyProperlyUnlockedDomain(boolean isAdmin) {
    assertThat(loadByEntity(domain).getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES);
    ImmutableList<DomainHistory> historyEntries =
        getHistoryEntriesOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE, DomainHistory.class);
    assertThat(historyEntries).hasSize(2);
    historyEntries.forEach(
        entry -> {
          assertThat(entry.getRequestedByRegistrar()).isEqualTo(!isAdmin);
          assertThat(entry.getBySuperuser()).isEqualTo(isAdmin);
          assertThat(entry.getReason())
              .contains("ock of a domain through a RegistryLock operation");
        });
    if (isAdmin) {
      assertNoBillingEvents();
    } else {
      assertBillingEvents(historyEntries);
    }
  }

  private void assertNoDomainChanges() {
    assertThat(loadByEntity(domain)).isEqualTo(domain);
  }

  private void assertBillingEvent(DomainHistory historyEntry) {
    assertBillingEvents(ImmutableList.of(historyEntry));
  }

  private void assertBillingEvents(ImmutableList<DomainHistory> historyEntries) {
    Set<BillingBase> expectedEvents =
        historyEntries.stream()
            .map(
                entry ->
                    new BillingEvent.Builder()
                        .setReason(Reason.SERVER_STATUS)
                        .setTargetId(domain.getForeignKey())
                        .setRegistrarId(domain.getCurrentSponsorRegistrarId())
                        .setCost(Tld.get(domain.getTld()).getRegistryLockOrUnlockBillingCost())
                        .setEventTime(clock.now())
                        .setBillingTime(clock.now())
                        .setDomainHistory(entry)
                        .build())
            .collect(Collectors.toSet());
    DatabaseHelper.assertBillingEvents(expectedEvents);
  }
}
