<?php

declare(strict_types=1);

namespace Drupal\Tests\package_manager\Kernel;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\fixture_manipulator\ActiveFixtureManipulator;
use Drupal\fixture_manipulator\FixtureManipulator;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\package_manager\Exception\SandboxEventException;
use Drupal\package_manager\ValidationResult;
use Drupal\package_manager\Validator\LockFileValidator;
use Drupal\package_manager\Validator\PhpTufValidator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests Drupal\package_manager\Validator\PhpTufValidator.
 *
 * @internal
 */
#[CoversClass(PhpTufValidator::class)]
#[Group('package_manager')]
#[Group('#slow')]
#[RunTestsInSeparateProcesses]
class PhpTufValidatorTest extends PackageManagerKernelTestBase {

  use StringTranslationTrait;

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

    // PHP-TUF must be enabled for this test to run.
    $this->setSetting('package_manager_bypass_tuf', FALSE);

    // audit.block-insecure is only available in Composer 2.9.2 onwards.
    try {
      (new ActiveFixtureManipulator())
        ->addConfig(['audit.block-insecure' => FALSE])
        ->commitChanges();
    }
    catch (\RuntimeException $e) {
      $this->assertStringContainsString('Setting audit.block-insecure does not exist', $e->getMessage());
    }

    (new ActiveFixtureManipulator())
      ->addConfig([
        'repositories.drupal' => [
          'type' => 'composer',
          'url' => 'https://packages.drupal.org/8',
          'tuf' => TRUE,
        ],
        'allow-plugins.' . PhpTufValidator::PLUGIN_NAME => TRUE,
      ])
      ->addPackage([
        'name' => PhpTufValidator::PLUGIN_NAME,
        'type' => 'composer-plugin',
        'require' => [
          'composer-plugin-api' => '*',
        ],
        'extra' => [
          'class' => 'PhpTufComposerPlugin',
        ],
      ])
      ->commitChanges()
      ->updateLock();
  }

  /**
   * Tests that there are no errors if the plugin is set up correctly.
   */
  public function testPluginInstalledAndConfiguredProperly(): void {
    $this->assertStatusCheckResults([]);
    $this->assertResults([]);
  }

  /**
   * Tests there is an error if the plugin is not installed in the project root.
   */
  public function testPluginNotInstalledInProjectRoot(): void {
    (new ActiveFixtureManipulator())
      ->removePackage(PhpTufValidator::PLUGIN_NAME)
      ->commitChanges()
      ->updateLock();

    $messages = [
      $this->t('The <code>php-tuf/composer-integration</code> plugin is not installed.'),
      // Composer automatically removes the plugin from the `allow-plugins`
      // list when the plugin package is removed.
      $this->t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
    ];
    $result = ValidationResult::createError($messages, $this->t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
    $this->assertStatusCheckResults([$result]);
    $this->assertResults([$result], PreCreateEvent::class);
  }

  /**
   * Tests removing the plugin from the stage on pre-require.
   */
  public function testPluginRemovedFromStagePreRequire(): void {
    $this->getStageFixtureManipulator()
      ->removePackage(PhpTufValidator::PLUGIN_NAME);

    $messages = [
      $this->t('The <code>php-tuf/composer-integration</code> plugin is not installed.'),
      // Composer automatically removes the plugin from the `allow-plugins`
      // list when the plugin package is removed.
      $this->t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
    ];
    $result = ValidationResult::createError($messages, $this->t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
    $this->assertResults([$result], PreRequireEvent::class);
  }

  /**
   * Tests removing the plugin from the stage before applying it.
   */
  public function testPluginRemovedFromStagePreApply(): void {
    $stage = $this->createStage();
    $stage->create();
    $stage->require(['ext-json:*']);

    (new FixtureManipulator())
      ->removePackage(PhpTufValidator::PLUGIN_NAME)
      ->commitChanges($stage->getSandboxDirectory());

    $messages = [
      $this->t('The <code>php-tuf/composer-integration</code> plugin is not installed.'),
      // Composer automatically removes the plugin from the `allow-plugins`
      // list when the plugin package is removed.
      $this->t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
    ];
    $result = ValidationResult::createError($messages, $this->t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
    try {
      $stage->apply();
      $this->fail('Expected an exception but none was thrown.');
    }
    catch (SandboxEventException $e) {
      $this->assertInstanceOf(PreApplyEvent::class, $e->event);
      $this->assertValidationResultsEqual([$result], $e->event->getResults());
    }
  }

  /**
   * Data provider for testing invalid plugin configuration.
   *
   * @return array[]
   *   The test cases.
   */
  public static function providerInvalidConfiguration(): array {
    return [
      'plugin specifically disallowed' => [
        [
          'allow-plugins.' . PhpTufValidator::PLUGIN_NAME => FALSE,
        ],
        [
          t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
        ],
      ],
      'all plugins disallowed' => [
        [
          'allow-plugins' => FALSE,
        ],
        [
          t('The <code>php-tuf/composer-integration</code> plugin is not listed as an allowed plugin.'),
        ],
      ],
      'packages.drupal.org not using TUF' => [
        [
          'repositories.drupal' => [
            'type' => 'composer',
            'url' => 'https://packages.drupal.org/8',
          ],
        ],
        [
          t('TUF is not enabled for the <code>https://packages.drupal.org/8</code> repository.'),
        ],
      ],
    ];
  }

  /**
   * Data provider for testing invalid plugin configuration in the stage.
   *
   * @return \Generator
   *   The test cases.
   */
  public static function providerInvalidConfigurationInStage(): \Generator {
    foreach (static::providerInvalidConfiguration() as $name => $arguments) {
      $arguments[] = PreRequireEvent::class;
      yield "$name on pre-require" => $arguments;

      array_splice($arguments, -1, NULL, PreApplyEvent::class);
      yield "$name on pre-apply" => $arguments;
    }
  }

  /**
   * Tests errors caused by invalid plugin configuration in the project root.
   *
   * @param array $config
   *   The Composer configuration to set.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_messages
   *   The expected error messages.
   */
  #[DataProvider('providerInvalidConfiguration')]
  public function testInvalidConfigurationInProjectRoot(array $config, array $expected_messages): void {
    (new ActiveFixtureManipulator())->addConfig($config)->commitChanges()->updateLock();

    $result = ValidationResult::createError($expected_messages, $this->t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
    $this->assertStatusCheckResults([$result]);
    $this->assertResults([$result], PreCreateEvent::class);
  }

  /**
   * Tests errors caused by invalid plugin configuration in the stage directory.
   *
   * @param array $config
   *   The Composer configuration to set.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup[] $expected_messages
   *   The expected error messages.
   * @param string $event_class
   *   The event before which the plugin's configuration should be changed.
   */
  #[DataProvider('providerInvalidConfigurationInStage')]
  public function testInvalidConfigurationInStage(array $config, array $expected_messages, string $event_class): void {
    $listener = function (PreRequireEvent|PreApplyEvent $event) use ($config): void {
      (new FixtureManipulator())
        ->addConfig($config)
        ->commitChanges($event->sandboxManager->getSandboxDirectory())
        ->updateLock();
    };
    $this->addEventTestListener($listener, $event_class);

    // LockFileValidator will complain because we have not added, removed, or
    // updated any packages in the stage. In this very specific situation, it's
    // okay to disable that validator to remove the interference.
    if ($event_class === PreApplyEvent::class) {
      $lock_file_validator = $this->container->get(LockFileValidator::class);
      $this->container->get('event_dispatcher')
        ->removeSubscriber($lock_file_validator);
    }

    $result = ValidationResult::createError($expected_messages, $this->t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
    $this->assertResults([$result], $event_class);
  }

}
