<?php

namespace Drupal\simpletest\Form;

use Drupal\Core\Database\Connection;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Test\EnvironmentCleanerInterface;
use Drupal\Core\Url;
use Drupal\simpletest\TestDiscovery;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Test results form for $test_id.
 *
 * Note that the UI strings are not translated because this form is also used
 * from run-tests.sh.
 *
 * @internal
 *
 * @see simpletest_script_open_browser()
 * @see run-tests.sh
 */
class SimpletestResultsForm extends FormBase {

  /**
   * Associative array of themed result images keyed by status.
   *
   * @var array
   */
  protected $statusImageMap;

  /**
   * The database connection service.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The environment cleaner service.
   *
   * @var \Drupal\Core\Test\EnvironmentCleanerInterface
   */
  protected $cleaner;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('database'),
      $container->get('environment_cleaner')
    );
  }

  /**
   * Constructs a \Drupal\simpletest\Form\SimpletestResultsForm object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection service.
   */
  public function __construct(Connection $database, EnvironmentCleanerInterface $cleaner) {
    $this->database = $database;
    $this->cleaner = $cleaner;
  }

  /**
   * Builds the status image map.
   */
  protected static function buildStatusImageMap() {
    $image_pass = [
      '#theme' => 'image',
      '#uri' => 'core/misc/icons/73b355/check.svg',
      '#width' => 18,
      '#height' => 18,
      '#alt' => 'Pass',
    ];
    $image_fail = [
      '#theme' => 'image',
      '#uri' => 'core/misc/icons/e32700/error.svg',
      '#width' => 18,
      '#height' => 18,
      '#alt' => 'Fail',
    ];
    $image_exception = [
      '#theme' => 'image',
      '#uri' => 'core/misc/icons/e29700/warning.svg',
      '#width' => 18,
      '#height' => 18,
      '#alt' => 'Exception',
    ];
    $image_debug = [
      '#theme' => 'image',
      '#uri' => 'core/misc/icons/e29700/warning.svg',
      '#width' => 18,
      '#height' => 18,
      '#alt' => 'Debug',
    ];
    return [
      'pass' => $image_pass,
      'fail' => $image_fail,
      'exception' => $image_exception,
      'debug' => $image_debug,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'simpletest_results_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, $test_id = NULL) {
    // Make sure there are test results to display and a re-run is not being
    // performed.
    $results = [];
    if (is_numeric($test_id) && !$results = $this->getResults($test_id)) {
      $this->messenger()->addError($this->t('No test results to display.'));
      return $this->redirect('simpletest.test_form');
    }

    // Load all classes and include CSS.
    $form['#attached']['library'][] = 'simpletest/drupal.simpletest';
    // Add the results form.
    $filter = static::addResultForm($form, $results, $this->getStringTranslation());

    // Actions.
    $form['#action'] = Url::fromRoute('simpletest.result_form', ['test_id' => 're-run'])->toString();
    $form['action'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Actions'),
      '#attributes' => ['class' => ['container-inline']],
      '#weight' => -11,
    ];

    $form['action']['filter'] = [
      '#type' => 'select',
      '#title' => 'Filter',
      '#options' => [
        'all' => $this->t('All (@count)', ['@count' => count($filter['pass']) + count($filter['fail'])]),
        'pass' => $this->t('Pass (@count)', ['@count' => count($filter['pass'])]),
        'fail' => $this->t('Fail (@count)', ['@count' => count($filter['fail'])]),
      ],
    ];
    $form['action']['filter']['#default_value'] = ($filter['fail'] ? 'fail' : 'all');

    // Categorized test classes for to be used with selected filter value.
    $form['action']['filter_pass'] = [
      '#type' => 'hidden',
      '#default_value' => implode(',', $filter['pass']),
    ];
    $form['action']['filter_fail'] = [
      '#type' => 'hidden',
      '#default_value' => implode(',', $filter['fail']),
    ];

    $form['action']['op'] = [
      '#type' => 'submit',
      '#value' => $this->t('Run tests'),
    ];

    $form['action']['return'] = [
      '#type' => 'link',
      '#title' => $this->t('Return to list'),
      '#url' => Url::fromRoute('simpletest.test_form'),
    ];

    if (is_numeric($test_id)) {
      $this->cleaner->cleanResultsTable($test_id);
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $pass = $form_state->getValue('filter_pass') ? explode(',', $form_state->getValue('filter_pass')) : [];
    $fail = $form_state->getValue('filter_fail') ? explode(',', $form_state->getValue('filter_fail')) : [];

    if ($form_state->getValue('filter') == 'all') {
      $classes = array_merge($pass, $fail);
    }
    elseif ($form_state->getValue('filter') == 'pass') {
      $classes = $pass;
    }
    else {
      $classes = $fail;
    }

    if (!$classes) {
      $form_state->setRedirect('simpletest.test_form');
      return;
    }

    $form_execute = [];
    $form_state_execute = new FormState();
    foreach ($classes as $class) {
      $form_state_execute->setValue(['tests', $class], $class);
    }

    // Submit the simpletest test form to rerun the tests.
    // Under normal circumstances, a form object's submitForm() should never be
    // called directly, FormBuilder::submitForm() should be called instead.
    // However, it calls $form_state->setProgrammed(), which disables the Batch API.
    $simpletest_test_form = SimpletestTestForm::create(\Drupal::getContainer());
    $simpletest_test_form->buildForm($form_execute, $form_state_execute);
    $simpletest_test_form->submitForm($form_execute, $form_state_execute);
    if ($redirect = $form_state_execute->getRedirect()) {
      $form_state->setRedirectUrl($redirect);
    }
  }

  /**
   * Get test results for $test_id.
   *
   * @param int $test_id
   *   The test_id to retrieve results of.
   *
   * @return array
   *   Array of results grouped by test_class.
   */
  protected function getResults($test_id) {
    return $this->database->select('simpletest')
      ->fields('simpletest')
      ->condition('test_id', $test_id)
      ->orderBy('test_class')
      ->orderBy('message_id')
      ->execute()
      ->fetchAll();
  }

  /**
   * Adds the result form to a $form.
   *
   * This is a static method so that run-tests.sh can use it to generate a
   * results page completely external to Drupal. This is why the UI strings are
   * not wrapped in t().
   *
   * @param array $form
   *   The form to attach the results to.
   * @param array $results
   *   The simpletest results.
   *
   * @return array
   *   A list of tests the passed and failed. The array has two keys, 'pass' and
   *   'fail'. Each contains a list of test classes.
   *
   * @see simpletest_script_open_browser()
   * @see run-tests.sh
   */
  public static function addResultForm(array &$form, array $results) {
    // Transform the test results to be grouped by test class.
    $test_results = [];
    foreach ($results as $result) {
      if (!isset($test_results[$result->test_class])) {
        $test_results[$result->test_class] = [];
      }
      $test_results[$result->test_class][] = $result;
    }

    $image_status_map = static::buildStatusImageMap();

    // Keep track of which test cases passed or failed.
    $filter = [
      'pass' => [],
      'fail' => [],
    ];

    // Summary result widget.
    $form['result'] = [
      '#type' => 'fieldset',
      '#title' => 'Results',
      // Because this is used in a theme-less situation need to provide a
      // default.
      '#attributes' => [],
    ];
    $form['result']['summary'] = $summary = [
      '#theme' => 'simpletest_result_summary',
      '#pass' => 0,
      '#fail' => 0,
      '#exception' => 0,
      '#debug' => 0,
    ];

    \Drupal::service('test_discovery')->registerTestNamespaces();

    // Cycle through each test group.
    $header = [
      'Message',
      'Group',
      'Filename',
      'Line',
      'Function',
      ['colspan' => 2, 'data' => 'Status'],
    ];
    $form['result']['results'] = [];
    foreach ($test_results as $group => $assertions) {
      // Create group details with summary information.
      $info = TestDiscovery::getTestInfo($group);
      $form['result']['results'][$group] = [
        '#type' => 'details',
        '#title' => $info['name'],
        '#open' => TRUE,
        '#description' => $info['description'],
      ];
      $form['result']['results'][$group]['summary'] = $summary;
      $group_summary =& $form['result']['results'][$group]['summary'];

      // Create table of assertions for the group.
      $rows = [];
      foreach ($assertions as $assertion) {
        $row = [];
        $row[] = ['data' => ['#markup' => $assertion->message]];
        $row[] = $assertion->message_group;
        $row[] = \Drupal::service('file_system')->basename(($assertion->file));
        $row[] = $assertion->line;
        $row[] = $assertion->function;
        $row[] = ['data' => $image_status_map[$assertion->status]];

        $class = 'simpletest-' . $assertion->status;
        if ($assertion->message_group == 'Debug') {
          $class = 'simpletest-debug';
        }
        $rows[] = ['data' => $row, 'class' => [$class]];

        $group_summary['#' . $assertion->status]++;
        $form['result']['summary']['#' . $assertion->status]++;
      }
      $form['result']['results'][$group]['table'] = [
        '#type' => 'table',
        '#header' => $header,
        '#rows' => $rows,
      ];

      // Set summary information.
      $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0;
      $form['result']['results'][$group]['#open'] = !$group_summary['#ok'];

      // Store test group (class) as for use in filter.
      $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group;
    }

    // Overall summary status.
    $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0;

    return $filter;
  }

}
