<?php

namespace Drupal\Tests\quickedit\FunctionalJavascript;

use Drupal\node\Entity\NodeType;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;

/**
 * Tests that Layout Builder functions with Quick Edit.
 *
 * @covers layout_builder_entity_view_alter()
 * @covers layout_builder_quickedit_render_field()
 *
 * @group quickedit
 */
class LayoutBuilderQuickEditTest extends QuickEditJavascriptTestBase {

  use EntityReferenceTestTrait;
  use ContextualLinkClickTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'node',
    'layout_builder',
    'field_ui',
  ];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'starterkit_theme';

  /**
   * The article node under test.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $article;

  /**
   * A user with permissions to edit Articles and use Quick Edit.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $contentAuthorUser;

  /**
   * Whether the test is currently using Layout Builder on the entity.
   *
   * Allows performing assertions with and without Layout Builder.
   *
   * @var bool
   *
   * @see ::assertEntityInstanceFieldStates()
   * @see ::assertEntityInstanceFieldMarkup()
   */
  protected $usingLayoutBuilder = FALSE;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->drupalPlaceBlock('page_title_block');

    // Create the Article node type.
    $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
    $this->article = $this->drupalCreateNode([
      'type' => 'article',
      'title' => 'My Test Node',
      'body' => [
        'value' => 'Hello Layout Builder!',
        'format' => 'plain_text',
      ],
    ]);

    // Log in as a content author who can use Quick Edit and edit Articles.
    $this->contentAuthorUser = $this->drupalCreateUser([
      'access contextual links',
      'access in-place editing',
      'access content',
      'edit any article content',
    ]);
    $this->drupalLogin($this->contentAuthorUser);
  }

  /**
   * Tests that Quick Edit still works even when there are duplicate fields.
   *
   * @see https://www.drupal.org/project/drupal/issues/3041850
   */
  public function testQuickEditIgnoresDuplicateFields() {
    // Place the body field a second time using Layout Builder.
    $this->enableLayouts('admin/structure/types/manage/article/display/default');
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();
    $this->loginLayoutAdmin();
    $this->drupalGet('admin/structure/types/manage/article/display/default/layout');
    $page->clickLink('Add block');
    $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
    $assert_session->waitForElementVisible('named', ['link', 'Body']);
    $page->clickLink('Body');
    $assert_session->waitForElementVisible('named', ['button', 'Add block']);
    $page->pressButton('Add block');
    $assert_session->waitForElementVisible('named', ['button', 'Save layout']);
    $page->pressButton('Save layout');
    $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status'));
    $assert_session->pageTextContains('The layout has been saved.');

    $this->drupalLogin($this->contentAuthorUser);
    $this->usingLayoutBuilder = TRUE;
    $this->assertQuickEditInit(['title']);
    $this->drupalLogin($this->drupalCreateUser([
      'access contextual links',
      'access in-place editing',
      'access content',
      'edit any article content',
      'administer nodes',
    ]));
    $this->assertQuickEditInit(['title', 'uid', 'created']);
  }

  /**
   * Tests Quick Edit boots correctly with Layout Builder defaults & overrides.
   *
   * @param bool $use_revisions
   *   If revisions are used.
   * @param bool $admin_permission
   *   Whether to grant admin permissions to the user created for the test.
   *
   * @dataProvider providerEnableDisableLayoutBuilder
   */
  public function testEnableDisableLayoutBuilder($use_revisions, $admin_permission = FALSE) {
    if (!$use_revisions) {
      $content_type = NodeType::load('article');
      $content_type->setNewRevision(FALSE);
      $content_type->save();
    }
    $fields = [
      'title',
      'body',
    ];
    if ($admin_permission) {
      $fields = array_merge($fields, ['uid', 'created']);
      $this->drupalLogin($this->drupalCreateUser([
        'access contextual links',
        'access in-place editing',
        'access content',
        'edit any article content',
        'administer nodes',
      ]));
    }

    // Test article with Layout Builder disabled.
    $this->assertQuickEditInit($fields);

    // Test article with Layout Builder enabled.
    $this->enableLayouts('admin/structure/types/manage/article/display/default');
    $this->usingLayoutBuilder = TRUE;
    $this->assertQuickEditInit($fields);

    // Test article with Layout Builder override.
    $this->createLayoutOverride();
    $this->assertQuickEditInit($fields);

    // If we're using revisions, it's not possible to disable Layout Builder
    // without deleting the node (unless the revisions containing the override
    // would be deleted).
    if (!$use_revisions) {
      // Test article with Layout Builder when reverted back to defaults.
      $this->revertLayoutToDefaults();
      $this->assertQuickEditInit($fields);

      // Test with Layout Builder disabled after being enabled.
      $this->usingLayoutBuilder = FALSE;
      $this->disableLayoutBuilder('admin/structure/types/manage/article/display/default');
      $this->assertQuickEditInit($fields);
    }
  }

  /**
   * DataProvider for testEnableDisableLayoutBuilder().
   */
  public function providerEnableDisableLayoutBuilder() {
    return [
      'use revisions, not admin' => [TRUE],
      'do not use revisions, not admin' => [FALSE],
      'use revisions, admin' => [TRUE, TRUE],
      'do not use revisions, admin' => [FALSE, TRUE],
    ];
  }

  /**
   * Enables layouts at an admin path.
   *
   * @param string $path
   *   The manage display path.
   */
  protected function enableLayouts($path) {
    // Save the current user to re-login after Layout Builder changes.
    $user = $this->loggedInUser;
    $this->loginLayoutAdmin();
    $page = $this->getSession()->getPage();
    $this->drupalGet($path);
    $page->checkField('layout[enabled]');
    $page->checkField('layout[allow_custom]');
    $page->pressButton('Save');
    $this->drupalLogin($user);
  }

  /**
   * {@inheritdoc}
   */
  protected function assertEntityInstanceFieldStates($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_states): void {
    parent::assertEntityInstanceFieldStates($entity_type_id, $entity_id, $entity_instance_id, $this->replaceLayoutBuilderFieldIdKeys($expected_field_states));
  }

  /**
   * {@inheritdoc}
   */
  protected function assertEntityInstanceFieldMarkup($expected_field_attributes): void {
    if (func_num_args() === 4) {
      $expected_field_attributes = func_get_arg(3);
      @trigger_error('Calling ' . __METHOD__ . '() with 4 arguments is deprecated in drupal:9.1.0 and will throw an error in drupal:10.0.0. See https://www.drupal.org/project/drupal/issues/3037436', E_USER_DEPRECATED);
    }
    parent::assertEntityInstanceFieldMarkup($this->replaceLayoutBuilderFieldIdKeys($expected_field_attributes));
  }

  /**
   * Replaces the array keys with Layout Builder field IDs when needed.
   *
   * @param array $array
   *   The array with field IDs as keys.
   *
   * @return array
   *   The array with the keys replaced.
   */
  protected function replaceLayoutBuilderFieldIdKeys(array $array) {
    if (!$this->usingLayoutBuilder) {
      return $array;
    }
    $replacement = [];
    foreach ($array as $field_key => $value) {
      $new_field_key = $this->getQuickEditFieldId($field_key);
      $replacement[$new_field_key] = $value;
    }
    return $replacement;
  }

  /**
   * Login the Layout admin user for the test.
   */
  protected function loginLayoutAdmin() {
    // Enable for the Layout Builder.
    $this->drupalLogin($this->drupalCreateUser([
      'configure any layout',
      'access content',
      'administer node display',
      'administer node fields',
      'administer blocks',
    ]));
  }

  /**
   * Creates a layout override.
   */
  protected function createLayoutOverride() {
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();

    // Save the current user to re-login after Layout Builder changes.
    $user = $this->loggedInUser;
    $this->loginLayoutAdmin();
    $this->drupalGet('node/' . $this->article->id() . '/layout');

    $page->pressButton('Save layout');
    $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status'));
    $assert_session->pageTextContains('The layout override has been saved.');
    $this->drupalLogin($user);
  }

  /**
   * Reverts a layout override.
   */
  protected function revertLayoutToDefaults() {
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();
    // Save the current user to re-login after Layout Builder changes.
    $user = $this->loggedInUser;
    $this->loginLayoutAdmin();
    $this->drupalGet('node/' . $this->article->id() . '/layout');
    $assert_session->buttonExists('Revert to defaults');
    $page->pressButton('Revert to defaults');
    $page->pressButton('Revert');
    $assert_session->pageTextContains('The layout has been reverted back to defaults.');
    $this->drupalLogin($user);
  }

  /**
   * Disables Layout Builder.
   *
   * @param string $path
   *   The path to the manage display page.
   */
  protected function disableLayoutBuilder($path) {
    $page = $this->getSession()->getPage();
    // Save the current user to re-login after Layout Builder changes.
    $user = $this->loggedInUser;
    $this->loginLayoutAdmin();
    $this->drupalGet($path);
    $page->uncheckField('layout[allow_custom]');
    $page->uncheckField('layout[enabled]');
    $page->pressButton('Save');
    $page->pressButton('Confirm');
    $this->drupalLogin($user);
  }

  /**
   * Asserts that Quick Edit is initialized on the node view correctly.
   *
   * @todo Replace calls to this method with calls to ::doTestArticle() in
   *    https://www.drupal.org/node/3037436.
   *
   * @param string[] $fields
   *   The fields test.
   */
  private function assertQuickEditInit(array $fields): void {
    $this->assertNotEmpty($fields);
    $node = $this->article;
    $this->drupalGet('node/' . $node->id());

    // Initial state.
    $this->awaitQuickEditForEntity('node', 1);
    $this->assertEntityInstanceStates([
      'node/1[0]' => 'closed',
    ]);
    $field_states = [];
    foreach ($fields as $field) {
      $field_states["node/1/$field/en/full"] = 'inactive';
    }
    $this->assertEntityInstanceFieldStates('node', 1, 0, $field_states);
  }

  /**
   * {@inheritdoc}
   */
  protected function getQuickEditFieldId($original_field_id) {
    $page = $this->getSession()->getPage();
    $parts = explode('/', $original_field_id);
    // Removes the last part of the field id which will contain the Quick Edit
    // view mode ID. When using the Layout Builder the view_mode will contain a
    // hash of the layout sections and will be different each time the layout
    // changes.
    array_pop($parts);
    $field_key_without_view_mode = implode('/', $parts);
    $element = $page->find('css', "[data-quickedit-field-id^=\"$field_key_without_view_mode\"]");
    $this->assertNotEmpty($element, "Found Quick Edit-enabled field whose data-quickedit-field attribute starts with: $field_key_without_view_mode");
    try {
      $has_attribute = $element->hasAttribute('data-quickedit-field-id');
    }
    catch (\Exception $e) {
      $has_attribute = FALSE;
    }
    $this->assertTrue($has_attribute, $field_key_without_view_mode);
    $new_field_id = $element->getAttribute('data-quickedit-field-id');
    return $new_field_id;
  }

}
