<?php

declare(strict_types=1);

namespace Drupal\Tests\package_manager\Kernel;

use Drupal\Core\Logger\RfcLogLevel;
use Drupal\package_manager\ProjectInfo;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests Drupal\package_manager\ProjectInfo.
 *
 * @internal
 */
#[CoversClass(ProjectInfo::class)]
#[Group('auto_updates')]
#[RunTestsInSeparateProcesses]
class ProjectInfoTest extends PackageManagerKernelTestBase {

  /**
   * Tests get installable releases.
   *
   * @param string $fixture
   *   The fixture file name.
   * @param string $installed_version
   *   The installed version core version to set.
   * @param string[] $expected_versions
   *   The expected versions.
   *
   * @legacy-covers ::getInstallableReleases
   */
  #[DataProvider('providerGetInstallableReleases')]
  public function testGetInstallableReleases(string $fixture, string $installed_version, array $expected_versions): void {
    [$project] = explode('.', $fixture);
    $fixtures_directory = __DIR__ . '/../../fixtures/release-history/';
    if ($project === 'drupal') {
      $this->setCoreVersion($installed_version);
    }
    else {
      // Update the version and the project of the project.
      $this->enableModules(['package_manager_test_update']);
      $extension_info_update = [
        'version' => $installed_version,
        'project' => 'package_manager_test_update',
      ];
      // @todo Replace with use of the trait from the Update Status module in https://drupal.org/i/3348234.
      $this->config('update_test.settings')
        ->set("system_info.$project", $extension_info_update)
        ->save();
      // The Update Status module will always request Drupal core's update XML.
      $metadata_fixtures['drupal'] = $fixtures_directory . 'drupal.9.8.2.xml';
    }
    $metadata_fixtures[$project] = "$fixtures_directory$fixture";
    $this->setReleaseMetadata($metadata_fixtures);
    $project_info = new ProjectInfo($project);
    $actual_releases = $project_info->getInstallableReleases();
    // Assert that we returned the correct releases in the expected order.
    $this->assertSame($expected_versions, array_keys($actual_releases));
    // Assert that we version keys match the actual releases.
    foreach ($actual_releases as $version => $release) {
      $this->assertSame($version, $release->getVersion());
    }
  }

  /**
   * Data provider for testGetInstallableReleases().
   *
   * @return mixed[][]
   *   The test cases.
   */
  public static function providerGetInstallableReleases(): array {
    return [
      'core, no updates' => [
        'drupal.9.8.2.xml',
        '9.8.2',
        [],
      ],
      'core, on supported branch, pre-release in next minor' => [
        'drupal.9.8.0-alpha1.xml',
        '9.7.1',
        ['9.8.0-alpha1'],
      ],
      'core, on unsupported branch, updates in multiple supported branches' => [
        'drupal.9.8.2.xml',
        '9.6.0-alpha1',
        ['9.8.2', '9.8.1', '9.8.0', '9.8.0-alpha1', '9.7.1', '9.7.0', '9.7.0-alpha1'],
      ],
      // A test case with an unpublished release, 9.8.0, and unsupported
      // release, 9.8.1, both of these releases should not be returned.
      'core, filter out unsupported and unpublished releases' => [
        'drupal.9.8.2-unsupported_unpublished.xml',
        '9.6.0-alpha1',
        ['9.8.2', '9.8.0-alpha1', '9.7.1', '9.7.0', '9.7.0-alpha1'],
      ],
      'core, supported branches before and after installed release' => [
        'drupal.9.8.2.xml',
        '9.8.0-alpha1',
        ['9.8.2', '9.8.1', '9.8.0'],
      ],
      'core, one insecure release filtered out' => [
        'drupal.9.8.1-security.xml',
        '9.8.0-alpha1',
        ['9.8.1'],
      ],
      'core, skip insecure releases and return secure releases' => [
        'drupal.9.8.2-older-sec-release.xml',
        '9.7.0-alpha1',
        ['9.8.2', '9.8.1', '9.8.1-beta1', '9.8.0-alpha1', '9.7.1'],
      ],
      'contrib, semver and legacy' => [
        'package_manager_test_update.7.0.1.xml',
        '8.x-6.0-alpha1',
        ['7.0.1', '7.0.0', '7.0.0-alpha1', '8.x-6.2', '8.x-6.1', '8.x-6.0'],
      ],
      'contrib, semver and legacy, some lower' => [
        'package_manager_test_update.7.0.1.xml',
        '8.x-6.1',
        ['7.0.1', '7.0.0', '7.0.0-alpha1', '8.x-6.2'],
      ],
      'contrib, semver and legacy, on semantic dev' => [
        'package_manager_test_update.7.0.1.xml',
        '7.0.x-dev',
        ['7.0.1', '7.0.0', '7.0.0-alpha1'],
      ],
      'contrib, semver and legacy, on legacy dev' => [
        'package_manager_test_update.7.0.1.xml',
        '8.x-6.x-dev',
        ['7.0.1', '7.0.0', '7.0.0-alpha1', '8.x-6.2', '8.x-6.1', '8.x-6.0', '8.x-6.0-alpha1'],
      ],
    ];
  }

  /**
   * Tests a project that is not in the codebase.
   */
  public function testNewProject(): void {
    $fixtures_directory = __DIR__ . '/../../fixtures/release-history/';
    $metadata_fixtures['drupal'] = $fixtures_directory . 'drupal.9.8.2.xml';
    $metadata_fixtures['package_manager_test_update'] = $fixtures_directory . 'package_manager_test_update.7.0.1.xml';
    $this->setReleaseMetadata($metadata_fixtures);
    $available = update_get_available(TRUE);
    $this->assertSame(['drupal'], array_keys($available));
    $this->setReleaseMetadata($metadata_fixtures);
    $state = $this->container->get('state');
    // Set the state that the Update Status module uses to store last checked
    // time ensure our calls do not affect it.
    $state->set('update.last_check', 123);
    $project_info = new ProjectInfo('package_manager_test_update');
    $project_data = $project_info->getProjectInfo();
    // Ensure the project information is correct.
    $this->assertSame('Package Manager Test Update', $project_data['title']);
    $all_releases = [
      '7.0.1',
      '7.0.0',
      '7.0.0-alpha1',
      '8.x-6.2',
      '8.x-6.1',
      '8.x-6.0',
      '8.x-6.0-alpha1',
      '7.0.x-dev',
      '8.x-6.x-dev',
      '8.x-5.x',
    ];
    $uninstallable_releases = ['7.0.x-dev', '8.x-6.x-dev', '8.x-5.x'];
    $installable_releases = array_values(array_diff($all_releases, $uninstallable_releases));
    $this->assertSame(
      $all_releases,
      array_keys($project_data['releases'])
    );
    $this->assertSame(
      $installable_releases,
      array_keys($project_info->getInstallableReleases())
    );
    $this->assertNull($project_info->getInstalledVersion());
    // Ensure we have not changed the state the Update Status module uses to
    // store the last checked time.
    $this->assertSame(123, $state->get('update.last_check'));

    $this->assertTrue($this->failureLogger->hasRecordThatContains('Invalid project format: Array', (string) RfcLogLevel::ERROR));
    $this->assertTrue($this->failureLogger->hasRecordThatContains('[name] => Package Manager Test Update 8.x-5.x', (string) RfcLogLevel::ERROR));
    // Prevent the logged errors from causing failures during tear-down.
    $this->failureLogger->reset();
  }

  /**
   * Tests a project with a status other than "published".
   *
   * @legacy-covers ::getInstallableReleases
   */
  public function testNotPublishedProject(): void {
    $this->setReleaseMetadata(['drupal' => __DIR__ . '/../../fixtures/release-history/drupal.9.8.2_unknown_status.xml']);
    $project_info = new ProjectInfo('drupal');
    $this->expectException(\RuntimeException::class);
    $this->expectExceptionMessage("The project 'drupal' can not be updated because its status is any status besides published");
    $project_info->getInstallableReleases();
  }

  /**
   * Data provider for ::testInstalledVersionSafe().
   *
   * @return array[]
   *   The test cases.
   */
  public static function providerInstalledVersionSafe(): array {
    $dir = __DIR__ . '/../../fixtures/release-history';

    return [
      'safe version' => [
        '9.8.0',
        $dir . '/drupal.9.8.2.xml',
        TRUE,
      ],
      'unpublished version' => [
        '9.8.0',
        $dir . '/drupal.9.8.2-unsupported_unpublished.xml',
        FALSE,
      ],
      'unsupported branch' => [
        '9.6.1',
        $dir . '/drupal.9.8.2-unsupported_unpublished.xml',
        FALSE,
      ],
      'unsupported version' => [
        '9.8.1',
        $dir . '/drupal.9.8.2-unsupported_unpublished.xml',
        FALSE,
      ],
      'insecure version' => [
        '9.8.0',
        $dir . '/drupal.9.8.1-security.xml',
        FALSE,
      ],
    ];
  }

  /**
   * Tests checking if the currently installed version of a project is safe.
   *
   * @param string $installed_version
   *   The currently installed version of the project.
   * @param string $release_xml
   *   The path of the release metadata.
   * @param bool $expected_to_be_safe
   *   Whether the installed version of the project is expected to be found
   *   safe.
   *
   * @legacy-covers ::isInstalledVersionSafe
   */
  #[DataProvider('providerInstalledVersionSafe')]
  public function testInstalledVersionSafe(string $installed_version, string $release_xml, bool $expected_to_be_safe): void {
    $this->setCoreVersion($installed_version);
    $this->setReleaseMetadata(['drupal' => $release_xml]);

    $project_info = new ProjectInfo('drupal');
    $this->assertSame($expected_to_be_safe, $project_info->isInstalledVersionSafe());
  }

  /**
   * Data provider for testGetSupportedBranches().
   *
   * @return mixed[][]
   *   The test cases.
   */
  public static function providerGetSupportedBranches(): array {
    $dir = __DIR__ . '/../../fixtures/release-history/';

    return [
      'xml with supported branches' => [
        $dir . 'drupal.10.0.0.xml',
        [
          '9.5.',
          '9.6.',
          '9.7.',
          '10.0.',
        ],
      ],
      'xml with supported branches not set' => [
        $dir . 'drupal.9.8.1-supported_branches_not_set.xml',
        [],
      ],
      'xml with empty supported branches' => [
        $dir . 'drupal.9.8.1-empty_supported_branches.xml',
        [
          '',
        ],
      ],
    ];
  }

  /**
   * Tests get supported branches.
   *
   * @param string $release_xml
   *   The path of the release metadata.
   * @param string[] $expected_supported_branches
   *   The expected supported branches.
   *
   * @legacy-covers ::getSupportedBranches
   */
  #[DataProvider('providerGetSupportedBranches')]
  public function testGetSupportedBranches(string $release_xml, array $expected_supported_branches): void {
    $this->setReleaseMetadata(['drupal' => $release_xml]);
    $project_info = new ProjectInfo('drupal');
    $this->assertSame($expected_supported_branches, $project_info->getSupportedBranches());
  }

}
