<?php

declare(strict_types=1);

namespace Drupal\KernelTests\Core\Test;

use Drupal\Core\Test\TestDiscovery;
use Drupal\KernelTests\KernelTestBase;
use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use Symfony\Component\Process\Process;

/**
 * Tests equality of test discovery between run-tests.sh and PHPUnit CLI.
 *
 * Generate the list of tests for all the tests that PHPUnit can discover.
 * The goal here is to successfully generate the list, without any
 * duplicate namespace errors, deprecation errors or so forth. This keeps
 * us from committing tests which don't break under run-tests.sh, but do
 * break under the PHPUnit CLI test runner tool. We then cross check the
 * list thus generated, with the list generated by
 * \Drupal\Core\Test\TestDiscovery, which is used by run-tests.sh, to ensure
 * both methods will run the same tests,
 */
#[Group('TestSuites')]
#[Group('Test')]
#[Group('#slow')]
#[IgnoreDeprecations]
class PhpUnitTestDiscoveryTest extends KernelTestBase {

  private const TEST_LIST_MISMATCH_MESSAGE =
    "The list of test classes to be executed misses some files, see below. Make sure to:\n" .
    "1) give test classes a name that ends with a *Test suffix, and that the file is named accordingly;\n" .
    "2) the file of the test class is reachable by the directories specified in the <testsuites> section of the phpunit.xml file;\n" .
    "3) your version of the phpunit.xml configuration file is aligned with the PHPUnit and Drupal versions being used in testing.\n";

  /**
   * The filepath to the XML file to be used for dumping the test list.
   */
  private string $xmlOutputFile;

  /**
   * {@inheritdoc}
   */
  public function setUp(): void {
    parent::setUp();
    $xmlOutputFile = $this->container->getParameter('app.root') . DIRECTORY_SEPARATOR . 'test-list.xml';
    touch($xmlOutputFile);
    $this->xmlOutputFile = realpath($xmlOutputFile);
  }

  /**
   * {@inheritdoc}
   */
  public function tearDown(): void {
    @unlink($this->xmlOutputFile);
    parent::tearDown();
  }

  /**
   * Tests equality of test discovery between run-tests.sh and PHPUnit CLI.
   */
  public function testPhpUnitTestDiscoveryEqualsInternal(): void {
    // Drupal's test discovery, used by run-tests.sh.
    $testDiscovery = new TestDiscovery(
      $this->container->getParameter('app.root'),
      $this->container->get('class_loader')
    );
    $internalList = [];
    foreach ($testDiscovery->getTestClasses() as $group) {
      foreach (array_keys($group) as $class) {
        $internalList[] = $class;
      }
    }
    $internalList = array_unique($internalList);
    asort($internalList);

    // Location of PHPUnit configuration file.
    $configurationFilePath = $this->root . \DIRECTORY_SEPARATOR . 'core';
    // @todo once PHPUnit 10 is no longer used, remove the condition.
    // @see https://www.drupal.org/project/drupal/issues/3497116
    if (RunnerVersion::getMajor() >= 11) {
      $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
    }

    // PHPUnit's test discovery - via CLI execution.
    $process = new Process([
      'vendor/bin/phpunit',
      '--configuration',
      $configurationFilePath,
      '--list-tests-xml',
      $this->xmlOutputFile,
    ], $this->root);
    $process
      ->setTimeout(300)
      ->setIdleTimeout(300)
      ->run();
    $this->assertEquals(0, $process->getExitCode(),
      'COMMAND: ' . $process->getCommandLine() . "\n" .
      'OUTPUT: ' . $process->getOutput() . "\n" .
      'ERROR: ' . $process->getErrorOutput() . "\n"
    );

    $phpUnitXmlList = new \DOMDocument();
    $phpUnitXmlList->loadXML(file_get_contents($this->xmlOutputFile));
    $phpUnitClientList = [];
    // Try PHPUnit 10 format first.
    // @todo remove once PHPUnit 10 is no longer used.
    foreach ($phpUnitXmlList->getElementsByTagName('testCaseClass') as $node) {
      $phpUnitClientList[] = $node->getAttribute('name');
    }
    // If empty, try PHPUnit 11+ format.
    if (empty($phpUnitClientList)) {
      foreach ($phpUnitXmlList->getElementsByTagName('testClass') as $node) {
        $phpUnitClientList[] = $node->getAttribute('name');
      }
    }
    asort($phpUnitClientList);

    // Check against Drupal's discovery.
    $this->assertEquals(implode("\n", $phpUnitClientList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE);
  }

}
