<?php

declare(strict_types=1);

namespace Drupal\Tests\standard\Traits;

use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
use Drupal\Component\Utility\Html;
use Drupal\contact\Entity\ContactForm;
use Drupal\Core\Url;
use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\image\Entity\ImageStyle;
use Drupal\media\Entity\MediaType;
use Drupal\media\Plugin\media\Source\Image;
use Drupal\Tests\RequirementsPageTrait;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Symfony\Component\Validator\ConstraintViolation;

/**
 * Provides a test method to test the Standard installation profile or recipe.
 */
trait StandardTestTrait {
  use SchemaCheckTestTrait;
  use RequirementsPageTrait;

  /**
   * The admin user.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $adminUser;

  /**
   * Tests Standard installation profile or recipe.
   */
  public function testStandard(): void {
    $this->drupalGet('');
    $this->assertSession()->pageTextContains('Powered by Drupal');
    $this->assertSession()->pageTextContains('Congratulations and welcome to the Drupal community.');

    // Test anonymous user can access 'Main navigation' block.
    $this->adminUser = $this->drupalCreateUser([
      'administer blocks',
      'administer block content',
      'post comments',
      'skip comment approval',
      'create article content',
      'create page content',
    ]);
    $this->drupalLogin($this->adminUser);
    // Configure the block.
    $this->drupalGet('admin/structure/block/add/system_menu_block:main/olivero');
    $this->submitForm([
      'region' => 'sidebar',
      'id' => 'main_navigation',
    ], 'Save block');
    // Verify admin user can see the block.
    $this->drupalGet('');
    $this->assertSession()->pageTextContains('Main navigation');

    // Verify we have role = complementary on help_block blocks.
    $this->drupalGet('admin/structure/block');
    $this->assertSession()->elementAttributeContains('xpath', "//div[@id='block-olivero-help']", 'role', 'complementary');

    // Verify anonymous user can see the block.
    $this->drupalLogout();
    $this->assertSession()->pageTextContains('Main navigation');

    // Ensure comments don't show in the front page RSS feed.
    // Create an article.
    $this->drupalCreateNode([
      'type' => 'article',
      'title' => 'Foobar',
      'promote' => 1,
      'status' => 1,
      'body' => [['value' => 'Then she picked out two somebodies,<br />Sally and me', 'format' => 'basic_html']],
    ]);

    // Add a comment.
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('node/1');
    // Verify that a line break is present.
    $this->assertSession()->responseContains('Then she picked out two somebodies,<br>Sally and me');
    $this->submitForm([
      'subject[0][value]' => 'Bar foo',
      'comment_body[0][value]' => 'Then she picked out two somebodies, Sally and me',
    ], 'Save');
    // Fetch the feed.
    $this->drupalGet('rss.xml');
    $this->assertSession()->responseContains('Foobar');
    $this->assertSession()->responseNotContains('Then she picked out two somebodies, Sally and me');

    // Ensure block body exists.
    $this->drupalGet('block/add');
    $this->assertSession()->fieldExists('body[0][value]');

    // Now we have all configuration imported, test all of them for schema
    // conformance. Ensures all imported default configuration is valid when
    // standard profile modules are enabled.
    $names = $this->container->get('config.storage')->listAll();
    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
    $typed_config = $this->container->get('config.typed');
    foreach ($names as $name) {
      $config = $this->config($name);
      $this->assertConfigSchema($typed_config, $name, $config->get());
    }

    // Validate all configuration.
    // @todo Generalize in https://www.drupal.org/project/drupal/issues/2164373
    foreach (Editor::loadMultiple() as $editor) {
      // Currently only text editors using CKEditor 5 can be validated.
      if ($editor->getEditor() !== 'ckeditor5') {
        continue;
      }

      $this->assertSame([], array_map(
        function (ConstraintViolation $v) {
          return (string) $v->getMessage();
        },
        iterator_to_array(CKEditor5::validatePair(
          $editor,
          $editor->getFilterFormat()
        ))
      ));
    }

    // Ensure that configuration from the Standard profile is not reused when
    // enabling a module again since it contains configuration that can not be
    // installed. For example, editor.editor.basic_html is editor configuration
    // that depends on the CKEditor 5 module. The CKEditor 5 module can not be
    // installed before the editor module since it depends on the editor module.
    // The installer does not have this limitation since it ensures that all of
    // the install profiles dependencies are installed before creating the
    // editor configuration.
    foreach (FilterFormat::loadMultiple() as $filter) {
      // Ensure that editor can be uninstalled by removing use in filter
      // formats. It is necessary to prime the filter collection before removing
      // the filter.
      $filter->filters();
      $filter->removeFilter('editor_file_reference');
      $filter->save();
    }
    \Drupal::service('module_installer')->uninstall(['editor', 'ckeditor5']);
    $this->rebuildContainer();
    \Drupal::service('module_installer')->install(['editor']);
    /** @var \Drupal\contact\ContactFormInterface $contact_form */
    $contact_form = ContactForm::load('feedback');
    $recipients = $contact_form->getRecipients();
    $this->assertEquals(['simpletest@example.com'], $recipients);

    $role = Role::create([
      'id' => 'admin_theme',
      'label' => 'Admin theme',
    ]);
    $role->grantPermission('view the administration theme');
    $role->save();
    $this->adminUser->addRole($role->id())->save();
    $this->drupalGet('node/add');
    $this->assertSession()->statusCodeEquals(200);

    // Ensure that there are no pending updates after installation.
    $this->drupalLogin($this->rootUser);
    $this->drupalGet('update.php/selection');
    $this->updateRequirementsProblem();
    $this->drupalGet('update.php/selection');
    $this->assertSession()->pageTextContains('No pending updates.');

    // Ensure that there are no pending entity updates after installation.
    $this->assertFalse($this->container->get('entity.definition_update_manager')->needsUpdates(), 'After installation, entity schema is up to date.');

    // Make sure the optional image styles are not installed.
    $this->drupalGet('admin/config/media/image-styles');
    $this->assertSession()->pageTextNotContains('Max 325x325');
    $this->assertSession()->pageTextNotContains('Max 650x650');
    $this->assertSession()->pageTextNotContains('Max 1300x1300');
    $this->assertSession()->pageTextNotContains('Max 2600x2600');

    // Make sure the optional image styles are installed after enabling
    // the responsive_image module.
    $this->installResponsiveImage();
    $this->drupalGet('admin/config/media/image-styles');
    $this->assertSession()->pageTextContains('Max 325x325');
    $this->assertSession()->pageTextContains('Max 650x650');
    $this->assertSession()->pageTextContains('Max 1300x1300');
    $this->assertSession()->pageTextContains('Max 2600x2600');

    // Make sure all image styles has webp conversion as last effect.
    foreach (ImageStyle::loadMultiple() as $style) {
      $effects = $style->getEffects()->getInstanceIds();
      $last = $style->getEffects()->get(end($effects));
      $this->assertSame('image_convert_avif', $last->getConfiguration()['id']);
      $this->assertSame('webp', $last->getConfiguration()['data']['extension']);
    }

    // Verify certain routes' responses are cacheable by Dynamic Page Cache, to
    // ensure these responses are very fast for authenticated users.
    $this->drupalLogin($this->adminUser);
    $url = Url::fromRoute('contact.site_page');
    $this->drupalGet($url);
    // Verify that site-wide contact page cannot be cached by Dynamic Page
    // Cache.
    $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (poor cacheability)');

    $url = Url::fromRoute('<front>');
    $this->drupalGet($url);
    $this->drupalGet($url);
    // Verify that frontpage is cached by Dynamic Page Cache.
    $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');

    $url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
    $this->drupalGet($url);
    $this->drupalGet($url);
    // Verify that full node page is cached by Dynamic Page Cache.
    $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');

    $url = Url::fromRoute('entity.user.canonical', ['user' => 1]);
    $this->drupalGet($url);
    $this->drupalGet($url);
    // Verify that user profile page is cached by Dynamic Page Cache.
    $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');

    // Make sure the editorial workflow is installed after enabling the
    // content_moderation module.
    \Drupal::service('module_installer')->install(['content_moderation']);
    $role = Role::create([
      'id' => 'admin_workflows',
      'label' => 'Admin workflow',
    ]);
    $role->grantPermission('administer workflows');
    $role->save();
    $this->adminUser->addRole($role->id())->save();
    $this->rebuildContainer();
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial');
    $this->assertSession()->pageTextContains('Draft');
    $this->assertSession()->pageTextContains('Published');
    $this->assertSession()->pageTextContains('Archived');
    $this->assertSession()->pageTextContains('Create New Draft');
    $this->assertSession()->pageTextContains('Publish');
    $this->assertSession()->pageTextContains('Archive');
    $this->assertSession()->pageTextContains('Restore to Draft');
    $this->assertSession()->pageTextContains('Restore');

    \Drupal::service('module_installer')->install(['media']);
    $role = Role::create([
      'id' => 'admin_media',
      'label' => 'Admin media',
    ]);
    $role->grantPermission('administer media');
    $role->grantPermission('administer media display');
    $role->save();
    $this->adminUser->addRole($role->id())->save();
    $assert_session = $this->assertSession();
    /** @var \Drupal\media\Entity\MediaType $media_type */
    foreach (MediaType::loadMultiple() as $media_type) {
      $media_type_machine_name = $media_type->id();
      $this->drupalGet('media/add/' . $media_type_machine_name);
      // Get the form element, and its HTML representation.
      $form_selector = '#media-' . Html::cleanCssIdentifier($media_type_machine_name) . '-add-form';
      $form = $assert_session->elementExists('css', $form_selector);
      $form_html = $form->getOuterHtml();

      // The name field should be hidden.
      $assert_session->fieldNotExists('Name', $form);
      // The source field should be shown before the vertical tabs.
      $source_field_label = $media_type->getSource()->getSourceFieldDefinition($media_type)->getLabel();
      $test_source_field = $assert_session->elementExists('xpath', "//*[contains(text(), '$source_field_label')]", $form)->getOuterHtml();
      $vertical_tabs = $assert_session->elementExists('css', '.js-form-type-vertical-tabs', $form)->getOuterHtml();
      $this->assertGreaterThan(strpos($form_html, $test_source_field), strpos($form_html, $vertical_tabs));
      // The "Published" checkbox should be the last element.
      $date_field = $assert_session->fieldExists('Date', $form)->getOuterHtml();
      $published_checkbox = $assert_session->fieldExists('Published', $form)->getOuterHtml();
      $this->assertGreaterThan(strpos($form_html, $date_field), strpos($form_html, $published_checkbox));
      if (is_a($media_type->getSource(), Image::class, TRUE)) {
        // Assert the default entity view display is configured with an image
        // style.
        $this->drupalGet('/admin/structure/media/manage/' . $media_type->id() . '/display');
        $assert_session->fieldValueEquals('fields[field_media_image][type]', 'image');
        $assert_session->elementTextContains('css', 'tr[data-drupal-selector="edit-fields-field-media-image"]', 'Image style: Large (480×480)');
        // By default for media types with an image source, only the image
        // component should be enabled.
        $assert_session->elementsCount('css', 'input[name$="_settings_edit"]', 1);
      }

    }

    // Tests that user 1 does not have an all-access pass.
    $this->drupalLogin($this->rootUser);
    $this->drupalGet('admin');
    $this->assertSession()->statusCodeEquals(200);

    User::load(1)
      ->removeRole('administrator')
      ->save();
    // Clear caches so change take effect in system under test.
    $this->rebuildAll();

    $this->drupalGet('admin');
    $this->assertSession()->statusCodeEquals(403);
  }

  /**
   * Installs the responsive image module.
   */
  protected function installResponsiveImage(): void {
    // Install responsive_image module.
    \Drupal::service('module_installer')->install(['responsive_image']);
    $this->rebuildContainer();
  }

}
