// Copyright 2017 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.flows.domain;

import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESAVE_TIMES;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.flows.domain.DomainTransferFlowTestCase.persistWithPendingTransfer;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.DELETED_DOMAINS_GRACE;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.DELETED_DOMAINS_NOGRACE;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.NET_ADDS_10_YR;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.NET_ADDS_1_YR;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.NET_RENEWS_3_YR;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.RESTORED_DOMAINS;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST;
import static google.registry.model.tld.Tld.TldState.PREDELEGATION;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertDomainDnsRequests;
import static google.registry.testing.DatabaseHelper.assertPollMessages;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadByKeyIfPresent;
import static google.registry.testing.DatabaseHelper.loadByKeysIfPresent;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newHost;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries;
import static google.registry.util.DateTimeUtils.END_INSTANT;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static google.registry.util.DateTimeUtils.minusDays;
import static google.registry.util.DateTimeUtils.minusMonths;
import static google.registry.util.DateTimeUtils.minusYears;
import static google.registry.util.DateTimeUtils.plusDays;
import static google.registry.util.DateTimeUtils.plusHours;
import static google.registry.util.DateTimeUtils.plusMonths;
import static google.registry.util.DateTimeUtils.plusYears;
import static org.joda.money.CurrencyUnit.USD;
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 com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.batch.ResaveEntityAction;
import google.registry.flows.EppException;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppRequestSource;
import google.registry.flows.FlowUtils.NotLoggedInException;
import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.domain.DomainDeleteFlow.DomainToDeleteHasHostsException;
import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.LogsSubject;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.logging.Level;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Unit tests for {@link DomainDeleteFlow}. */
class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain> {

  private Domain domain;
  private DomainHistory earlierHistoryEntry;

  private static final Instant TIME_BEFORE_FLOW = Instant.parse("2000-06-06T22:00:00.0Z");
  private static final Instant A_MONTH_AGO = minusMonths(TIME_BEFORE_FLOW, 1);
  private static final Instant A_MONTH_FROM_NOW = plusMonths(TIME_BEFORE_FLOW, 1);

  private static final ImmutableMap<String, String> FEE_06_MAP =
      ImmutableMap.of("FEE_VERSION", "fee-0.6", "FEE_NS", "fee");
  private static final ImmutableMap<String, String> FEE_11_MAP =
      ImmutableMap.of("FEE_VERSION", "fee-0.11", "FEE_NS", "fee11");
  private static final ImmutableMap<String, String> FEE_12_MAP =
      ImmutableMap.of("FEE_VERSION", "fee-0.12", "FEE_NS", "fee12");
  private static final ImmutableMap<String, String> FEE_STD_1_0_MAP =
      ImmutableMap.of("FEE_VERSION", "epp:fee-1.0", "FEE_NS", "fee1_00");

  DomainDeleteFlowTest() {
    setEppInput("domain_delete.xml");
    clock.setTo(TIME_BEFORE_FLOW);
  }

  @BeforeEach
  void initDomainTest() {
    createTld("tld");
  }

  private void setUpSuccessfulTest() throws Exception {
    createReferencedEntities(A_MONTH_FROM_NOW);
    BillingRecurrence autorenewBillingEvent =
        persistResource(createAutorenewBillingEvent("TheRegistrar").build());
    PollMessage.Autorenew autorenewPollMessage =
        persistResource(createAutorenewPollMessage("TheRegistrar").build());

    domain =
        persistResource(
            domain
                .asBuilder()
                .setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
                .setAutorenewPollMessage(autorenewPollMessage.createVKey())
                .build());

    assertMutatingFlow(true);
  }

  private void createReferencedEntities(Instant expirationTime) throws Exception {
    domain =
        persistResource(
            DatabaseHelper.newDomain(getUniqueIdFromCommand())
                .asBuilder()
                .setCreationTimeForTest(TIME_BEFORE_FLOW)
                .setRegistrationExpirationTime(expirationTime)
                .build());
    earlierHistoryEntry =
        persistResource(
            new DomainHistory.Builder()
                .setType(DOMAIN_CREATE)
                .setDomain(domain)
                .setModificationTime(clock.now())
                .setRegistrarId(domain.getCreationRegistrarId())
                .build());
  }

  private void setUpGracePeriods(GracePeriod... gracePeriods) {
    domain =
        persistResource(
            domain.asBuilder().setGracePeriods(ImmutableSet.copyOf(gracePeriods)).build());
  }

  private void setUpGracePeriodDurations() {
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setAddGracePeriodLength(Duration.ofDays(3))
            .setRenewGracePeriodLength(Duration.ofDays(2))
            .setAutoRenewGracePeriodLength(Duration.ofDays(1))
            .setRedemptionGracePeriodLength(Duration.ofHours(1))
            .setPendingDeleteLength(Duration.ofHours(2))
            .build());
  }

  private void setUpAutorenewGracePeriod() throws Exception {
    createReferencedEntities(plusYears(A_MONTH_AGO, 1));
    BillingRecurrence autorenewBillingEvent =
        persistResource(
            createAutorenewBillingEvent("TheRegistrar").setEventTime(A_MONTH_AGO).build());
    PollMessage.Autorenew autorenewPollMessage =
        persistResource(
            createAutorenewPollMessage("TheRegistrar").setEventTime(A_MONTH_AGO).build());
    domain =
        persistResource(
            domain
                .asBuilder()
                .setGracePeriods(
                    ImmutableSet.of(
                        GracePeriod.createForRecurrence(
                            GracePeriodStatus.AUTO_RENEW,
                            domain.getRepoId(),
                            plusDays(A_MONTH_AGO, 45),
                            "TheRegistrar",
                            autorenewBillingEvent.createVKey())))
                .setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
                .setAutorenewPollMessage(autorenewPollMessage.createVKey())
                .build());
    assertMutatingFlow(true);
  }

  private void assertAutorenewClosedAndCancellationCreatedFor(
      BillingEvent graceBillingEvent, DomainHistory historyEntryDomainDelete) {
    assertAutorenewClosedAndCancellationCreatedFor(
        graceBillingEvent, historyEntryDomainDelete, clock.now());
  }

  private void assertAutorenewClosedAndCancellationCreatedFor(
      BillingEvent graceBillingEvent, DomainHistory historyEntryDomainDelete, Instant eventTime) {
    assertBillingEvents(
        createAutorenewBillingEvent("TheRegistrar").setRecurrenceEndTime(eventTime).build(),
        graceBillingEvent,
        new BillingCancellation.Builder()
            .setReason(graceBillingEvent.getReason())
            .setTargetId("example.tld")
            .setRegistrarId("TheRegistrar")
            .setEventTime(eventTime)
            .setBillingTime(plusDays(TIME_BEFORE_FLOW, 1))
            .setBillingEvent(graceBillingEvent.createVKey())
            .setDomainHistory(historyEntryDomainDelete)
            .build());
  }

  private void assertOnlyBillingEventIsClosedAutorenew(String registrarId) {
    // There should be no billing events (even timed to when the transfer would have expired) except
    // for the now closed autorenew one.
    assertBillingEvents(
        createAutorenewBillingEvent(registrarId).setRecurrenceEndTime(clock.now()).build());
  }

  private BillingEvent createBillingEvent(Reason reason, Money cost) {
    return new BillingEvent.Builder()
        .setReason(reason)
        .setTargetId("example.tld")
        .setRegistrarId("TheRegistrar")
        .setCost(cost)
        .setPeriodYears(2)
        .setEventTime(minusDays(TIME_BEFORE_FLOW, 4))
        .setBillingTime(plusDays(TIME_BEFORE_FLOW, 1))
        .setDomainHistory(earlierHistoryEntry)
        .build();
  }

  private BillingRecurrence.Builder createAutorenewBillingEvent(String registrarId) {
    return new BillingRecurrence.Builder()
        .setReason(Reason.RENEW)
        .setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
        .setTargetId("example.tld")
        .setRegistrarId(registrarId)
        .setEventTime(A_MONTH_FROM_NOW)
        .setRecurrenceEndTime(END_INSTANT)
        .setDomainHistory(earlierHistoryEntry);
  }

  private PollMessage.Autorenew.Builder createAutorenewPollMessage(String registrarId) {
    return new PollMessage.Autorenew.Builder()
        .setTargetId("example.tld")
        .setRegistrarId(registrarId)
        .setEventTime(A_MONTH_FROM_NOW)
        .setAutorenewEndTime(END_INSTANT)
        .setHistoryEntry(earlierHistoryEntry);
  }

  @Test
  void testNotLoggedIn() {
    sessionMetadata.setRegistrarId(null);
    EppException thrown = assertThrows(NotLoggedInException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_asyncActionsAreEnqueued() throws Exception {
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setRedemptionGracePeriodLength(Duration.ofDays(3))
            .setPendingDeleteLength(Duration.ofDays(2))
            .build());
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    cloudTasksHelper.assertTasksEnqueued(
        QUEUE_ASYNC_ACTIONS,
        new TaskMatcher()
            .path(ResaveEntityAction.PATH)
            .method(HttpMethod.POST)
            .service("backend")
            .header("content-type", "application/x-www-form-urlencoded")
            .param(PARAM_RESOURCE_KEY, domain.createVKey().stringify())
            .param(PARAM_REQUESTED_TIME, clock.now().toString())
            .param(PARAM_RESAVE_TIMES, plusDays(clock.now(), 5).toString())
            .scheduleTime(clock.now().plus(Duration.ofDays(3))));
  }

  @Test
  void testDryRun() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriods(
        GracePeriod.create(
            GracePeriodStatus.ADD,
            domain.getRepoId(),
            plusDays(TIME_BEFORE_FLOW, 1),
            "TheRegistrar",
            null));
    dryRunFlowAssertResponse(loadFile("generic_success_response.xml"));
  }

  @Test
  void testDryRun_noGracePeriods() throws Exception {
    setUpSuccessfulTest();
    dryRunFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
  }

  private void doAddGracePeriodDeleteTest(
      GracePeriodStatus gracePeriodStatus, String responseFilename) throws Exception {
    doAddGracePeriodDeleteTest(gracePeriodStatus, responseFilename, ImmutableMap.of());
  }

  private void doAddGracePeriodDeleteTest(
      GracePeriodStatus gracePeriodStatus,
      String responseFilename,
      Map<String, String> substitutions)
      throws Exception {
    // Persist the billing event so it can be retrieved for cancellation generation and checking.
    setUpSuccessfulTest();
    BillingEvent graceBillingEvent =
        persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 123)));
    setUpGracePeriods(
        GracePeriod.forBillingEvent(gracePeriodStatus, domain.getRepoId(), graceBillingEvent));
    // We should see exactly one poll message, which is for the autorenew 1 month in the future.
    assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile(responseFilename, substitutions));
    // Check that the domain is fully deleted.
    assertThat(reloadResourceByForeignKey()).isNull();
    // The add grace period is for a billable action, so it should trigger a cancellation.
    assertAutorenewClosedAndCancellationCreatedFor(
        graceBillingEvent, getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE, DomainHistory.class));
    assertDomainDnsRequests("example.tld");
    // There should be no poll messages. The previous autorenew poll message should now be deleted.
    assertThat(getPollMessages("TheRegistrar")).isEmpty();
  }

  @Test
  void testSuccess_updatedEppUpdateTimeAfterPendingRedemption() throws Exception {
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setRedemptionGracePeriodLength(Duration.ofDays(3))
            .setPendingDeleteLength(Duration.ofDays(2))
            .build());
    setRegistrarIdForFlow("TheRegistrar");
    setUpSuccessfulTest();
    clock.advanceOneMilli();

    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));

    Domain domain = reloadResourceByForeignKey();
    Instant redemptionEndTime = plusDays(domain.getLastEppUpdateTime(), 3);
    Domain domainAtRedemptionTime = domain.cloneProjectedAtTime(redemptionEndTime);
    assertAboutDomains()
        .that(domainAtRedemptionTime)
        .hasLastEppUpdateRegistrarId("TheRegistrar")
        .and()
        .hasLastEppUpdateTime(redemptionEndTime);
  }

  @Test
  void testSuccess_addGracePeriodResultsInImmediateDelete() throws Exception {
    sessionMetadata.setServiceExtensionUris(ImmutableSet.of());
    doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "generic_success_response.xml");
  }

  @Test
  void testSuccess_addGracePeriodCredit_std_v1() throws Exception {
    doAddGracePeriodDeleteTest(
        GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_STD_1_0_MAP);
  }

  @Test
  void testSuccess_addGracePeriodDelete_doesNotRefundXapFee() throws Exception {
    setUpSuccessfulTest();
    BillingEvent createBillingEvent =
        persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 123)));
    BillingEvent xapBillingEvent =
        persistResource(createBillingEvent(Reason.FEE_EXPIRY_ACCESS, Money.of(USD, 100)));
    setUpGracePeriods(
        GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), createBillingEvent));
    assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_fee.xml", FEE_STD_1_0_MAP));
    assertThat(reloadResourceByForeignKey()).isNull();
    DomainHistory historyEntryDomainDelete =
        getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE, DomainHistory.class);
    assertBillingEvents(
        createAutorenewBillingEvent("TheRegistrar").setRecurrenceEndTime(clock.now()).build(),
        createBillingEvent,
        xapBillingEvent,
        new BillingCancellation.Builder()
            .setReason(Reason.CREATE)
            .setTargetId("example.tld")
            .setRegistrarId("TheRegistrar")
            .setEventTime(clock.now())
            .setBillingTime(plusDays(TIME_BEFORE_FLOW, 1))
            .setBillingEvent(createBillingEvent.createVKey())
            .setDomainHistory(historyEntryDomainDelete)
            .build());
  }

  private void doSuccessfulTest_noAddGracePeriod(String responseFilename) throws Exception {
    doSuccessfulTest_noAddGracePeriod(responseFilename, ImmutableMap.of());
  }

  private void doSuccessfulTest_noAddGracePeriod(
      String responseFilename, Map<String, String> substitutions) throws Exception {
    // Persist the billing event so it can be retrieved for cancellation generation and checking.
    setUpSuccessfulTest();
    BillingEvent renewBillingEvent =
        persistResource(createBillingEvent(Reason.RENEW, Money.of(USD, 456)));
    setUpGracePeriods(
        GracePeriod.forBillingEvent(GracePeriodStatus.RENEW, domain.getRepoId(), renewBillingEvent),
        // This grace period has no associated billing event, so it won't cause a cancellation.
        GracePeriod.create(
            GracePeriodStatus.TRANSFER,
            domain.getRepoId(),
            plusDays(TIME_BEFORE_FLOW, 1),
            "NewRegistrar",
            null));
    // We should see exactly one poll message, which is for the autorenew 1 month in the future.
    assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
    Instant expectedExpirationTime = minusYears(domain.getRegistrationExpirationTime(), 2);
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile(responseFilename, substitutions));
    Domain resource = reloadResourceByForeignKey();
    // Check that the domain is in the pending delete state.
    assertAboutDomains()
        .that(resource)
        .hasStatusValue(StatusValue.PENDING_DELETE)
        .and()
        .hasDeletionTime(
            clock
                .now()
                .plus(Tld.get("tld").getRedemptionGracePeriodLength())
                .plus(Tld.get("tld").getPendingDeleteLength()))
        .and()
        .hasExactlyStatusValues(StatusValue.INACTIVE, StatusValue.PENDING_DELETE)
        .and()
        .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_DELETE);
    // We leave the original expiration time unchanged; if the expiration time is before the
    // deletion time, that means once it passes the domain will experience a "phantom autorenew"
    // where the expirationTime advances and the grace period appears, but since the delete flow
    // closed the autorenew recurrences immediately, there are no other autorenew effects.
    assertAboutDomains().that(resource).hasRegistrationExpirationTime(expectedExpirationTime);
    assertLastHistoryContainsResource(resource);
    // All existing grace periods that were for billable actions should cause cancellations.
    assertAutorenewClosedAndCancellationCreatedFor(
        renewBillingEvent, getOnlyHistoryEntryOfType(resource, DOMAIN_DELETE, DomainHistory.class));
    // All existing grace periods should be gone, and a new REDEMPTION one should be added.
    assertThat(resource.getGracePeriods())
        .containsExactly(
            GracePeriod.create(
                GracePeriodStatus.REDEMPTION,
                domain.getRepoId(),
                clock.now().plus(Tld.get("tld").getRedemptionGracePeriodLength()),
                "TheRegistrar",
                null,
                resource.getGracePeriods().iterator().next().getGracePeriodId()));
    assertDeletionPollMessageFor(resource, "Domain deleted.");
  }

  private void assertDeletionPollMessageFor(Domain domain, String expectedMessage) {
    // There should be a future poll message at the deletion time. The previous autorenew poll
    // message should now be deleted.
    assertAboutDomains().that(domain).hasDeletePollMessage();
    Instant deletionTime = domain.getDeletionTime();
    assertThat(getPollMessages("TheRegistrar", deletionTime.minus(Duration.ofMinutes(1))))
        .isEmpty();
    assertThat(getPollMessages("TheRegistrar", deletionTime)).hasSize(1);
    assertThat(domain.getDeletePollMessage())
        .isEqualTo(getOnlyPollMessage("TheRegistrar").createVKey());
    PollMessage.OneTime deletePollMessage = loadByKey(domain.getDeletePollMessage());
    assertThat(deletePollMessage.getMsg()).isEqualTo(expectedMessage);
  }

  @Test
  void testSuccess_noAddGracePeriodResultsInPendingDelete() throws Exception {
    sessionMetadata.setServiceExtensionUris(ImmutableSet.of());
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending.xml");
  }

  @Test
  void testSuccess_renewGracePeriodCredit_std_v1() throws Exception {
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_STD_1_0_MAP);
  }

  @Test
  void testSuccess_autorenewPollMessageIsNotDeleted() throws Exception {
    setUpSuccessfulTest();
    // Modify the autorenew poll message so that it has unacked messages in the past. This should
    // prevent it from being deleted when the domain is deleted.
    persistResource(
        loadByKey(reloadResourceByForeignKey().getAutorenewPollMessage())
            .asBuilder()
            .setEventTime(minusYears(A_MONTH_FROM_NOW, 3))
            .build());
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    // There should now be two poll messages; one for the delete of the domain (in the future), and
    // another for the unacked autorenew messages.
    Instant deletionTime = reloadResourceByForeignKey().getDeletionTime();
    assertThat(getPollMessages("TheRegistrar", deletionTime.minus(Duration.ofMinutes(1))))
        .hasSize(1);
    assertThat(getPollMessages("TheRegistrar", deletionTime)).hasSize(2);
  }

  @Test
  void testSuccess_nonDefaultRedemptionGracePeriod() throws Exception {
    sessionMetadata.setServiceExtensionUris(ImmutableSet.of());
    persistResource(
        Tld.get("tld").asBuilder().setRedemptionGracePeriodLength(Duration.ofMinutes(7)).build());
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending.xml");
  }

  @Test
  void testSuccess_nonDefaultPendingDeleteLength() throws Exception {
    sessionMetadata.setServiceExtensionUris(ImmutableSet.of());
    persistResource(
        Tld.get("tld").asBuilder().setPendingDeleteLength(Duration.ofMinutes(8)).build());
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending.xml");
  }

  @Test
  void testSuccess_autoRenewGracePeriod_std_v1() throws Exception {
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_STD_1_0_MAP));
  }

  @Test
  void testSuccess_autoRenewGracePeriod_priceChanges_std_v1() throws Exception {
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setRenewBillingCostTransitions(
                ImmutableSortedMap.of(
                    START_INSTANT,
                    Money.of(USD, 11),
                    minusDays(TIME_BEFORE_FLOW, 5),
                    Money.of(USD, 20)))
            .build());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_STD_1_0_MAP));
  }

  @Test
  void testSuccess_noPendingTransfer_deletedAndHasNoTransferData() throws Exception {
    setRegistrarIdForFlow("TheRegistrar");
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    Domain domain = reloadResourceByForeignKey();
    assertThat(domain.getTransferData()).isEqualTo(DomainTransferData.EMPTY);
  }

  @Test
  void testSuccess_pendingTransfer() throws Exception {
    setRegistrarIdForFlow("TheRegistrar");
    setUpSuccessfulTest();
    // Modify the domain we are testing to include a pending transfer.
    DomainTransferData oldTransferData =
        persistWithPendingTransfer(reloadResourceByForeignKey()).getTransferData();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    Domain domain = reloadResourceByForeignKey();
    // Check that the domain is in the pending delete state.
    // The PENDING_TRANSFER status should be gone.
    assertAboutDomains()
        .that(domain)
        .hasExactlyStatusValues(StatusValue.INACTIVE, StatusValue.PENDING_DELETE)
        .and()
        .hasDeletionTime(
            clock
                .now()
                .plus(Tld.get("tld").getRedemptionGracePeriodLength())
                .plus(Tld.get("tld").getPendingDeleteLength()))
        .and()
        .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_TRANSFER_REQUEST, DOMAIN_DELETE);
    // All existing grace periods should be gone, and a new REDEMPTION one should be added.
    assertThat(domain.getGracePeriods())
        .containsExactly(
            GracePeriod.create(
                GracePeriodStatus.REDEMPTION,
                domain.getRepoId(),
                clock.now().plus(Tld.get("tld").getRedemptionGracePeriodLength()),
                "TheRegistrar",
                null,
                domain.getGracePeriods().iterator().next().getGracePeriodId()));
    // The poll message (in the future) to the losing registrar for implicit ack should be gone.
    assertThat(getPollMessages("TheRegistrar", clock.now().plus(Duration.ofDays(31)))).isEmpty();
    // The poll message in the future to the gaining registrar should be gone too, but there
    // should be one at the current time to the gaining registrar.
    PollMessage gainingPollMessage = getOnlyPollMessage("NewRegistrar");
    assertThat(gainingPollMessage.getEventTime()).isEqualTo(clock.now());
    assertThat(
            gainingPollMessage
                .getResponseData()
                .stream()
                .filter(TransferResponse.class::isInstance)
                .map(TransferResponse.class::cast)
                .collect(onlyElement())
                .getTransferStatus())
        .isEqualTo(TransferStatus.SERVER_CANCELLED);
    PendingActionNotificationResponse panData =
        gainingPollMessage
            .getResponseData()
            .stream()
            .filter(PendingActionNotificationResponse.class::isInstance)
            .map(PendingActionNotificationResponse.class::cast)
            .collect(onlyElement());
    assertThat(panData.getTrid())
        .isEqualTo(Trid.create("transferClient-trid", "transferServer-trid"));
    assertThat(panData.getActionResult()).isFalse();
    // There should be a future poll message to the losing registrar at the deletion time.
    Instant deletionTime = domain.getDeletionTime();
    assertThat(getPollMessages("TheRegistrar", deletionTime.minus(Duration.ofMinutes(1))))
        .isEmpty();
    assertThat(getPollMessages("TheRegistrar", deletionTime)).hasSize(1);
    assertOnlyBillingEventIsClosedAutorenew("TheRegistrar");
    // The domain TransferData should reflect the cancelled transfer as we expect, with
    // all the speculative server-approve fields nulled out.
    assertThat(domain.getTransferData())
        .isEqualTo(
            oldTransferData
                .copyConstantFieldsToBuilder()
                .setTransferStatus(TransferStatus.SERVER_CANCELLED)
                .setPendingTransferExpirationTime(clock.now())
                .build());
    // The server-approve entities should all be deleted.
    assertThat(loadByKeyIfPresent(oldTransferData.getServerApproveBillingEvent())).isEmpty();
    assertThat(loadByKeyIfPresent(oldTransferData.getServerApproveAutorenewEvent())).isEmpty();
    assertThat(loadByKeyIfPresent(oldTransferData.getServerApproveAutorenewPollMessage()))
        .isEmpty();
    assertThat(oldTransferData.getServerApproveEntities()).isNotEmpty(); // Just a sanity check.
    assertThat(loadByKeysIfPresent(oldTransferData.getServerApproveEntities())).isEmpty();
  }

  @Test
  void testUnlinkingOfResources() throws Exception {
    sessionMetadata.setServiceExtensionUris(ImmutableSet.of());
    setUpSuccessfulTest();
    // Persist the billing event so it can be retrieved for cancellation generation and checking.
    BillingEvent graceBillingEvent =
        persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 123)));
    // Use a grace period so that the delete is immediate, simplifying the assertions below.
    setUpGracePeriods(
        GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), graceBillingEvent));
    // Add a nameserver.
    Host host = persistResource(newHost("ns1.example.tld"));
    persistResource(
        ForeignKeyUtils.loadResource(Domain.class, getUniqueIdFromCommand(), clock.now())
            .get()
            .asBuilder()
            .setNameservers(ImmutableSet.of(host.createVKey()))
            .build());
    // Persist another domain that's already been deleted and references this  host.
    persistResource(
        DatabaseHelper.newDomain("example1.tld")
            .asBuilder()
            .setNameservers(ImmutableSet.of(host.createVKey()))
            .setDeletionTime(START_INSTANT)
            .build());
    Instant eventTime = clock.now();
    runFlowAssertResponse(loadFile("generic_success_response.xml"));
    assertDomainDnsRequests("example.tld");
    assertAutorenewClosedAndCancellationCreatedFor(
        graceBillingEvent,
        getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE, DomainHistory.class),
        eventTime);
  }

  @Test
  void testSuccess_deletedSubordinateDomain() throws Exception {
    setUpSuccessfulTest();
    persistResource(
        newHost("ns1." + getUniqueIdFromCommand())
            .asBuilder()
            .setSuperordinateDomain(reloadResourceByForeignKey().createVKey())
            .setDeletionTime(minusDays(clock.now(), 1))
            .build());
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    assertDomainDnsRequests("example.tld");
    assertOnlyBillingEventIsClosedAutorenew("TheRegistrar");
  }

  @Test
  void testFailure_predelegation() throws Exception {
    createTld("tld", PREDELEGATION);
    setUpSuccessfulTest();
    EppException thrown = assertThrows(BadCommandForRegistryPhaseException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_superuserPredelegation() throws Exception {
    createTld("tld", PREDELEGATION);
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
  }

  @Test
  void testFailure_neverExisted() throws Exception {
    ResourceDoesNotExistException thrown =
        assertThrows(ResourceDoesNotExistException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
  }

  @Test
  void testFailure_existedButWasDeleted() throws Exception {
    persistDeletedDomain(getUniqueIdFromCommand(), clock.now().minus(Duration.ofDays(1)));
    ResourceDoesNotExistException thrown =
        assertThrows(ResourceDoesNotExistException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains(String.format("(%s)", getUniqueIdFromCommand()));
  }

  @Test
  void testFailure_hasSubordinateHosts() throws Exception {
    Domain domain = persistActiveDomain(getUniqueIdFromCommand());
    Host subordinateHost =
        persistResource(
            newHost("ns1." + getUniqueIdFromCommand())
                .asBuilder()
                .setSuperordinateDomain(reloadResourceByForeignKey().createVKey())
                .build());
    persistResource(domain.asBuilder().addSubordinateHost(subordinateHost.getHostName()).build());
    EppException thrown = assertThrows(DomainToDeleteHasHostsException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_unauthorizedClient() throws Exception {
    sessionMetadata.setRegistrarId("NewRegistrar");
    persistActiveDomain(getUniqueIdFromCommand());
    EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_superuserUnauthorizedClient() throws Exception {
    sessionMetadata.setRegistrarId("NewRegistrar");
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));

    HistoryEntry deleteHistoryEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    Instant now = clock.now();
    assertPollMessages(
        new PollMessage.OneTime.Builder()
            .setRegistrarId("TheRegistrar")
            .setHistoryEntry(deleteHistoryEntry)
            .setEventTime(now)
            .setMsg(
                "Domain example.tld was deleted by registry administrator with final deletion"
                    + " effective: 2000-07-11T22:00:00.010Z")
            .setResponseData(
                ImmutableList.of(
                    DomainPendingActionNotificationResponse.create(
                        "example.tld", true, deleteHistoryEntry.getTrid(), now)))
            .build(),
        new PollMessage.OneTime.Builder()
            .setRegistrarId("TheRegistrar")
            .setHistoryEntry(deleteHistoryEntry)
            .setEventTime(Instant.parse("2000-07-11T22:00:00.010Z"))
            .setMsg("Deleted by registry administrator.")
            .setResponseData(
                ImmutableList.of(
                    DomainPendingActionNotificationResponse.create(
                        "example.tld",
                        true,
                        deleteHistoryEntry.getTrid(),
                        Instant.parse("2000-07-11T22:00:00.010Z"))))
            .build());
  }

  @Test
  void testFailure_notAuthorizedForTld() throws Exception {
    setUpSuccessfulTest();
    persistResource(
        loadRegistrar("TheRegistrar").asBuilder().setAllowedTlds(ImmutableSet.of()).build());
    EppException thrown = assertThrows(NotAuthorizedForTldException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_superuserNotAuthorizedForTld() throws Exception {
    setUpSuccessfulTest();
    persistResource(
        loadRegistrar("TheRegistrar").asBuilder().setAllowedTlds(ImmutableSet.of()).build());
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
  }

  @Test
  void testFailure_clientDeleteProhibited() throws Exception {
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .addStatusValue(StatusValue.CLIENT_DELETE_PROHIBITED)
            .build());
    ResourceStatusProhibitsOperationException thrown =
        assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("clientDeleteProhibited");
  }

  @Test
  void testFailure_serverDeleteProhibited() throws Exception {
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .addStatusValue(StatusValue.SERVER_DELETE_PROHIBITED)
            .build());
    ResourceStatusProhibitsOperationException thrown =
        assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("serverDeleteProhibited");
  }

  @Test
  void testFailure_pendingDelete() throws Exception {
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .addStatusValue(StatusValue.PENDING_DELETE)
            .build());
    ResourceStatusProhibitsOperationException thrown =
        assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("pendingDelete");
  }

  @Test
  void testSuccess_clientDeleteProhibited_superuser() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "15", "PENDING_DELETE_DAYS", "0"));
    setUpSuccessfulTest();

    persistResource(
        domain.asBuilder().addStatusValue(StatusValue.CLIENT_DELETE_PROHIBITED).build());
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
  }

  @Test
  void testSuccess_serverDeleteProhibited_superuser() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "15", "PENDING_DELETE_DAYS", "0"));
    setUpSuccessfulTest();

    persistResource(
        domain.asBuilder().addStatusValue(StatusValue.SERVER_DELETE_PROHIBITED).build());
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
  }

  @Test
  void testFailure_pendingDelete_superuser() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "15", "PENDING_DELETE_DAYS", "0"));
    setUpSuccessfulTest();
    persistResource(domain.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());

    ResourceStatusProhibitsOperationException thrown =
        assertThrows(
            ResourceStatusProhibitsOperationException.class,
            () -> runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER));
    assertThat(thrown).hasMessageThat().contains("pendingDelete");
  }

  @Test
  void testSuccess_metadata() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput("domain_delete_metadata.xml");
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlow();
    assertAboutDomains()
        .that(reloadResourceByForeignKey())
        .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_DELETE)
        .and()
        .hasLastEppUpdateTime(clock.now())
        .and()
        .hasLastEppUpdateRegistrarId("TheRegistrar");
    assertAboutHistoryEntries()
        .that(getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE))
        .hasType(DOMAIN_DELETE)
        .and()
        .hasMetadataReason("domain-delete-test")
        .and()
        .hasMetadataRequestedByRegistrar(false);
  }

  @Test
  void testFailure_metadataNotFromTool() throws Exception {
    setEppInput("domain_delete_metadata.xml");
    persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
    EppException thrown = assertThrows(OnlyToolCanPassMetadataException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testIcannActivityReportField_getsLogged() throws Exception {
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlow();
    assertIcannReportingActivityFieldLogged("srs-dom-delete");
    assertTldsFieldLogged("tld");
  }

  @Test
  void testIcannTransactionRecord_testTld_notStored() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriodDurations();
    persistResource(Tld.get("tld").asBuilder().setTldType(TldType.TEST).build());
    clock.advanceOneMilli();
    earlierHistoryEntry =
        persistResource(
            earlierHistoryEntry
                .asBuilder()
                .setType(DOMAIN_CREATE)
                .setModificationTime(minusDays(TIME_BEFORE_FLOW, 2))
                .setDomainTransactionRecords(
                    ImmutableSet.of(
                        DomainTransactionRecord.create(
                            "tld", plusDays(TIME_BEFORE_FLOW, 1), NET_ADDS_1_YR, 1)))
                .build());
    runFlow();
    DomainHistory persistedEntry = (DomainHistory) getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    // No transaction records should be recorded for test TLDs
    assertThat(persistedEntry.getDomainTransactionRecords()).isEmpty();
  }

  @Test
  void testIcannTransactionRecord_noGrace_entryOutsideMaxGracePeriod() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriodDurations();
    clock.advanceOneMilli();
    earlierHistoryEntry =
        persistResource(
            earlierHistoryEntry
                .asBuilder()
                .setType(DOMAIN_CREATE)
                .setModificationTime(minusDays(TIME_BEFORE_FLOW, 4))
                .setDomainTransactionRecords(
                    ImmutableSet.of(
                        DomainTransactionRecord.create(
                            "tld", plusDays(TIME_BEFORE_FLOW, 1), NET_ADDS_1_YR, 1)))
                .build());
    runFlow();
    DomainHistory persistedEntry = (DomainHistory) getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    // Transaction record should just be the non-grace period delete
    assertThat(persistedEntry.getDomainTransactionRecords())
        .containsExactly(
            DomainTransactionRecord.create(
                "tld", plusHours(clock.now(), 3), DELETED_DOMAINS_NOGRACE, 1));
  }

  @Test
  void testIcannTransactionRecord_noGrace_noAddOrRenewRecords() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriodDurations();
    clock.advanceOneMilli();
    earlierHistoryEntry =
        persistResource(
            earlierHistoryEntry
                .asBuilder()
                .setType(DOMAIN_CREATE)
                .setModificationTime(minusDays(TIME_BEFORE_FLOW, 2))
                .setDomainTransactionRecords(
                    ImmutableSet.of(
                        // Only add or renew records counts should be cancelled
                        DomainTransactionRecord.create(
                            "tld", plusDays(TIME_BEFORE_FLOW, 1), RESTORED_DOMAINS, 1)))
                .build());
    runFlow();
    DomainHistory persistedEntry = (DomainHistory) getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    // Transaction record should just be the non-grace period delete
    assertThat(persistedEntry.getDomainTransactionRecords())
        .containsExactly(
            DomainTransactionRecord.create(
                "tld", plusHours(clock.now(), 3), DELETED_DOMAINS_NOGRACE, 1));
  }

  /** Verifies that if there's no add grace period, we still cancel out valid renew records */
  @Test
  void testIcannTransactionRecord_noGrace_hasRenewRecord() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriodDurations();
    clock.advanceOneMilli();
    DomainTransactionRecord renewRecord =
        DomainTransactionRecord.create("tld", plusDays(TIME_BEFORE_FLOW, 1), NET_RENEWS_3_YR, 1);
    // We don't want to cancel non-add or renew records
    DomainTransactionRecord notCancellableRecord =
        DomainTransactionRecord.create("tld", plusDays(TIME_BEFORE_FLOW, 1), RESTORED_DOMAINS, 5);
    earlierHistoryEntry =
        persistResource(
            earlierHistoryEntry
                .asBuilder()
                .setType(DOMAIN_CREATE)
                .setModificationTime(minusDays(TIME_BEFORE_FLOW, 2))
                .setDomainTransactionRecords(ImmutableSet.of(renewRecord, notCancellableRecord))
                .build());
    runFlow();
    DomainHistory persistedEntry = (DomainHistory) getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    // We should only see the non-grace period delete record and the renew cancellation record
    assertThat(persistedEntry.getDomainTransactionRecords())
        .containsExactly(
            DomainTransactionRecord.create(
                "tld", plusHours(clock.now(), 3), DELETED_DOMAINS_NOGRACE, 1),
            renewRecord.asBuilder().setReportAmount(-1).build());
  }

  @Test
  void testIcannTransactionRecord_inGrace_noRecords() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriods(
        GracePeriod.create(
            GracePeriodStatus.ADD,
            domain.getRepoId(),
            plusDays(TIME_BEFORE_FLOW, 1),
            "TheRegistrar",
            null));
    setUpGracePeriodDurations();
    clock.advanceOneMilli();
    runFlow();
    DomainHistory persistedEntry = (DomainHistory) getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    // Transaction record should just be the grace period delete
    assertThat(persistedEntry.getDomainTransactionRecords())
        .containsExactly(
            DomainTransactionRecord.create("tld", clock.now(), DELETED_DOMAINS_GRACE, 1));
  }

  @Test
  void testIcannTransactionRecord_inGrace_multipleRecords() throws Exception {
    setUpSuccessfulTest();
    setUpGracePeriods(
        GracePeriod.create(
            GracePeriodStatus.ADD,
            domain.getRepoId(),
            plusDays(TIME_BEFORE_FLOW, 1),
            "TheRegistrar",
            null));
    setUpGracePeriodDurations();
    clock.advanceOneMilli();
    earlierHistoryEntry =
        persistResource(
            earlierHistoryEntry
                .asBuilder()
                .setType(DOMAIN_CREATE)
                .setModificationTime(minusDays(TIME_BEFORE_FLOW, 2))
                .setDomainTransactionRecords(
                    ImmutableSet.of(
                        DomainTransactionRecord.create(
                            "tld", plusDays(TIME_BEFORE_FLOW, 1), NET_ADDS_10_YR, 1)))
                .build());
    DomainTransactionRecord existingRecord =
        DomainTransactionRecord.create("tld", plusDays(TIME_BEFORE_FLOW, 2), NET_ADDS_10_YR, 1);
    // Create a HistoryEntry with a later modification time
    persistResource(
        new DomainHistory.Builder()
            .setType(DOMAIN_CREATE)
            .setDomain(domain)
            .setModificationTime(minusDays(TIME_BEFORE_FLOW, 1))
            .setRegistrarId("TheRegistrar")
            .setDomainTransactionRecords(ImmutableSet.of(existingRecord))
            .build());
    runFlow();
    DomainHistory persistedEntry = (DomainHistory) getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
    // Transaction record should be the grace period delete, and the more recent cancellation record
    assertThat(persistedEntry.getDomainTransactionRecords())
        .containsExactly(
            DomainTransactionRecord.create("tld", clock.now(), DELETED_DOMAINS_GRACE, 1),
            // The cancellation record is the same as the original, except with a -1 counter
            existingRecord.asBuilder().setReportAmount(-1).build());
  }

  @Test
  void testSuccess_superuserExtension_nonZeroDayGrace_nonZeroDayPendingDelete() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "15", "PENDING_DELETE_DAYS", "4"));
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
    Domain resource = reloadResourceByForeignKey();
    assertAboutDomains()
        .that(resource)
        .hasExactlyStatusValues(StatusValue.INACTIVE, StatusValue.PENDING_DELETE)
        .and()
        .hasDeletionTime(clock.now().plus(Duration.ofDays(19)));
    assertThat(resource.getGracePeriods())
        .containsExactly(
            GracePeriod.create(
                GracePeriodStatus.REDEMPTION,
                domain.getRepoId(),
                clock.now().plus(Duration.ofDays(15)),
                "TheRegistrar",
                null,
                resource.getGracePeriods().iterator().next().getGracePeriodId()));
    assertDeletionPollMessageFor(resource, "Deleted by registry administrator.");
  }

  @Test
  void testSuccess_superuserExtension_zeroDayGrace_nonZeroDayPendingDelete() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "0", "PENDING_DELETE_DAYS", "4"));
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
    Domain resource = reloadResourceByForeignKey();
    assertAboutDomains()
        .that(resource)
        .hasExactlyStatusValues(StatusValue.INACTIVE, StatusValue.PENDING_DELETE)
        .and()
        .hasDeletionTime(clock.now().plus(Duration.ofDays(4)));
    assertThat(resource.getGracePeriods()).isEmpty();
    assertDeletionPollMessageFor(resource, "Deleted by registry administrator.");
  }

  @Test
  void testSuccess_superuserExtension_nonZeroDayGrace_zeroDayPendingDelete() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "15", "PENDING_DELETE_DAYS", "0"));
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
    Domain resource = reloadResourceByForeignKey();
    assertAboutDomains()
        .that(resource)
        .hasExactlyStatusValues(StatusValue.INACTIVE, StatusValue.PENDING_DELETE)
        .and()
        .hasDeletionTime(clock.now().plus(Duration.ofDays(15)));
    assertThat(resource.getGracePeriods())
        .containsExactly(
            GracePeriod.create(
                GracePeriodStatus.REDEMPTION,
                domain.getRepoId(),
                clock.now().plus(Duration.ofDays(15)),
                "TheRegistrar",
                null,
                resource.getGracePeriods().iterator().next().getGracePeriodId()));
    assertDeletionPollMessageFor(resource, "Deleted by registry administrator.");
  }

  @Test
  void testSuccess_superuserExtension_zeroDayGrace_zeroDayPendingDelete() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "0", "PENDING_DELETE_DAYS", "0"));
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    assertThat(reloadResourceByForeignKey()).isNull();
    Domain resavedDomain = loadByEntity(domain);
    assertDeletionPollMessageFor(resavedDomain, "Deleted by registry administrator.");
  }

  @Test
  void testSuccess_immediateDelete_withSuperuserAndMetadataExtension() throws Exception {
    sessionMetadata.setRegistrarId("NewRegistrar");
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput(
        "domain_delete_superuser_and_metadata_extension.xml",
        ImmutableMap.of("REDEMPTION_GRACE_PERIOD_DAYS", "0", "PENDING_DELETE_DAYS", "0"));
    setUpSuccessfulTest();
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    assertThat(reloadResourceByForeignKey()).isNull();
    assertDeletionPollMessageFor(
        loadByEntity(domain), "Deleted by registry administrator: Broke world.");
  }

  @Test
  void testFailure_allocationTokenNotSupportedOnDelete() {
    setEppInput("domain_delete_allocationtoken.xml");
    EppException thrown = assertThrows(UnimplementedExtensionException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_freeCreation_deletionDuringGracePeriod() throws Exception {
    // Deletion during the add grace period should still work even if the credit is 0
    setUpSuccessfulTest();
    BillingEvent graceBillingEvent =
        persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 0)));
    setUpGracePeriods(
        GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), graceBillingEvent));
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_fee_free_grace_stdv1.xml"));
  }

  @Test
  void testSuccess_skipsPollMessage_whenConfigured() throws Exception {
    setUpSuccessfulTest();
    domain =
        persistResource(
            domain.asBuilder().setPersistedCurrentSponsorRegistrarId("NewRegistrar").build());
    setRegistrarIdForFlow("NewRegistrar");
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    assertPollMessages();
  }

  @Test
  void testSuccess_logsSqlStatements() throws Exception {
    setUpSuccessfulTest();
    runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
    LogsSubject.assertAboutLogs()
        .that(logHandler)
        .hasLogAtLevelWithMessage(
            Level.INFO,
            "SQL_STATEMENT_LOG: insert into \"DomainHistory\" (history_by_superuser,"
                + "history_registrar_id,history_modification_time,history_other_registrar_id,"
                + "history_period_unit,history_period_value,history_reason,history");
  }

  @Test
  void testSuccess_renewGracePeriodCredit_v06() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_06_MAP);
  }

  @Test
  void testSuccess_renewGracePeriodCredit_v11() throws Exception {
    setEppInput("domain_delete.xml", FEE_11_MAP);
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_11_MAP);
  }

  @Test
  void testSuccess_renewGracePeriodCredit_v12() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    doSuccessfulTest_noAddGracePeriod("domain_delete_response_pending_fee.xml", FEE_12_MAP);
  }

  @Test
  void testSuccess_addGracePeriodCredit_v06() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_06_MAP);
  }

  @Test
  void testSuccess_addGracePeriodCredit_v11() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_11_MAP);
  }

  @Test
  void testSuccess_addGracePeriodCredit_v12() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    doAddGracePeriodDeleteTest(GracePeriodStatus.ADD, "domain_delete_response_fee.xml", FEE_12_MAP);
  }

  @Test
  void testSuccess_autoRenewGracePeriod_v06() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_06_MAP));
  }

  @Test
  void testSuccess_autoRenewGracePeriod_v11() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_11_MAP));
  }

  @Test
  void testSuccess_autoRenewGracePeriod_v12() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_12_MAP));
  }

  @Test
  void testSuccess_autoRenewGracePeriod_priceChanges_v06() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_11.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setRenewBillingCostTransitions(
                ImmutableSortedMap.of(
                    START_INSTANT,
                    Money.of(USD, 11),
                    minusDays(TIME_BEFORE_FLOW, 5),
                    Money.of(USD, 20)))
            .build());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_06_MAP));
  }

  @Test
  void testSuccess_autoRenewGracePeriod_priceChanges_v11() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_0_12.getUri());
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setRenewBillingCostTransitions(
                ImmutableSortedMap.of(
                    START_INSTANT,
                    Money.of(USD, 11),
                    minusDays(TIME_BEFORE_FLOW, 5),
                    Money.of(USD, 20)))
            .build());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_11_MAP));
  }

  @Test
  void testSuccess_autoRenewGracePeriod_priceChanges_v12() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setRenewBillingCostTransitions(
                ImmutableSortedMap.of(
                    START_INSTANT,
                    Money.of(USD, 11),
                    minusDays(TIME_BEFORE_FLOW, 5),
                    Money.of(USD, 20)))
            .build());
    setUpAutorenewGracePeriod();
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_autorenew_fee.xml", FEE_12_MAP));
  }

  @Test
  void testSuccess_freeCreation_deletionDuringGracePeriod_v12() throws Exception {
    removeServiceExtensionUri(ServiceExtension.FEE_1_00.getUri());
    // Deletion during the add grace period should still work even if the credit is 0
    setUpSuccessfulTest();
    BillingEvent graceBillingEvent =
        persistResource(createBillingEvent(Reason.CREATE, Money.of(USD, 0)));
    setUpGracePeriods(
        GracePeriod.forBillingEvent(GracePeriodStatus.ADD, domain.getRepoId(), graceBillingEvent));
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("domain_delete_response_fee_free_grace_v12.xml"));
  }
}
