// 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.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.union;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ForeignKeyUtils.loadResource;
import static google.registry.model.common.FeatureFlag.FeatureName.FORBID_INSECURE_ALGORITHMS_RFC_9904;
import static google.registry.model.eppcommon.StatusValue.CLIENT_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD;
import static google.registry.model.eppcommon.StatusValue.CLIENT_RENEW_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.CLIENT_TRANSFER_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.CLIENT_UPDATE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
import static google.registry.model.eppcommon.StatusValue.SERVER_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD;
import static google.registry.model.eppcommon.StatusValue.SERVER_RENEW_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertDomainDnsRequests;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.assertNoDnsRequests;
import static google.registry.testing.DatabaseHelper.assertPollMessagesForResource;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistActiveSubordinateHost;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistFeatureFlag;
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.START_INSTANT;
import static google.registry.util.DateTimeUtils.minusDays;
import static google.registry.util.DateTimeUtils.plusDays;
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.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.config.RegistryConfig;
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.AddExistingValueException;
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
import google.registry.flows.ResourceFlowUtils.RemoveNonexistentValueException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
import google.registry.flows.domain.DomainFlowUtils.EmptySecDnsUpdateException;
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeOperationException;
import google.registry.flows.domain.DomainFlowUtils.InvalidDsRecordException;
import google.registry.flows.domain.DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException;
import google.registry.flows.domain.DomainFlowUtils.LinkedResourcesDoNotExistException;
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeChangeNotSupportedException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
import google.registry.flows.domain.DomainFlowUtils.SecDnsAllUsageException;
import google.registry.flows.domain.DomainFlowUtils.TooManyDsRecordsException;
import google.registry.flows.domain.DomainFlowUtils.TooManyNameserversException;
import google.registry.flows.domain.DomainFlowUtils.UrgentAttributeNotSupportedException;
import google.registry.flows.exceptions.ContactsProhibitedException;
import google.registry.flows.exceptions.OnlyToolCanPassMetadataException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.secdns.DomainDsData;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import java.time.Instant;
import java.util.Optional;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

  private static final String SHA_256_DIGEST =
      "D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A";

  private static final DomainDsData SOME_DSDATA =
      DomainDsData.create(
          1,
          2,
          2,
          base16().decode("9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"));
  private static final ImmutableMap<String, String> OTHER_DSDATA_TEMPLATE_MAP =
      ImmutableMap.of(
          "KEY_TAG", "12346",
          "ALG", "8",
          "DIGEST_TYPE", "2",
          "DIGEST", SHA_256_DIGEST);

  @BeforeEach
  void beforeEach() {
    createTld("tld");
    setEppInput("domain_update.xml");
  }

  private void persistReferencedEntities() {
    for (int i = 1; i <= 14; ++i) {
      persistActiveHost(String.format("ns%d.example.foo", i));
    }
  }

  private Domain persistDomain() throws Exception {
    Host host = loadResource(Host.class, "ns1.example.foo", clock.now()).get();
    Domain domain =
        persistResource(
            DatabaseHelper.newDomain(getUniqueIdFromCommand())
                .asBuilder()
                .setNameservers(ImmutableSet.of(host.createVKey()))
                .build());
    persistResource(
        new DomainHistory.Builder()
            .setType(DOMAIN_CREATE)
            .setModificationTime(clock.now())
            .setRegistrarId(domain.getCreationRegistrarId())
            .setDomain(domain)
            .build());
    clock.advanceOneMilli();
    return domain;
  }

  private void doSuccessfulTest() throws Exception {
    doSuccessfulTest("generic_success_response.xml");
  }

  private void doSuccessfulTest(String expectedXmlFilename) throws Exception {
    assertMutatingFlow(true);
    runFlowAssertResponse(loadFile(expectedXmlFilename));
    Domain domain = reloadResourceByForeignKey();
    // Check that the domain was updated. These values came from the xml.
    assertAboutDomains()
        .that(domain)
        .hasStatusValue(CLIENT_HOLD)
        .and()
        .hasAuthInfoPwd("2BARfoo")
        .and()
        .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE)
        .and()
        .hasLastEppUpdateTime(clock.now())
        .and()
        .hasLastEppUpdateRegistrarId("TheRegistrar")
        .and()
        .hasNoAutorenewEndTime();
    assertNoBillingEvents();
    assertDomainDnsRequests("example.tld");
    assertLastHistoryContainsResource(reloadResourceByForeignKey());
  }

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

  @Test
  void testDryRun() throws Exception {
    persistReferencedEntities();
    persistDomain();
    dryRunFlowAssertResponse(loadFile("generic_success_response.xml"));
  }

  @Test
  void testSuccess() throws Exception {
    persistReferencedEntities();
    persistDomain();
    doSuccessfulTest();
  }

  @Test
  void testSuccess_clTridNotSpecified() throws Exception {
    setEppInput("domain_update_no_cltrid.xml");
    persistReferencedEntities();
    persistDomain();
    doSuccessfulTest("generic_success_response_no_cltrid.xml");
  }

  @Test
  void testSuccess_noExistingAuthData() throws Exception {
    setEppInput("domain_update_no_auth_change.xml");
    persistReferencedEntities();
    persistDomain();
    persistResource(
        reloadResourceByForeignKey()
            .asBuilder()
            .setAuthInfo(DomainAuthInfo.create(PasswordAuth.create(null)))
            .build());
    assertMutatingFlow(true);
    runFlowAssertResponse(loadFile("generic_success_response.xml"));
    Domain domain = reloadResourceByForeignKey();
    // Check that the domain was updated. These values came from the xml.
    assertAboutDomains()
        .that(domain)
        .hasStatusValue(CLIENT_HOLD)
        .and()
        .hasAuthInfoPwd(null)
        .and()
        .hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE)
        .and()
        .hasLastEppUpdateTime(clock.now())
        .and()
        .hasLastEppUpdateRegistrarId("TheRegistrar")
        .and()
        .hasNoAutorenewEndTime();
    assertNoBillingEvents();
    assertDomainDnsRequests("example.tld");
    assertLastHistoryContainsResource(reloadResourceByForeignKey());
  }

  @Test
  void testSuccess_cachingDisabled() throws Exception {
    boolean origIsCachingEnabled = RegistryConfig.isEppResourceCachingEnabled();
    try {
      RegistryConfig.overrideIsEppResourceCachingEnabledForTesting(false);
      persistReferencedEntities();
      persistDomain();
      doSuccessfulTest();
    } finally {
      RegistryConfig.overrideIsEppResourceCachingEnabledForTesting(origIsCachingEnabled);
    }
  }

  @Test
  void testSuccess_inQuietPeriod() throws Exception {
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setTldStateTransitions(ImmutableSortedMap.of(START_INSTANT, QUIET_PERIOD))
            .build());
    persistReferencedEntities();
    persistDomain();
    doSuccessfulTest();
  }

  @Test
  void testFailure_whenAddingNewContacts() throws Exception {
    // This EPP adds a new technical contact mak21 that wasn't already present.
    setEppInput("domain_update_empty_registrant.xml");
    persistReferencedEntities();
    persistDomain();
    // Fails because the update adds some new contacts, although the registrant has been removed.
    EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  private void modifyDomainToHave13Nameservers() throws Exception {
    ImmutableSet.Builder<VKey<Host>> nameservers = new ImmutableSet.Builder<>();
    for (int i = 1; i < 15; i++) {
      if (i != 2) { // Skip 2 since that's the one that the tests will add.
        nameservers.add(
            loadResource(Host.class, String.format("ns%d.example.foo", i), clock.now())
                .get()
                .createVKey());
      }
    }
    persistResource(
        reloadResourceByForeignKey().asBuilder().setNameservers(nameservers.build()).build());
  }

  @Test
  void testSuccess_maxNumberOfNameservers() throws Exception {
    persistReferencedEntities();
    persistDomain();
    // Modify domain to have 13 nameservers. We will then remove one and add one in the test.
    modifyDomainToHave13Nameservers();
    doSuccessfulTest();
  }

  @Test
  void testSuccess_addAndRemoveLargeNumberOfNameservers() throws Exception {
    persistReferencedEntities();
    persistDomain();
    setEppInput("domain_update_max_everything.xml");
    // Create 26 hosts. Start the domain with half of them.
    ImmutableSet.Builder<VKey<Host>> nameservers = new ImmutableSet.Builder<>();
    for (int i = 0; i < 26; i++) {
      Host host = persistActiveHost(String.format("max_test_%d.example.tld", i));
      if (i < 13) {
        nameservers.add(host.createVKey());
      }
    }
    persistResource(
        reloadResourceByForeignKey().asBuilder().setNameservers(nameservers.build()).build());
    clock.advanceOneMilli();
    assertMutatingFlow(true);
    runFlowAssertResponse(loadFile("generic_success_response.xml"));
    Domain domain = reloadResourceByForeignKey();
    assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
    assertThat(domain.getNameservers()).hasSize(13);
    assertNoBillingEvents();
    assertDomainDnsRequests("example.tld");
  }

  @Test
  void testSuccess_metadata() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput("domain_update_metadata.xml");
    persistReferencedEntities();
    persistDomain();
    runFlow();
    Domain domain = reloadResourceByForeignKey();
    assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
    assertAboutHistoryEntries()
        .that(getOnlyHistoryEntryOfType(domain, DOMAIN_UPDATE))
        .hasMetadataReason("domain-update-test")
        .and()
        .hasMetadataRequestedByRegistrar(true);
  }

  @Test
  void testSuccess_metadataNotFromTool() throws Exception {
    setEppInput("domain_update_metadata.xml");
    persistReferencedEntities();
    persistDomain();
    EppException thrown = assertThrows(OnlyToolCanPassMetadataException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_removeContact() throws Exception {
    setEppInput("domain_update_remove_contact.xml");
    persistReferencedEntities();
    persistDomain();
    doSuccessfulTest();
  }

  @Test
  void testSuccess_addAndRemoveSubordinateHostNameservers() throws Exception {
    // Test that operations involving subordinate hosts as nameservers do not change the subordinate
    // host relationship itself.
    setEppInput("domain_update_subordinate_hosts.xml");
    persistReferencedEntities();
    Domain domain = persistDomain();
    persistActiveSubordinateHost("ns1.example.tld", domain);
    Host addedHost = persistActiveSubordinateHost("ns2.example.tld", domain);
    persistResource(
        domain
            .asBuilder()
            .addSubordinateHost("ns1.example.tld")
            .addSubordinateHost("ns2.example.tld")
            .setNameservers(
                ImmutableSet.of(
                    loadResource(Host.class, "ns1.example.tld", clock.now()).get().createVKey()))
            .build());
    clock.advanceOneMilli();
    assertMutatingFlow(true);
    runFlowAssertResponse(loadFile("generic_success_response.xml"));
    domain = reloadResourceByForeignKey();
    assertThat(domain.getNameservers()).containsExactly(addedHost.createVKey());
    assertThat(domain.getSubordinateHosts()).containsExactly("ns1.example.tld", "ns2.example.tld");
    Host existingHost = loadResource(Host.class, "ns1.example.tld", clock.now()).get();
    addedHost = loadResource(Host.class, "ns2.example.tld", clock.now()).get();
    assertThat(existingHost.getSuperordinateDomain()).isEqualTo(domain.createVKey());
    assertThat(addedHost.getSuperordinateDomain()).isEqualTo(domain.createVKey());
  }

  @Test
  void testSuccess_removeClientUpdateProhibited() throws Exception {
    setEppInput("domain_update_remove_client_update_prohibited.xml");
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setStatusValues(ImmutableSet.of(CLIENT_UPDATE_PROHIBITED))
            .build());
    clock.advanceOneMilli();
    runFlow();
    assertAboutDomains()
        .that(reloadResourceByForeignKey())
        .doesNotHaveStatusValue(CLIENT_UPDATE_PROHIBITED);
  }

  private void doSecDnsSuccessfulTest(
      String xmlFilename,
      ImmutableSet<DomainDsData> originalDsData,
      ImmutableSet<DomainDsData> expectedDsData,
      boolean dnsTaskEnqueued)
      throws Exception {
    doSecDnsSuccessfulTest(
        xmlFilename, originalDsData, expectedDsData, OTHER_DSDATA_TEMPLATE_MAP, dnsTaskEnqueued);
  }

  private void doSecDnsSuccessfulTest(
      String xmlFilename,
      ImmutableSet<DomainDsData> originalDsData,
      ImmutableSet<DomainDsData> expectedDsData,
      ImmutableMap<String, String> substitutions,
      boolean dnsTaskEnqueued)
      throws Exception {
    setEppInput(xmlFilename, substitutions);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(originalDsData)
            .build());
    assertMutatingFlow(true);
    clock.advanceOneMilli();
    runFlowAssertResponse(loadFile("generic_success_response.xml"));
    Domain resource = reloadResourceByForeignKey();
    assertAboutDomains().that(resource).hasOnlyOneHistoryEntryWhich().hasType(DOMAIN_UPDATE);
    assertThat(resource.getDsData())
        .isEqualTo(
            expectedDsData.stream()
                .map(ds -> ds.cloneWithDomainRepoId(resource.getRepoId()))
                .collect(toImmutableSet()));
    if (dnsTaskEnqueued) {
      assertDomainDnsRequests("example.tld");
    } else {
      assertNoDnsRequests();
    }
  }

  @Test
  void testSuccess_secDnsAdd() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        null,
        ImmutableSet.of(DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
        ImmutableMap.of(
            "KEY_TAG", "12346", "ALG", "8", "DIGEST_TYPE", "2", "DIGEST", SHA_256_DIGEST),
        true);
  }

  @Test
  void testSuccess_secDnsAddPreservesExisting() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        ImmutableSet.of(SOME_DSDATA),
        ImmutableSet.of(
            SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
        ImmutableMap.of(
            "KEY_TAG", "12346", "ALG", "8", "DIGEST_TYPE", "2", "DIGEST", SHA_256_DIGEST),
        true);
  }

  @Test
  void testFailure_secDnsAddSame_throwsException() throws Exception {
    EppException thrown =
        assertThrows(
            AddExistingValueException.class,
            () ->
                doSecDnsSuccessfulTest(
                    "domain_update_dsdata_add.xml",
                    ImmutableSet.of(SOME_DSDATA),
                    ImmutableSet.of(SOME_DSDATA),
                    ImmutableMap.of(
                        "KEY_TAG",
                        "1",
                        "ALG",
                        "2",
                        "DIGEST_TYPE",
                        "2",
                        "DIGEST",
                        "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"),
                    false));
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_secDnsAddOnlyKeyTagRemainsSame() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        ImmutableSet.of(SOME_DSDATA),
        ImmutableSet.of(
            SOME_DSDATA,
            DomainDsData.create(
                1,
                8,
                4,
                base16()
                    .decode(
                        "768412320F7B0AA5812FCE428DC4706B3CAE50E02A64CAA16A782249BFE8EFC4B7EF1CCB126255D196047DFEDF17A0A9"))),
        ImmutableMap.of(
            "KEY_TAG",
            "1",
            "ALG",
            "8",
            "DIGEST_TYPE",
            "4",
            "DIGEST",
            "768412320F7B0AA5812FCE428DC4706B3CAE50E02A64CAA16A782249BFE8EFC4B7EF1CCB126255D196047DFEDF17A0A9"),
        true);
  }

  // Changing any of the four fields in DomainDsData should result in a new object
  @Test
  void testSuccess_secDnsAddOnlyChangeKeyTag() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        ImmutableSet.of(SOME_DSDATA),
        ImmutableSet.of(
            SOME_DSDATA,
            DomainDsData.create(
                12346,
                2,
                2,
                base16()
                    .decode("9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"))),
        ImmutableMap.of(
            "KEY_TAG",
            "12346",
            "ALG",
            "2",
            "DIGEST_TYPE",
            "2",
            "DIGEST",
            "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"),
        true);
  }

  @Test
  void testSuccess_secDnsAddOnlyChangeAlgorithm() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        ImmutableSet.of(SOME_DSDATA),
        ImmutableSet.of(
            SOME_DSDATA,
            DomainDsData.create(
                1,
                8,
                2,
                base16()
                    .decode("9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"))),
        ImmutableMap.of(
            "KEY_TAG",
            "1",
            "ALG",
            "8",
            "DIGEST_TYPE",
            "2",
            "DIGEST",
            "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"),
        true);
  }

  @Test
  void testSuccess_secDnsAddOnlyChangeDigestType() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        ImmutableSet.of(SOME_DSDATA),
        ImmutableSet.of(
            SOME_DSDATA,
            DomainDsData.create(
                1,
                2,
                4,
                base16()
                    .decode(
                        "768412320F7B0AA5812FCE428DC4706B3CAE50E02A64CAA16A782249BFE8EFC4B7EF1CCB126255D196047DFEDF17A0A9"))),
        ImmutableMap.of(
            "KEY_TAG",
            "1",
            "ALG",
            "2",
            "DIGEST_TYPE",
            "4",
            "DIGEST",
            "768412320F7B0AA5812FCE428DC4706B3CAE50E02A64CAA16A782249BFE8EFC4B7EF1CCB126255D196047DFEDF17A0A9"),
        true);
  }

  @Test
  void testSuccess_secDnsAddOnlyChangeDigest() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        ImmutableSet.of(SOME_DSDATA),
        ImmutableSet.of(
            SOME_DSDATA,
            DomainDsData.create(
                1,
                2,
                2,
                base16()
                    .decode("0F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"))),
        ImmutableMap.of(
            "KEY_TAG",
            "1",
            "ALG",
            "2",
            "DIGEST_TYPE",
            "2",
            "DIGEST",
            "0F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"),
        true);
  }

  @Test
  void testSuccess_secDnsAddToMaxRecords() throws Exception {
    ImmutableSet.Builder<DomainDsData> builder = new ImmutableSet.Builder<>();
    for (int i = 0; i < 7; ++i) {
      builder.add(
          DomainDsData.create(
              i,
              2,
              2,
              base16().decode("9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08")));
    }
    ImmutableSet<DomainDsData> commonDsData = builder.build();

    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add.xml",
        commonDsData,
        ImmutableSet.copyOf(
            union(
                commonDsData,
                ImmutableSet.of(
                    DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))))),
        true);
  }

  @Test
  void testSuccess_secDnsRemove() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_rem.xml",
        ImmutableSet.of(
            SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
        ImmutableSet.of(SOME_DSDATA),
        true);
  }

  @Test
  void testSuccess_secDnsRemoveAll() throws Exception {
    // As an aside, this test also validates that it's ok to set the 'urgent' attribute to false.
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_rem_all.xml",
        ImmutableSet.of(
            SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
        ImmutableSet.of(),
        true);
  }

  @Test
  void testSuccess_secDnsAddRemove() throws Exception {
    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add_rem.xml",
        ImmutableSet.of(
            SOME_DSDATA, DomainDsData.create(12345, 8, 2, base16().decode(SHA_256_DIGEST))),
        ImmutableSet.of(
            SOME_DSDATA, DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))),
        true);
  }

  @Test
  void testSuccess_secDnsAddRemoveToMaxRecords() throws Exception {
    ImmutableSet.Builder<DomainDsData> builder = new ImmutableSet.Builder<>();
    for (int i = 0; i < 7; ++i) {
      builder.add(
          DomainDsData.create(
              i,
              2,
              2,
              base16().decode("9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08")));
    }
    ImmutableSet<DomainDsData> commonDsData = builder.build();

    doSecDnsSuccessfulTest(
        "domain_update_dsdata_add_rem.xml",
        ImmutableSet.copyOf(
            union(
                commonDsData,
                ImmutableSet.of(
                    DomainDsData.create(12345, 8, 2, base16().decode(SHA_256_DIGEST))))),
        ImmutableSet.copyOf(
            union(
                commonDsData,
                ImmutableSet.of(
                    DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST))))),
        true);
  }

  @Test
  void testFailure_secDnsAddRemoveSame_throwsException() throws Exception {
    EppException thrown =
        assertThrows(
            AddRemoveSameValueException.class,
            () ->
                doSecDnsSuccessfulTest(
                    "domain_update_dsdata_add_rem_same.xml",
                    ImmutableSet.of(
                        SOME_DSDATA,
                        DomainDsData.create(
                            12345,
                            3,
                            1,
                            base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
                    ImmutableSet.of(
                        SOME_DSDATA,
                        DomainDsData.create(
                            12345,
                            3,
                            1,
                            base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"))),
                    false));
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsRemoveAlreadyNotThere_throwsException() throws Exception {
    EppException thrown =
        assertThrows(
            RemoveNonexistentValueException.class,
            () ->
                doSecDnsSuccessfulTest(
                    "domain_update_dsdata_rem.xml",
                    ImmutableSet.of(SOME_DSDATA),
                    ImmutableSet.of(SOME_DSDATA),
                    false));
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  void doServerStatusBillingTest(String xmlFilename, boolean isBillable) throws Exception {
    setEppInput(xmlFilename);
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));

    if (isBillable) {
      assertBillingEvents(
          new BillingEvent.Builder()
              .setReason(Reason.SERVER_STATUS)
              .setTargetId("example.tld")
              .setRegistrarId("TheRegistrar")
              .setCost(Money.of(USD, 19))
              .setEventTime(clock.now())
              .setBillingTime(clock.now())
              .setDomainHistory(
                  getOnlyHistoryEntryOfType(
                      reloadResourceByForeignKey(), DOMAIN_UPDATE, DomainHistory.class))
              .build());
    } else {
      assertNoBillingEvents();
    }
  }

  @Test
  void testSuccess_addServerStatusBillingEvent() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    persistReferencedEntities();
    persistDomain();
    doServerStatusBillingTest("domain_update_add_server_status.xml", true);
  }

  @Test
  void testSuccess_removeServerStatusBillingEvent() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    persistReferencedEntities();
    Domain removeStatusDomain = persistDomain();
    persistResource(removeStatusDomain.asBuilder().addStatusValue(SERVER_RENEW_PROHIBITED).build());
    doServerStatusBillingTest("domain_update_remove_server_status.xml", true);
  }

  @Test
  void testSuccess_changeServerStatusBillingEvent() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    persistReferencedEntities();
    Domain changeStatusDomain = persistDomain();
    persistResource(changeStatusDomain.asBuilder().addStatusValue(SERVER_RENEW_PROHIBITED).build());
    doServerStatusBillingTest("domain_update_change_server_status.xml", true);
  }

  @Test
  void testSuccess_noBillingEventOnNonServerStatusChange() throws Exception {
    persistActiveDomain(getUniqueIdFromCommand());
    doServerStatusBillingTest("domain_update_add_non_server_status.xml", false);
  }

  @Test
  void testSuccess_noBillingEventOnServerHoldStatusChange() throws Exception {
    persistActiveDomain(getUniqueIdFromCommand());
    doServerStatusBillingTest("domain_update_add_server_hold_status.xml", false);
  }

  @Test
  void testSuccess_noBillingEventOnServerStatusChangeNotFromRegistrar() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    persistActiveDomain(getUniqueIdFromCommand());
    doServerStatusBillingTest("domain_update_add_server_status_non_registrar.xml", false);
  }

  @Test
  void testSuccess_superuserClientUpdateProhibited() throws Exception {
    setEppInput("domain_update_add_server_hold_status.xml");
    persistReferencedEntities();
    persistResource(
        persistActiveDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setStatusValues(ImmutableSet.of(CLIENT_UPDATE_PROHIBITED))
            .build());
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    assertAboutDomains()
        .that(reloadResourceByForeignKey())
        .hasStatusValue(CLIENT_UPDATE_PROHIBITED)
        .and()
        .hasStatusValue(SERVER_HOLD);
  }

  private void doSecDnsFailingTest(
      Class<? extends EppException> expectedException, String xmlFilename) throws Exception {
    setEppInput(xmlFilename);
    persistReferencedEntities();
    persistActiveDomain(getUniqueIdFromCommand());
    EppException thrown = assertThrows(expectedException, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsAllCannotBeFalse() throws Exception {
    doSecDnsFailingTest(SecDnsAllUsageException.class, "domain_update_dsdata_rem_all_false.xml");
  }

  @Test
  void testFailure_secDnsEmptyNotAllowed() throws Exception {
    doSecDnsFailingTest(EmptySecDnsUpdateException.class, "domain_update_dsdata_empty.xml");
  }

  @Test
  void testFailure_secDnsUrgentNotSupported() throws Exception {
    doSecDnsFailingTest(
        UrgentAttributeNotSupportedException.class, "domain_update_dsdata_urgent.xml");
  }

  @Test
  void testFailure_secDnsChangeNotSupported() throws Exception {
    doSecDnsFailingTest(
        MaxSigLifeChangeNotSupportedException.class, "domain_update_maxsiglife.xml");
  }

  @Test
  void testFailure_secDnsInvalidDigestType() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 3, new byte[] {0, 1, 2})))
            .build());
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsSha1DigestType() throws Exception {
    setEppInput(
        "domain_update_dsdata_add.xml",
        ImmutableMap.of(
            "KEY_TAG", "12346",
            "ALG", "3",
            "DIGEST_TYPE", "1",
            "DIGEST", "A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"));
    persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
    persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_secDnsSha1_flagInactive() throws Exception {
    setEppInput(
        "domain_update_dsdata_add.xml",
        ImmutableMap.of(
            "KEY_TAG", "12346",
            "ALG", "3",
            "DIGEST_TYPE", "1",
            "DIGEST", "A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"));
    persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
    persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.INACTIVE);
    runFlow();
    Domain domain = reloadResourceByForeignKey();
    assertAboutDomains()
        .that(domain)
        .hasExactlyDsData(
            DomainDsData.create(
                12346, 3, 1, base16().decode("A94A8FE5CCB19BA61C4C0873D391E987982FBBD3")));
  }

  @Test
  void testFailure_secDnsMultipleInvalidDigestTypes() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(
                ImmutableSet.of(
                    DomainDsData.create(1, 2, 3, new byte[] {0, 1, 2}),
                    DomainDsData.create(2, 2, 6, new byte[] {0, 1, 2})))
            .build());
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("digestType=3");
    assertThat(thrown).hasMessageThat().contains("digestType=6");
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsInvalidDigestLength() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 2, new byte[] {0, 1, 2})))
            .build());
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
    assertThat(thrown)
        .hasMessageThat()
        .contains("Domain contains DS record(s) with an invalid digest length");
  }

  @Test
  void testFailure_secDnsMultipleInvalidDigestLengths() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(
                ImmutableSet.of(
                    DomainDsData.create(1, 2, 2, new byte[] {0, 1, 2, 3, 4}),
                    DomainDsData.create(2, 2, 2, new byte[] {5, 6, 7})))
            .build());
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("0, 1, 2, 3, 4");
    assertThat(thrown).hasMessageThat().contains("5, 6, 7");
    assertThat(thrown)
        .hasMessageThat()
        .contains("Domain contains DS record(s) with an invalid digest length");
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsInvalidAlgorithm() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(ImmutableSet.of(DomainDsData.create(1, 99, 2, new byte[] {0, 1, 2})))
            .build());
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsForbiddenAlgorithm() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(
                ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
            .build());
    persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_secDnsForbiddenAlgorithm_flagInactive() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(
                ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
            .build());
    persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.INACTIVE);
    runFlow();
    Domain domain = reloadResourceByForeignKey();
    assertAboutDomains()
        .that(domain)
        .hasExactlyDsData(
            DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST)),
            DomainDsData.create(12346, 8, 2, base16().decode(SHA_256_DIGEST)));
  }

  @Test
  void testFailure_unrelatedUpdate_existingForbiddenAlgorithm() throws Exception {
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setDsData(
                ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
            .build());
    persistFeatureFlag(FORBID_INSECURE_ALGORITHMS_RFC_9904, FeatureStatus.ACTIVE);
    assertAboutEppExceptions()
        .that(assertThrows(InvalidDsRecordException.class, this::runFlow))
        .marshalsToXml();
  }

  @Test
  void testSuccess_unrelatedUpdate_existingForbiddenAlgorithm_flagInactive() throws Exception {
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setDsData(
                ImmutableSet.of(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST))))
            .build());
    runFlow();
    Domain updatedDomain = reloadResourceByForeignKey();
    assertAboutDomains()
        .that(updatedDomain)
        .hasExactlyDsData(DomainDsData.create(1, 1, 2, base16().decode(SHA_256_DIGEST)));
  }

  @Test
  void testFailure_secDnsMultipleInvalidAlgorithms() throws Exception {
    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(
                ImmutableSet.of(
                    DomainDsData.create(1, 998, 2, new byte[] {0, 1, 2}),
                    DomainDsData.create(2, 99, 2, new byte[] {0, 1, 2})))
            .build());
    EppException thrown = assertThrows(InvalidDsRecordException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("algorithm=998");
    assertThat(thrown).hasMessageThat().contains("algorithm=99");
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_secDnsTooManyDsRecords() throws Exception {
    ImmutableSet.Builder<DomainDsData> builder = new ImmutableSet.Builder<>();
    for (int i = 0; i < 8; ++i) {
      builder.add(DomainDsData.create(i, 2, 2, new byte[] {0, 1, 2}));
    }

    setEppInput("domain_update_dsdata_add.xml", OTHER_DSDATA_TEMPLATE_MAP);
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setDsData(builder.build())
            .build());
    EppException thrown = assertThrows(TooManyDsRecordsException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_tooManyNameservers() throws Exception {
    setEppInput("domain_update_add_nameserver.xml");
    persistReferencedEntities();
    persistDomain();
    // Modify domain so it has 13 nameservers. We will then try to add one in the test.
    modifyDomainToHave13Nameservers();
    EppException thrown = assertThrows(TooManyNameserversException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_wrongExtension() throws Exception {
    setEppInput("domain_update_wrong_extension.xml");
    persistReferencedEntities();
    persistDomain();
    EppException thrown = assertThrows(UnimplementedExtensionException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

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

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

  @Test
  void testFailure_missingHost() throws Exception {
    persistActiveHost("ns1.example.foo");
    persistActiveDomain(getUniqueIdFromCommand());
    LinkedResourcesDoNotExistException thrown =
        assertThrows(LinkedResourcesDoNotExistException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("(ns2.example.foo)");
  }

  @Test
  void testFailure_statusValueNotClientSettable() throws Exception {
    setEppInput("domain_update_prohibited_status.xml");
    persistReferencedEntities();
    persistDomain();
    EppException thrown = assertThrows(StatusNotClientSettableException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_superuserCanSetServerStatusValues() throws Exception {
    setEppInput("domain_update_prohibited_status.xml");
    persistReferencedEntities();
    persistDomain();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    // No poll message because the server status was added by the owning registrar.
    assertThat(getPollMessages()).isEmpty();
  }

  @Test
  void testSuccess_addingServerStatusValue_sendsPollMessage() throws Exception {
    setEppInput("domain_update_prohibited_status.xml");
    persistReferencedEntities();
    persistDomain();
    persistResource(
        reloadResourceByForeignKey()
            .asBuilder()
            .setPersistedCurrentSponsorRegistrarId("NewRegistrar")
            .build());
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    Domain updatedDomain = reloadResourceByForeignKey();
    assertPollMessagesForResource(
        updatedDomain,
        new PollMessage.OneTime.Builder()
            .setEventTime(clock.now())
            .setHistoryEntry(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
            .setRegistrarId("NewRegistrar")
            .setMsg("The registry administrator has added the status(es) [serverHold].")
            .setResponseData(
                ImmutableList.of(
                    DomainPendingActionNotificationResponse.create(
                        "example.tld", true, Trid.create("ABC-12345", "server-trid"), clock.now())))
            .build());
  }

  @Test
  void testSuccess_removingServerStatusValue_sendsPollMessage() throws Exception {
    setEppInput("domain_update_remove_server_statuses.xml");
    persistReferencedEntities();
    persistDomain();
    persistResource(
        reloadResourceByForeignKey()
            .asBuilder()
            .setPersistedCurrentSponsorRegistrarId("NewRegistrar")
            .setStatusValues(
                ImmutableSet.of(
                    SERVER_DELETE_PROHIBITED,
                    SERVER_TRANSFER_PROHIBITED,
                    SERVER_UPDATE_PROHIBITED,
                    CLIENT_DELETE_PROHIBITED,
                    CLIENT_RENEW_PROHIBITED,
                    CLIENT_HOLD,
                    SERVER_HOLD))
            .build());
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    Domain updatedDomain = reloadResourceByForeignKey();
    assertPollMessagesForResource(
        updatedDomain,
        new PollMessage.OneTime.Builder()
            .setEventTime(clock.now())
            .setHistoryEntry(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
            .setRegistrarId("NewRegistrar")
            .setMsg(
                "The registry administrator has removed the status(es) [serverHold,"
                    + " serverTransferProhibited, serverUpdateProhibited].")
            .setResponseData(
                ImmutableList.of(
                    DomainPendingActionNotificationResponse.create(
                        "example.tld", true, Trid.create("ABC-12345", "server-trid"), clock.now())))
            .build());
  }

  @Test
  void testSuccess_addingAndRemovingServerStatusValues_sendsPollMessage() throws Exception {
    setEppInput("domain_update_change_server_statuses.xml");
    persistReferencedEntities();
    persistDomain();
    persistResource(
        reloadResourceByForeignKey()
            .asBuilder()
            .setPersistedCurrentSponsorRegistrarId("NewRegistrar")
            .setStatusValues(
                ImmutableSet.of(
                    SERVER_DELETE_PROHIBITED,
                    SERVER_TRANSFER_PROHIBITED,
                    CLIENT_DELETE_PROHIBITED,
                    CLIENT_RENEW_PROHIBITED))
            .build());
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
    Domain updatedDomain = reloadResourceByForeignKey();
    assertPollMessagesForResource(
        updatedDomain,
        new PollMessage.OneTime.Builder()
            .setEventTime(clock.now())
            .setHistoryEntry(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
            .setRegistrarId("NewRegistrar")
            .setMsg(
                "The registry administrator has added the status(es) [serverHold,"
                    + " serverRenewProhibited] and removed the status(es)"
                    + " [serverTransferProhibited].")
            .setResponseData(
                ImmutableList.of(
                    DomainPendingActionNotificationResponse.create(
                        "example.tld", true, Trid.create("ABC-12345", "server-trid"), clock.now())))
            .build());
  }

  @Test
  void testFailure_serverUpdateProhibited_prohibitsNonSuperuserUpdates() throws Exception {
    persistReferencedEntities();
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .addStatusValue(SERVER_UPDATE_PROHIBITED)
            .build());
    Exception e = assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
    assertThat(e).hasMessageThat().contains("serverUpdateProhibited");
  }

  @Test
  void testSuccess_serverUpdateProhibited_allowsSuperuserUpdates() throws Exception {
    persistReferencedEntities();
    persistResource(persistDomain().asBuilder().addStatusValue(SERVER_UPDATE_PROHIBITED).build());
    clock.advanceOneMilli();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
  }

  @Test
  void testFailure_serverUpdateProhibited_notSettableWithoutSuperuser() throws Exception {
    setEppInput("domain_update_add_registry_lock.xml");
    persistReferencedEntities();
    persistDomain();
    Exception e = assertThrows(StatusNotClientSettableException.class, this::runFlow);
    assertThat(e).hasMessageThat().contains("serverUpdateProhibited");
  }

  @Test
  void testSuccess_serverUpdateProhibited_isSettableWithSuperuser() throws Exception {
    setEppInput("domain_update_add_registry_lock.xml");
    persistReferencedEntities();
    persistDomain();
    runFlowAssertResponse(
        CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
  }

  @Test
  void testFailure_clientUpdateProhibited() throws Exception {
    createTld("com");
    setEppInput("domain_update_authinfo.xml");
    persistReferencedEntities();
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setStatusValues(ImmutableSet.of(CLIENT_UPDATE_PROHIBITED))
            .build());
    EppException thrown =
        assertThrows(ResourceHasClientUpdateProhibitedException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_serverUpdateProhibited() throws Exception {
    persistReferencedEntities();
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setStatusValues(ImmutableSet.of(SERVER_UPDATE_PROHIBITED))
            .build());
    ResourceStatusProhibitsOperationException thrown =
        assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("serverUpdateProhibited");
  }

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

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

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

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

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

  @Test
  void testFailure_sameNameserverAddedAndRemoved() throws Exception {
    setEppInput("domain_update_add_remove_same_host.xml");
    persistReferencedEntities();
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setNameservers(
                ImmutableSet.of(
                    loadResource(Host.class, "ns1.example.foo", clock.now()).get().createVKey()))
            .build());
    EppException thrown = assertThrows(AddRemoveSameValueException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_addExistingNameserver() throws Exception {
    Host host = persistActiveHost("ns2.example.foo");
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setNameservers(ImmutableSet.of(host.createVKey()))
            .build());
    setEppInput("domain_update_add_nameserver.xml");
    assertAboutEppExceptions()
        .that(assertThrows(AddExistingValueException.class, this::runFlow))
        .marshalsToXml();
  }

  @Test
  void testFailure_addExistingStatusValue() throws Exception {
    persistResource(
        DatabaseHelper.newDomain(getUniqueIdFromCommand())
            .asBuilder()
            .setStatusValues(ImmutableSet.of(CLIENT_RENEW_PROHIBITED))
            .build());
    setEppInput("domain_update_add_non_server_status.xml");
    assertAboutEppExceptions()
        .that(assertThrows(AddExistingValueException.class, this::runFlow))
        .marshalsToXml();
  }

  @Test
  void testFailure_removeNonexistentNameserver() throws Exception {
    persistActiveDomain(getUniqueIdFromCommand());
    persistActiveHost("ns1.example.foo");
    setEppInput("domain_update_remove_nameserver.xml");
    assertAboutEppExceptions()
        .that(assertThrows(RemoveNonexistentValueException.class, this::runFlow))
        .marshalsToXml();
  }

  @Test
  void testFailure_removeNonexistentStatusValue() throws Exception {
    persistActiveDomain(getUniqueIdFromCommand());
    setEppInput("domain_update_remove_client_update_prohibited.xml");
    assertAboutEppExceptions()
        .that(assertThrows(RemoveNonexistentValueException.class, this::runFlow))
        .marshalsToXml();
  }

  @Test
  void testFailure_minimumDataset_addingNewRegistrantFails() throws Exception {
    persistReferencedEntities();
    persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
    // This EPP sets the registrant to sh8013, whereas in our test setup it is absent.
    setEppInput("domain_update_registrant.xml");
    RegistrantProhibitedException thrown =
        assertThrows(RegistrantProhibitedException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testFailure_addPendingDeleteHost() throws Exception {
    persistReferencedEntities();
    persistDomain();
    persistResource(
        loadResource(Host.class, "ns2.example.foo", clock.now())
            .get()
            .asBuilder()
            .addStatusValue(PENDING_DELETE)
            .build());
    clock.advanceOneMilli();
    LinkedResourceInPendingDeleteProhibitsOperationException thrown =
        assertThrows(LinkedResourceInPendingDeleteProhibitsOperationException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("ns2.example.foo");
  }

  @Test
  void testFailure_addedNameserverDisallowedInTld() throws Exception {
    persistReferencedEntities();
    persistDomain();
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setAllowedFullyQualifiedHostNames(ImmutableSet.of("ns1.example.foo"))
            .build());
    NameserversNotAllowedForTldException thrown =
        assertThrows(NameserversNotAllowedForTldException.class, this::runFlow);
    assertThat(thrown).hasMessageThat().contains("ns2.example.foo");
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_tldWithNameserverAllowList_removeNameserver() throws Exception {
    setEppInput("domain_update_remove_nameserver.xml");
    persistReferencedEntities();
    persistDomain();
    persistResource(
        reloadResourceByForeignKey()
            .asBuilder()
            .addNameserver(
                loadResource(Host.class, "ns2.example.foo", clock.now()).get().createVKey())
            .build());
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setAllowedFullyQualifiedHostNames(
                ImmutableSet.of("ns1.example.foo", "ns2.example.foo"))
            .build());
    assertThat(reloadResourceByForeignKey().getNameservers())
        .contains(loadResource(Host.class, "ns1.example.foo", clock.now()).get().createVKey());
    clock.advanceOneMilli();
    runFlow();
    assertThat(reloadResourceByForeignKey().getNameservers())
        .doesNotContain(
            loadResource(Host.class, "ns1.example.foo", clock.now())
                .map(ImmutableObject::createVKey)
                .get());
  }

  @Test
  void testFailure_tldWithNameserverAllowList_removeLastNameserver() throws Exception {
    persistReferencedEntities();
    persistDomain();
    setEppInput("domain_update_remove_nameserver.xml");
    persistResource(
        Tld.get("tld")
            .asBuilder()
            .setAllowedFullyQualifiedHostNames(ImmutableSet.of("ns1.example.foo"))
            .build());
    EppException thrown =
        assertThrows(
            NameserversNotSpecifiedForTldWithNameserverAllowListException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testSuccess_domainCreateNotRestricted_doNotApplyServerProhibitedStatusCodes()
      throws Exception {
    persistReferencedEntities();
    persistDomain();
    doSuccessfulTest();
    assertAboutDomains().that(reloadResourceByForeignKey()).hasExactlyStatusValues(CLIENT_HOLD);
  }

  // This test should throw an exception, because the fee extension is required when the fee is not
  // zero.
  @Test
  void testFailure_missingFeeOnNonFreeUpdate() throws Exception {
    setEppInput("domain_update_wildcard.xml", ImmutableMap.of("DOMAIN", "non-free-update.tld"));
    persistReferencedEntities();
    persistDomain();
    EppException thrown =
        assertThrows(FeesRequiredForNonFreeOperationException.class, this::runFlow);
    assertAboutEppExceptions().that(thrown).marshalsToXml();
  }

  @Test
  void testIcannActivityReportField_getsLogged() throws Exception {
    persistReferencedEntities();
    persistDomain();
    runFlow();
    assertIcannReportingActivityFieldLogged("srs-dom-update");
    assertTldsFieldLogged("tld");
  }

  @Test
  void testSuperuserExtension_turnsOffAutorenew() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput("domain_update_superuser_extension.xml", ImmutableMap.of("AUTORENEWS", "false"));
    Instant expirationTime = plusYears(clock.now(), 3);
    persistReferencedEntities();
    persistResource(
        persistDomain().asBuilder().setRegistrationExpirationTime(expirationTime).build());
    clock.advanceOneMilli();
    runFlowAsSuperuser();
    assertAboutDomains().that(reloadResourceByForeignKey()).hasAutorenewEndTime(expirationTime);
  }

  @Test
  void testSuperuserExtension_turnsOnAutorenew() throws Exception {
    eppRequestSource = EppRequestSource.TOOL;
    setEppInput("domain_update_superuser_extension.xml", ImmutableMap.of("AUTORENEWS", "true"));
    Instant expirationTime = plusYears(clock.now(), 3);
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setAutorenewEndTime(Optional.of(expirationTime))
            .setRegistrationExpirationTime(expirationTime)
            .build());
    clock.advanceOneMilli();
    runFlowAsSuperuser();
    assertAboutDomains().that(reloadResourceByForeignKey()).hasNoAutorenewEndTime();
  }

  @Test
  void testAddDnsPublishStatus_enqueueDnsTask() throws Exception {
    setEppInput(
        "domain_update_status_change.xml",
        ImmutableMap.of("STATUS_ADD", "clientHold", "STATUS_REM", "clientTransferProhibited"));
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setDomainName("example.tld")
            .setStatusValues(ImmutableSet.of(CLIENT_TRANSFER_PROHIBITED))
            .build());
    runFlowAsSuperuser();
    assertDomainDnsRequests("example.tld");
  }

  @Test
  void testRemoveEveryDnsPublishStatus_enqueueDnsTask() throws Exception {
    setEppInput(
        "domain_update_status_change.xml",
        ImmutableMap.of("STATUS_REM", "serverHold", "STATUS_ADD", "clientTransferProhibited"));
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setDomainName("example.tld")
            .setStatusValues(ImmutableSet.of(SERVER_HOLD))
            .build());
    runFlowAsSuperuser();
    assertDomainDnsRequests("example.tld");
  }

  @Test
  void testChangeSomeOrNoChangeDnsPublishStatus_doNotEnqueueDnsTask() throws Exception {
    setEppInput(
        "domain_update_status_change.xml",
        ImmutableMap.of("STATUS_ADD", "clientUpdateProhibited", "STATUS_REM", "pendingDelete"));
    persistReferencedEntities();
    persistResource(
        persistDomain()
            .asBuilder()
            .setDomainName("example.tld")
            .setStatusValues(ImmutableSet.of(PENDING_DELETE, SERVER_HOLD))
            .build());
    runFlowAsSuperuser();
    assertNoDnsRequests();
  }
}
