<?php

namespace Drupal\Tests\poll\Functional;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Tests\BrowserTestBase;
use Drupal\poll\PollInterface;
use Drupal\user\UserInterface;

/**
 * Defines a base class for testing the Poll module.
 */
abstract class PollTestBase extends BrowserTestBase {

  use StringTranslationTrait;

  /**
   * A user with administrative permissions.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $adminUser;

  /**
   * A user with access to view polls but without administrative permissions.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $webUser;

  /**
   * The poll under test.
   *
   * @var \Drupal\poll\PollInterface
   */
  protected $poll;

  /**
   * List of permissions used by adminUser.
   *
   * @var array
   */
  protected $adminPermissions = [];

  /**
   * List of permissions used by webUser.
   *
   * @var array
   */
  protected $webUserPermissions = [];

  /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = ['poll', 'node', 'block'];

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

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

    $this->drupalPlaceBlock('local_tasks_block');
    $this->drupalPlaceBlock('local_actions_block');
    $this->drupalPlaceBlock('system_breadcrumb_block');
    $this->drupalPlaceBlock('page_title_block');

    $this->adminUser = $this->drupalCreateUser(array_merge([
      'administer polls',
      'access polls',
      'access poll overview',
      'view poll results',
    ], $this->adminPermissions));
    $this->webUser = $this->drupalCreateUser(array_merge([
      'access polls',
      'cancel own vote',
    ], $this->webUserPermissions));
    $this->poll = $this->pollCreate();
  }

  /**
   * Creates a poll.
   *
   * @param int $choice_count
   *   (optional) The number of choices to generate. Defaults to 7.
   * @param \Drupal\user\UserInterface $account
   *   (optional) Poll creator user. Defaults to NULL, then use
   *   $this->adminUser.
   * @param bool $preview
   *   (optional) Whether to test if the preview is working or not. Defaults to
   *   TRUE.
   *
   * @return mixed
   *   The created poll entity, or FALSE on error.
   */
  public function pollCreate($choice_count = 7, ?UserInterface $account = NULL, $preview = TRUE) {
    if (!$account) {
      $this->drupalLogin($this->adminUser);
    }
    else {
      $this->drupalLogin($account);
    }

    // Get the form first to initialize the state of the internal browser.
    $this->drupalGet('poll/add');

    $question = $this->randomMachineName();
    $choices = $this->generateChoices($choice_count);
    [$edit, $index] = $this->pollGenerateEdit($question, $choices);

    // Re-submit the form until all choices are filled in.
    if (count($choices) > 0) {
      for ($delta = 0; $delta <= count($choices); $delta++) {
        $this->submitForm($edit, 'Add another item');
        [$edit, $index] = $this->pollGenerateEdit($question, $choices, $index);
      }
    }
    // If ($preview) {
    // $this->drupalPostForm('poll/add', $edit, t('Preview'));
    // $this->assertPollChoiceOrder($choices, $index, TRUE);
    // list($edit, $index) = $this->pollGenerateEdit($title, $choices, $index);
    // }.
    $this->submitForm($edit, 'Save');

    // Load the first poll returned from the database.
    $polls = \Drupal::entityTypeManager()
      ->getStorage('poll')
      ->loadByProperties(['question' => $question]);
    $poll = reset($polls);
    $this->assertSession()->pageTextContains("The poll $question has been added.");
    $this->assertInstanceOf(PollInterface::class, $poll);

    return $poll instanceof PollInterface ? $poll : FALSE;
  }

  /**
   * Generates POST values for the poll node form, specifically poll choices.
   *
   * @param string $question
   *   The poll question.
   * @param array $choices
   *   An array containing poll choices, as generated by
   *   PollTestBase::generateChoices().
   * @param int $index
   *   (optional) The amount/number of already submitted poll choices. Defaults
   *   to 0.
   *
   * @return array
   *   An indexed array containing:
   *   - The generated POST values, suitable for
   *     \Drupal\Tests\UiHelperTrait::submitForm().
   *   - The number of poll choices contained in 'edit', for potential re-usage
   *     in subsequent invocations of this function.
   */
  private function pollGenerateEdit($question, array $choices, $index = 0) {
    $max_new_choices = 1;
    $already_submitted_choices = array_slice($choices, 0, $index);
    $new_choices = array_values(array_slice($choices, $index, $max_new_choices));
    $edit = [
      'question[0][value]' => $question,
    ];
    foreach ($already_submitted_choices as $k => $text) {
      $edit['choice[' . $k . '][choice]'] = $text;
    }
    foreach ($new_choices as $k => $text) {
      $edit['choice[' . $k . '][choice]'] = $text;
    }
    return [$edit, count($already_submitted_choices) + count($new_choices)];
  }

  /**
   * Generates random choices for the poll.
   *
   * @param int $count
   *   (optional) The number of choices to generate. Defaults to 7.
   *
   * @return array
   *   An array of generated choices.
   */
  private function generateChoices($count = 7) {
    $choices = [];
    for ($i = 1; $i <= $count; $i++) {
      $choices[] = $this->randomMachineName();
    }
    return $choices;
  }

  /**
   * Asserts correct poll choice order in the node form after submission.
   *
   * Verifies both the order in the DOM and in the 'weight' form elements.
   *
   * @param array $choices
   *   An array containing poll choices, as generated by
   *   PollTestBase::generateChoices().
   * @param int $index
   *   (optional) The amount/number of already submitted poll choices. Defaults
   *   to 0.
   * @param bool $preview
   *   (optional) Whether to also check the poll preview. Defaults to FALSE.
   *
   * @see PollTestBase::pollGenerateEdit()
   */
  public function assertPollChoiceOrder(array $choices, $index = 0, $preview = FALSE) {
    $expected = [];
    $weight = 0;
    foreach ($choices as $id => $label) {
      if ($id < $index) {
        // Directly assert the weight form element value for this choice.
        $this->assertSession()->fieldValueEquals('choice[' . $id . '][_weight]', $weight);
        // The expected weight of each choice is higher than the previous one.
        $weight++;
        // Append to our (to be reversed) stack of labels.
        $expected[$weight] = $label;
      }
    }
    ksort($expected);

    // Verify DOM order of poll choices (i.e., #weight of form elements).
    $elements = $this->xpath('//input[starts-with(@name, :prefix) and contains(@name, :suffix)]', [
      ':prefix' => 'choice[chid:',
      ':suffix' => '][chtext]',
    ]);
    $expected_order = $expected;
    foreach ($elements as $element) {
      $next_label = array_shift($expected_order);
      $this->assertEquals((string) $element['value'], $next_label);
    }

    // If requested, also verify DOM order in preview.
    if ($preview) {
      $elements = $this->xpath('//div[contains(@class, :teaser)]/descendant::div[@class=:text]', [
        ':teaser' => 'node-teaser',
        ':text' => 'text',
      ]);
      $expected_order = $expected;
      foreach ($elements as $element) {
        $next_label = array_shift($expected_order);
        $this->assertEquals((string) $element, $next_label, "Found choice $next_label in preview.");
      }
    }
  }

  /**
   * Get the Choice ID of a poll.
   *
   * @param \Drupal\poll\PollInterface $poll
   *   The poll of the choice.
   * @param int $delta
   *   The number of the choice.
   *
   * @return int
   *   Returns the choice ID or an error.
   *
   * @throws \InvalidArgumentException
   *   Throws it if the poll does not have the choice delta.
   */
  protected function getChoiceId(PollInterface $poll, $delta) {
    $options = $poll->getOptions();
    $keys = array_keys($options);
    foreach ($keys as $id => $key) {
      if ($delta == $id + 1) {
        return $key;
      }
    }
    throw new \InvalidArgumentException();
  }

  /**
   * Get the Choice ID of a poll from the label.
   *
   * @param \Drupal\poll\PollInterface $poll
   *   The poll of the choice.
   * @param string $label
   *   The label of the choice.
   *
   * @return int
   *   Returns the choice id or an error.
   *
   * @throws \InvalidArgumentException
   *   Throws it if the poll does not have the choice label.
   */
  protected function getChoiceIdByLabel(PollInterface $poll, $label) {
    $options = $poll->getOptions();
    foreach ($options as $id => $option) {
      if ($label == $option) {
        return $id;
      }
    }
    throw new \InvalidArgumentException();
  }

}
