<?php

namespace Drupal\Tests\autosave_form\FunctionalJavascript\ContentEntity;

use Drupal\autosave_form\Storage\AutosaveEntityFormStorageInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\autosave_form\FunctionalJavascript\AutosaveFormTestBase;
use Drupal\user\EntityOwnerInterface;

/**
 * Base test class for testing autosave support for entity forms.
 */
abstract class ContentEntityAutosaveFormTestBase extends AutosaveFormTestBase {

  /**
   * The entity type to be tested.
   *
   * @var string
   */
  protected $entityType;

  /**
   * The bundle of the entity type to be tested.
   *
   * @var string
   */
  protected $bundle;

  /**
   * The original entity title.
   *
   * @var string
   */
  protected $originalEntityTitle;

  /**
   * The changed entity title.
   *
   * @var string
   */
  protected $changedEntityTitle;

  /**
   * The name of the field with unlimited cardinality to test ajax requests.
   *
   * @var string
   */
  protected $unlimitedCardinalityField = 'autosave_unlimited_field';

  /**
   * The name of the required test field.
   *
   * @var string
   */
  protected $requiredField = 'autosave_required_field';

  /**
   * Count of the changes to make.
   *
   * @var int
   */
  protected $testAutosaveFormExistingEntityChangesCount = 5;

  /**
   * {@inheritdoc}
   */
  protected function prepareSetUp() {
    $this->createMultipleTestField();
    $this->createRequiredTestField();
    parent::prepareSetUp();

    $this->originalEntityTitle = NULL;
    $this->changedEntityTitle = NULL;
  }

  /**
   * Tests autosave.
   */
  public function testAutosaveForms() {
    // It looks like phantomjs is crashing if an ajax autosave form request is
    // still running when we go the next test and visit the entity form of
    // different entity in which case the currently running ajax autosave form
    // request will be aborted. In order to prevent this crash we re-log the
    // user prior to each test.
    $this->doTestAutosaveFormNewEntity();

    $this->relogUser();
    $this->doTestAutosaveFormExistingEntity();

    $this->relogUser();
    $this->doTestSavingRestoredEntityForm();

    $this->relogUser();
    $this->doTestConcurrentEditing();

    $this->relogUser();
    $this->doTestAutosaveAfterFormValidationFail();

    $this->relogUser();
    $this->doTestAutosaveStatesPurgingOnConfigEvent();
  }

  /**
   * Tests that the autosave form library is not loaded on new entity forms.
   */
  protected function doTestAutosaveFormNewEntity() {
    $this->drupalGet($this->getCreateNewEntityURL());
    $this->assertAutosaveFormLibraryLoaded(TRUE);
  }

  /**
   * Tests the autosave support on entity forms.
   */
  protected function doTestAutosaveFormExistingEntity() {
    $entity = $this->createTestEntity();
    $entity_id = $entity->id();
    $entity_form_edit_url = $entity->toUrl('edit-form');

    $this->drupalGet($entity_form_edit_url);
    $this->assertAutosaveFormLibraryLoaded(TRUE);
    $this->assertOriginalEntityTitleAsPageTitle();

    // Wait for at least having two autosave submits being executed and assert
    // that with no changes there will be no autosave states created.
    $this->assertTrue($this->waitForAutosaveSubmits(2));
    $this->assertEquals(0, $this->getCountAutosaveEntries($entity_id));

    $latest_autosave_timestamp_per_change = $this->makeAllEntityFormChanges($entity_id);

    // Test the autosave restore of each change.
    for ($change_id = $this->testAutosaveFormExistingEntityChangesCount; $change_id > 0; $change_id--) {
      // Reload page otherwise new auto saves will be done and time checks get incorrect
      $this->drupalGet($entity_form_edit_url);
      if (($last_autosave_timestamp = $this->getLastAutosaveTimestamp($entity_id)) && ($last_autosave_timestamp > $latest_autosave_timestamp_per_change[$change_id])) {
        $delete_timestamps = range($latest_autosave_timestamp_per_change[$change_id] + 1, $last_autosave_timestamp);
        $this->deleteAutosavedStates($delete_timestamps);
      }

      $this->reloadPageAndRestore($entity_form_edit_url, $this->getLastAutosaveTimestamp($entity_id));
      $this->assertCorrectlyRestoredEntityFormState($change_id);
    }
  }

  /**
   * Tests saving an entity form restored from an autosaved state.
   */
  protected function doTestSavingRestoredEntityForm() {
    $entity = $this->createTestEntity();
    $entity_form_edit_url = $entity->toUrl('edit-form');

    $this->drupalGet($entity_form_edit_url);

    $this->makeAllEntityFormChanges($entity->id());

    // Assure that an autosave submission has run.
    $this->assertTrue($this->waitForAutosaveSubmits(1));

    $this->reloadPageAndRestore($entity_form_edit_url, $this->getLastAutosaveTimestamp($entity->id()));
    $this->saveForm();

    $this->finalizeTestSavingRestoredEntityForm($entity->id());
  }

  /**
   * Submits the current form.
   */
  protected function saveForm() {
    $this->submitForm([], t('Save'));
  }

  /**
   * Tests concurrent editing.
   */
  protected function doTestConcurrentEditing() {
    $entity = $this->createTestEntity();

    // This test supports only entities implementing the entity changed
    // interface.
    if (!($entity instanceof EntityChangedInterface)) {
      return;
    }

    // Make one change and assert that an autosave entry has been created for
    // it.
    $entity_form_edit_url = $entity->toUrl('edit-form');
    $this->drupalGet($entity_form_edit_url);
    $this->assertTrue($this->waitForAutosaveSubmits(1));
    $this->makeEntityFormChange(1);
    $this->assertTrue($this->waitForAutosaveSubmits(2));
    $this->assertTrue($this->getCountAutosaveEntries($entity->id()) > 0);
    $this->assertAutosaveIsRunning(TRUE);

    // Meanwhile simulate saving by another user in the background.
    $entity->setChangedTime($entity->getChangedTime() + 1)
      ->save();

    // Ensure that after the entity is being saved in the background the
    // autosave submission is disabled by expecting maximum of one autosave
    // submission which will show the alert message on the page. If when the
    // following code is executed the autosave submission has not yet run then
    // there will be one autosave submission and afterwards autosave submission
    // should be disabled. If however when the following code is executed the
    // autosave submission has already run then autosave submission should have
    // been disabled already. In both cases we assert that we expect zero or
    // one autosave submission, but not more than one.
    $this->assertFalse($this->waitForAutosaveSubmits(2));

    $this->assertAutosaveIsRunning(FALSE);
    $this->assertEquals(0, $this->getCountAutosaveEntries($entity->id()));

    $message = $this->config('autosave_form.messages')
      ->get('entity_saved_in_background_alert_message');
    $this->assertSession()->responseContains($message);
  }

  /**
   * Tests the autosave message not being shown on reload after validation fail.
   */
  protected function doTestAutosaveAfterFormValidationFail() {
    // Create a test entity and ensure that the required field is not filled in
    // order to trigger a validation error on entity form submission.
    $entity = $this->createTestEntity();

    // Disable the HTML5 validation as it prevents the form submission when a
    // required field is empty, which however we want to do on purpose to test
    // how autosave_form behaves when a form is returned with validation errors,
    // but for that it has to be submitted first.
    \Drupal::state()->set('disable_html5_validation', TRUE);

    $entity_form_edit_url = $entity->toUrl('edit-form');
    $this->drupalGet($entity_form_edit_url);

    // Assure that the initial autosave submission for gathering initial input
    // has run.
    $this->assertTrue($this->waitForAutosaveSubmits(1));

    // Make the first change to trigger an autosave state creation, but do not
    // fill the required field.
    $this->alterTitleField();
    // Ensure a validation fail will occur.
    $this->emptyRequiredFieldTestAutosaveAfterFormValidationFail();

    // Ensure an autosave state is saved.
    $this->assertTrue($this->waitForAutosaveSubmits(2));
    $before_submission_autosave_entries = $this->getCountAutosaveEntries($entity->id());
    $this->assertTrue($before_submission_autosave_entries > 0);

    // Submit the form.
    $this->saveForm();

    // Do not prevent the HTML5 validation anymore.
    \Drupal::state()->delete('disable_html5_validation');

    $this->logHtmlOutput(__FUNCTION__ . ' after validation fail.');

    // Ensure the validation fail message is shown.
    $error_messages = $this->getSession()->getPage()->find('css', '.messages--error');
    $this->assertNotNull($error_messages);

    // Ensure that the autosave resume/discard message is not shown.
    $this->assertAutosaveResumeDiscardMessageIsShown(FALSE, $this->getLastAutosaveTimestamp($entity->id()));

    // Ensure that autosave submissions are running.
    $this->assertTrue($this->waitForAutosaveSubmits(2));
    // Ensure no further autosave states are being created without changes.
    $this->assertEquals($before_submission_autosave_entries, $this->getCountAutosaveEntries($entity->id()));
  }

  /**
   * Tests that autosave states are purged on modifying a form related config.
   */
  protected function doTestAutosaveStatesPurgingOnConfigEvent() {
    $entity = $this->createTestEntity();
    $entity_id = $entity->id();
    $entity_form_edit_url = $entity->toUrl('edit-form');

    $create_autosave_state = function () use ($entity_form_edit_url, $entity_id) {
      $this->drupalGet($entity_form_edit_url);
      // Wait for at least having two autosave submits being executed, make a
      // change wait for two more autosave submits and ensure an autosave state
      // has been created.
      $this->assertTrue($this->waitForAutosaveSubmits(2));
      $this->makeEntityFormChange(1);
      $this->assertTrue($this->waitForAutosaveSubmits(2));
      $this->assertEquals(1, $this->getCountAutosaveEntries($entity_id));
    };

    // Make a non-significant modification on a form related config and ensure
    // that the autosave state hasn't been purged.
    $create_autosave_state();
    $field_config = FieldConfig::loadByName($this->entityType, $this->bundle, $this->unlimitedCardinalityField);
    $field_config->setLabel('New Label Test AutosaveState Purge')
      ->save();
    $this->assertEquals(1, $this->getCountAutosaveEntries($entity_id));

    // Modify a form related config and ensure that the autosave state has been
    // purged.
    $field_storage = FieldStorageConfig::loadByName($this->entityType, $this->unlimitedCardinalityField);
    $field_storage->setCardinality(10)
      ->save();
    $this->assertEquals(0, $this->getCountAutosaveEntries($entity_id));

    // Delete a form related config and ensure that the autosave state has been
    // purged.
    $create_autosave_state();
    $field_storage = FieldStorageConfig::loadByName($this->entityType, $this->unlimitedCardinalityField);
    $field_storage->delete();
    $this->assertEquals(0, $this->getCountAutosaveEntries($entity_id));

    // Create a form related config and ensure that the autosave state has been
    // purged.
    $create_autosave_state();
    $this->createMultipleTestField();
    $this->assertEquals(0, $this->getCountAutosaveEntries($entity_id));
  }

  /**
   * Empties a required field.
   *
   * Helper method for ::doTestAutosaveAfterFormValidationFail() to empty a
   * required field on the entity form in order to trigger a form validation
   * fail on form submission.
   */
  protected function emptyRequiredFieldTestAutosaveAfterFormValidationFail() {
    $this->fillTestField($this->requiredField, 0, '');
  }

  /**
   * Tests correctly saved entity after autosave restore.
   *
   * Helper method for ::doTestSavingRestoredEntityForm() to test the saved
   * entity.
   *
   * @param mixed $entity_id
   *   The ID of the entity.
   */
  protected function finalizeTestSavingRestoredEntityForm($entity_id) {
    $entity = $this->reloadEntity($entity_id);
    // Change 1.
    $this->assertEquals($entity->label(), $this->changedEntityTitle);
    // Changes 2, 3 and 4.
    $this->assertEquals(2, $entity->get($this->unlimitedCardinalityField)->count());
    $this->assertEquals('delta 0', $entity->get($this->unlimitedCardinalityField)->get(0)->value);
    $this->assertEquals('delta 1', $entity->get($this->unlimitedCardinalityField)->get(1)->value);
    // Change 5.
    $this->assertEquals('required test field', $entity->get($this->requiredField)->get(0)->value);
  }

  /**
   * Executes all change steps.
   *
   * @param mixed $entity_id
   *   The ID of the entity.
   *
   * @return array
   *   An array keyed by the change ID and having as value the latest autosave
   *   timestamp.
   */
  protected function makeAllEntityFormChanges($entity_id) {
    $this->logHtmlOutput(__FUNCTION__ . ' before changes are made');

    // Assure the first autosave submission for gathering the initial input has
    // been executed before making any changes, otherwise it might happen that
    // a change is made too fast and makes its way into the initial user input
    // used for comparison in order to determine if a new autosave state has
    // to be created or not.
    $this->assertTrue($this->waitForAutosaveSubmits(1));

    $latest_autosave_timestamp_per_change = [];

    for ($change_id = 1; $change_id <= $this->testAutosaveFormExistingEntityChangesCount; $change_id++) {
      $before_change_autosave_entries = $this->getCountAutosaveEntries($entity_id);

      $this->makeEntityFormChange($change_id);

      // Assert that a new autosave has been created, but wait for at least two
      // autosave submits to exclude any race conditions.
      $this->assertTrue($this->waitForAutosaveSubmits(2));
      $after_change_autosave_entries = $this->getCountAutosaveEntries($entity_id);
      $this->assertTrue($after_change_autosave_entries > $before_change_autosave_entries);

      // Wait for at least two more autosave submits to ensure no additional
      // autosave states are being created.
      $this->assertTrue($this->waitForAutosaveSubmits(2));
      $this->assertEquals($after_change_autosave_entries, $this->getCountAutosaveEntries($entity_id));

      $latest_autosave_timestamp_per_change[$change_id] = $this->getLastAutosaveTimestamp($entity_id);
    }

    $this->logHtmlOutput(__FUNCTION__ . ' after changes are made');

    return $latest_autosave_timestamp_per_change;
  }

  /**
   * Makes a change by the given change/step ID.
   *
   * @param $change_id
   *   The change id of the change to make.
   */
  protected function makeEntityFormChange($change_id) {
    $this->logHtmlOutput(__FUNCTION__ . ' before change ' . $change_id);

    switch ($change_id) {
      case 1:
        // Alter the title field.
        $this->alterTitleField();
        break;

      case 2:
        // Fill the first item of the test field and assert that a new autosave
        // entry has been created.
        $this->fillTestField($this->unlimitedCardinalityField, 0, 'delta 0');
        break;

      case 3:
        // Add new item to the test field and assert that a new autosave entry has
        // been created.
        $new_delta_expected = 1;
        $this->addItemToUnlimitedTestField($new_delta_expected);
        break;

      case 4:
        // Fill the new item of the test field and assert that a new autosave
        // entry has been created.
        $field_delta = 1;
        $this->fillTestField($this->unlimitedCardinalityField, $field_delta, 'delta 1');
        break;

      case 5:
        // Fill the required test field.
        $this->fillTestField($this->requiredField, 0, 'required test field');
        break;
    }

    $this->logHtmlOutput(__FUNCTION__ . ' after change ' . $change_id);
  }

  /**
   * Tests the restored autosave state by the change ID.
   *
   * @param $change_id
   *  The change ID for which to test the restored autosaved state.
   *
   * @see ::makeEntityFormChange().
   */
  protected function assertCorrectlyRestoredEntityFormState($change_id) {
    $page = $this->getSession()->getPage();

    $this->logHtmlOutput(__FUNCTION__ . ' before restore of change ' . $change_id);

    switch ($change_id) {
      case 5:
        $test_field_delta_0 = $page->findField($this->requiredField . '[0][value]');
        $this->assertNotEmpty($test_field_delta_0);
        $this->assertEquals('required test field', $test_field_delta_0->getValue());

      case 4:
        $test_field_delta_1 = $page->findField($this->unlimitedCardinalityField . '[1][value]');
        $this->assertNotEmpty($test_field_delta_1);
        $this->assertEquals('delta 1', $test_field_delta_1->getValue());

      case 3:
        // Not applying because of case 4.
        if ($change_id == 3) {
          $test_field_delta_1 = $page->findField($this->unlimitedCardinalityField . '[1][value]');
          $this->assertNotEmpty($test_field_delta_1);
          $this->assertEquals('', $test_field_delta_1->getValue());
        }

      case 2:
        $test_field_delta_0 = $page->findField($this->unlimitedCardinalityField . '[0][value]');
        $this->assertNotEmpty($test_field_delta_0);
        $this->assertEquals('delta 0', $test_field_delta_0->getValue());

      case 1:
        $this->assertOriginalEntityTitleAsPageTitle();
        $entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityType);
        $this->assertEquals($this->changedEntityTitle, $page->findField($entity_type->getKey('label') . '[0][value]')->getValue());
        break;
    }

    $this->logHtmlOutput(__FUNCTION__ . ' after restore of change ' . $change_id);
  }

  /**
   * Alters the title field of the entity.
   *
   * @param string $changed_title
   *   The title to use to set on the title form field.
   */
  protected function alterTitleField($changed_title = 'changed title') {
    if ($label_field_name = \Drupal::entityTypeManager()->getDefinition($this->entityType)->getKey('label')) {
      $this->changedEntityTitle = $changed_title;
      $this->getSession()->getPage()->fillField($label_field_name . '[0][value]', $this->changedEntityTitle);
    }
  }

  /**
   * Fields a field item by its name and delta.
   *
   * @param $field_name
   *   The name of the field.
   * @param $delta
   *   The delta item of the field.
   * @param $value
   *   The value.
   */
  protected function fillTestField($field_name, $delta, $value) {
    $this->getSession()->getPage()->fillField("{$field_name}[{$delta}][value]", $value);
  }

  /**
   * Adds a new item to the unlimited test field.
   *
   * @param $delta
   *   The new excepted delta.
   */
  protected function addItemToUnlimitedTestField($delta) {
    $this->logHtmlOutput(__FUNCTION__ . ' before adding a new item to unlimited field');

    $page = $this->getSession()->getPage();
    $add_button = $page->find('css', '[data-drupal-selector="edit-' . Html::cleanCssIdentifier($this->unlimitedCardinalityField) . '-add-more"]');
    $this->assertTrue(!empty($add_button));
    $add_button->press();
    $result = $this->assertSession()->waitForElement('css', "[name=\"{$this->unlimitedCardinalityField}[{$delta}][value]\"]");
    $this->assertNotEmpty($result);

    $this->logHtmlOutput(__FUNCTION__ . ' after adding a new item to unlimited field');
  }

  /**
   * Returns the currently rendered page title.
   *
   * @return string|NULL
   *   The page title.
   */
  protected function getCurrentPageTitle() {
    $element = $this->getSession()->getPage()->find('css', '.page-title');
    $title = !empty($element) ? $element->getText() : NULL;
    return $title;
  }

  /**
   * Asserts that page title matches the original entity title.
   */
  protected function assertOriginalEntityTitleAsPageTitle() {
    $current_title = $this->getCurrentPageTitle();
    $this->assertTrue(strpos($current_title, $this->originalEntityTitle) !== FALSE);
  }

  /**
   * Gets the count of autosave states.
   *
   * @param mixed $entity_id
   *   The ID of the entity.
   *
   * @return int
   *   The count of autosave entries.
   */
  protected function getCountAutosaveEntries($entity_id) {
    $count = \Drupal::database()
      ->select(AutosaveEntityFormStorageInterface::AUTOSAVE_ENTITY_FORM_TABLE)
      ->condition('entity_id', $entity_id)
      ->countQuery()
      ->execute()
      ->fetchField();
    return $count !== FALSE ? $count : 0;
  }

  /**
   * Gets the count of autosave unique session entries.
   *
   * @return int
   *   The count of autosave unique session entries.
   */
  protected function getCountAutosaveSessionEntries() {
    $count = \Drupal::database()
      ->select(AutosaveEntityFormStorageInterface::AUTOSAVE_ENTITY_FORM_TABLE)
      ->groupBy('form_session_id')
      ->countQuery()
      ->execute()
      ->fetchField();
    return $count !== FALSE ? $count : 0;
  }

  /**
   * Returns the timestamp of the last autosave entry for the given entity ID.
   *
   * @param mixed $entity_id
   *   The ID of the entity.
   *
   * @return int|null
   *   The timestamp of the last autosave entry.
   */
  protected function getLastAutosaveTimestamp($entity_id) {
    $timestamp = \Drupal::database()
      ->select(AutosaveEntityFormStorageInterface::AUTOSAVE_ENTITY_FORM_TABLE, 't')
      ->fields('t', ['timestamp'])
      ->condition('entity_id', $entity_id)
      ->orderBy('timestamp', 'DESC')
      ->execute()
      ->fetchField();
    return $timestamp !== FALSE ? $timestamp : NULL;
  }

  /**
   * Deletes the autosaved states.
   *
   * @param $timestamps
   *   (optional) If specified, only the autosaved states with the given
   *   timestamps will be deleted.
   */
  protected function deleteAutosavedStates(array $timestamps = NULL) {
    $query = \Drupal::database()
      ->delete(AutosaveEntityFormStorageInterface::AUTOSAVE_ENTITY_FORM_TABLE);
    if (isset($timestamps)) {
      $query->condition('timestamp', $timestamps, 'IN');
    }
    $query->execute();
  }

  /**
   * Creates a new test entity.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The newly created entity.
   */
  protected function createTestEntity() {
    $entity_type_manager = \Drupal::entityTypeManager();
    $entity_type =  $entity_type_manager->getDefinition($this->entityType);
    $storage = $entity_type_manager->getStorage($this->entityType);

    $values = ['type' => $this->bundle];
    if ($label_field_name = $entity_type->getKey('label')) {
      $values[$label_field_name] =  'original title';
    }
    $entity = $storage->create($values);

    if ($entity instanceof EntityOwnerInterface) {
      $entity->setOwner($this->webUser);
    }
    elseif ($entity_type->hasKey('uid')) {
      $entity->set('uid', $this->webUser->id());
    }

    $entity->save();

    $this->originalEntityTitle = $entity->label();

    return $entity;
  }

  /**
   * Returns the test entity
   *
   * @param mixed $id
   *   The ID of the entity to load.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   An entity object. NULL if no matching entity is found.
   */
  protected function reloadEntity($id) {
    $storage = \Drupal::entityTypeManager()->getStorage($this->entityType);
    $storage->resetCache([$id]);
    return $storage->load($id);
  }

  /**
   * Creates a multiple field to test ajax submits with.
   */
  protected function createMultipleTestField() {
    if (!FieldStorageConfig::loadByName($this->entityType, $this->unlimitedCardinalityField)) {
      // Create an unlimited cardinality field.
      FieldStorageConfig::create([
        'field_name' => $this->unlimitedCardinalityField,
        'entity_type' => $this->entityType,
        'type' => 'text',
        'cardinality' => -1,
      ])->save();
    }

    if (!FieldConfig::loadByName($this->entityType, $this->bundle, $this->unlimitedCardinalityField)) {
      // Attach an instance of the field to the content type.
      FieldConfig::create([
        'field_name' => $this->unlimitedCardinalityField,
        'entity_type' => $this->entityType,
        'bundle' => $this->bundle,
        'label' => $this->randomMachineName() . '_label',
      ])->save();
      $this->getEntityFormDisplay($this->entityType, $this->bundle, 'default')
        ->setComponent($this->unlimitedCardinalityField, [
          'type' => 'text_textfield',
        ])
        ->save();
    }
  }

  /**
   * Creates a required test field to test ajax submits with.
   */
  protected function createRequiredTestField() {
    if (!FieldStorageConfig::loadByName($this->entityType, $this->requiredField)) {
      // Create a required test field.
      FieldStorageConfig::create([
        'field_name' => $this->requiredField,
        'entity_type' => $this->entityType,
        'type' => 'text',
        'cardinality' => 1,
      ])->save();
    }

    if (!FieldConfig::loadByName($this->entityType, $this->bundle, $this->requiredField)) {
      // Attach an instance of the field to the content type.
      FieldConfig::create([
        'field_name' => $this->requiredField,
        'entity_type' => $this->entityType,
        'bundle' => $this->bundle,
        'label' => $this->requiredField,
        'required' => TRUE,
      ])->save();
      $this->getEntityFormDisplay($this->entityType, $this->bundle, 'default')
        ->setComponent($this->requiredField, [
          'type' => 'text_textfield',
        ])
        ->save();
    }
  }

  /**
   * Logs out and logs in the web user.
   */
  protected function relogUser() {
    $this->drupalLogout();
    $this->drupalLogin($this->webUser);
  }

  /**
   * Returns the URL for creating a new entity.
   *
   * @return string
   *   The url for creating a new entity.
   */
  protected abstract function getCreateNewEntityURL();

  /**
   * Returns the entity form display associated with a bundle and form mode.
   *
   * This is just a wrapper around
   * Drupal\Core\Entity\EntityDisplayRepository::getFormDisplay() for Drupal
   * versions >= 8.8, with a fallback to entity_get_form_display() for prior
   * Drupal versions.
   *
   * @param string $entity_type
   *   The entity type.
   * @param string $bundle
   *   The bundle.
   * @param string $form_mode
   *   The form mode.
   *
   * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
   *   The entity form display associated with the given form mode.
   *
   * @todo Remove this once Drupal 8.7 is no longer supported.
   */
  private function getEntityFormDisplay($entity_type, $bundle, $form_mode) {
    if (version_compare(\Drupal::VERSION, '8.8', '>=')) {
      return \Drupal::service('entity_display.repository')->getFormDisplay($entity_type, $bundle, $form_mode);
    }
    else {
      // Because this code only runs for older Drupal versions, we do not need
      // or want IDEs or the Upgrade Status module warning people about this
      // deprecated code usage. Setting the function name dynamically
      // circumvents those warnings.
      $function = 'entity_get_form_display';
      return $function($entity_type, $bundle, $form_mode);
    }
  }
}
