<?php declare(strict_types=1);

/*
 * This file is part of Composer.
 *
 * (c) Nils Adermann <naderman@naderman.de>
 *     Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Composer\Test\Command;

use Composer\Package\Link;
use Composer\Semver\Constraint\MatchAllConstraint;
use Composer\Test\TestCase;
use InvalidArgumentException;

class UpdateCommandTest extends TestCase
{
    /**
     * @dataProvider provideUpdates
     * @param array<mixed> $composerJson
     * @param array<mixed> $command
     */
    public function testUpdate(array $composerJson, array $command, string $expected, bool $createLock = false): void
    {
        $this->initTempComposer($composerJson);

        if ($createLock) {
            $this->createComposerLock();
        }

        $appTester = $this->getApplicationTester();
        $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command));

        self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true)));
    }

    public static function provideUpdates(): \Generator
    {
        $rootDepAndTransitiveDep = [
            'repositories' => [
                'packages' => [
                    'type' => 'package',
                    'package' => [
                        ['name' => 'root/req', 'version' => '1.0.0', 'require' => ['dep/pkg' => '^1']],
                        ['name' => 'dep/pkg', 'version' => '1.0.0'],
                        ['name' => 'dep/pkg', 'version' => '1.0.1'],
                        ['name' => 'dep/pkg', 'version' => '1.0.2'],
                    ],
                ],
            ],
            'require' => [
                'root/req' => '1.*',
            ],
        ];

        yield 'simple update' => [
            $rootDepAndTransitiveDep,
            [],
            <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking dep/pkg (1.0.2)
  - Locking root/req (1.0.0)
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing dep/pkg (1.0.2)
  - Installing root/req (1.0.0)
OUTPUT
        ];

        yield 'simple update with very verbose output' => [
            $rootDepAndTransitiveDep,
            ['-vv' => true],
            <<<OUTPUT
Loading composer repositories with package information
Pool optimizer completed in %f seconds
Found %d package versions referenced in your dependency graph. %d (%d%%) were optimized away.
Updating dependencies
Dependency resolution completed in %f seconds
Analyzed %d packages to resolve dependencies
Analyzed %d rules to resolve dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
Installs: dep/pkg:1.0.2, root/req:1.0.0
  - Locking dep/pkg (1.0.2) from package repo (defining 4 packages)
  - Locking root/req (1.0.0) from package repo (defining 4 packages)
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
Installs: dep/pkg:1.0.2, root/req:1.0.0
  - Installing dep/pkg (1.0.2)
  - Installing root/req (1.0.0)
OUTPUT
        ];

        yield 'update with temporary constraint + --no-install' => [
            $rootDepAndTransitiveDep,
            ['--with' => ['dep/pkg:1.0.0'], '--no-install' => true],
            <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking dep/pkg (1.0.0)
  - Locking root/req (1.0.0)
OUTPUT
        ];

        yield 'update with temporary constraint failing resolution' => [
            $rootDepAndTransitiveDep,
            ['--with' => ['dep/pkg:^2']],
            <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires root/req 1.* -> satisfiable by root/req[1.0.0].
    - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2).
OUTPUT
        ];

        yield 'update with temporary constraint failing resolution on root package' => [
            $rootDepAndTransitiveDep,
            ['--with' => ['root/req:^2']],
            <<<OUTPUT
The temporary constraint "^2" for "root/req" must be a subset of the constraint in your composer.json (1.*)
Run `composer require root/req` or `composer require root/req:^2` instead to replace the constraint
OUTPUT
        ];

        yield 'update & bump' => [
            $rootDepAndTransitiveDep,
            ['--bump-after-update' => true],
            <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking dep/pkg (1.0.2)
  - Locking root/req (1.0.0)
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing dep/pkg (1.0.2)
  - Installing root/req (1.0.0)
Bumping dependencies
<warning>Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.</warning>
<warning>If your package is not a library, you can explicitly specify the "type" by using "composer config type project".</warning>
<warning>Alternatively you can use --dev-only to only bump dependencies within "require-dev".</warning>
No requirements to update in ./composer.json.
OUTPUT
            , true
        ];

        yield 'update & bump dev only' => [
            $rootDepAndTransitiveDep,
            ['--bump-after-update' => 'dev'],
            <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking dep/pkg (1.0.2)
  - Locking root/req (1.0.0)
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing dep/pkg (1.0.2)
  - Installing root/req (1.0.0)
Bumping dependencies
No requirements to update in ./composer.json.
OUTPUT
            , true
        ];

        yield 'update & dump with failing update' => [
            $rootDepAndTransitiveDep,
            ['--with' => ['dep/pkg:^2'], '--bump-after-update' => true],
            <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires root/req 1.* -> satisfiable by root/req[1.0.0].
    - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2).
OUTPUT
        ];
    }

    public function testUpdateWithPatchOnly(): void
    {
        $this->initTempComposer([
            'repositories' => [
                'packages' => [
                    'type' => 'package',
                    'package' => [
                        ['name' => 'root/req', 'version' => '1.0.0'],
                        ['name' => 'root/req', 'version' => '1.0.1'],
                        ['name' => 'root/req', 'version' => '1.1.0'],
                        ['name' => 'root/req2', 'version' => '1.0.0'],
                        ['name' => 'root/req2', 'version' => '1.0.1'],
                        ['name' => 'root/req2', 'version' => '1.1.0'],
                        ['name' => 'root/req3', 'version' => '1.0.0'],
                        ['name' => 'root/req3', 'version' => '1.0.1'],
                        ['name' => 'root/req3', 'version' => '1.1.0'],
                    ],
                ],
            ],
            'require' => [
                'root/req' => '1.*',
                'root/req2' => '1.*',
                'root/req3' => '1.*',
            ],
        ]);

        $package = self::getPackage('root/req', '1.0.0');
        $package2 = self::getPackage('root/req2', '1.0.0');
        $package3 = self::getPackage('root/req3', '1.0.0');
        $this->createComposerLock([$package, $package2, $package3]);

        $appTester = $this->getApplicationTester();
        // root/req fails because of incompatible --with requirement
        $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.1']]));

        $expected = <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires root/req 1.*, found root/req[1.0.0, 1.0.1, 1.1.0] but it conflicts with your temporary update constraint (root/req:[[>= 1.1.0.0-dev < 2.0.0.0-dev] [>= 1.0.0.0-dev < 1.1.0.0-dev]]).
OUTPUT;

        self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true)));

        $appTester = $this->getApplicationTester();
        // root/req upgrades to 1.0.1 as that is compatible with the --with requirement now
        // root/req2 upgrades to 1.0.1 only due to --patch-only
        // root/req3 does not update as it is not in the allowlist
        $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.0.1'], 'packages' => ['root/req', 'root/req2']]));

        $expected = <<<OUTPUT
Loading composer repositories with package information
Updating dependencies
Lock file operations: 0 installs, 2 updates, 0 removals
  - Upgrading root/req (1.0.0 => 1.0.1)
  - Upgrading root/req2 (1.0.0 => 1.0.1)
OUTPUT;

        self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true)));
    }

    public function testInteractiveModeThrowsIfNoPackageToUpdate(): void
    {
        $this->initTempComposer([
            'repositories' => [
                'packages' => [
                    'type' => 'package',
                    'package' => [
                        ['name' => 'root/req', 'version' => '1.0.0'],
                    ],
                ],
            ],
            'require' => [
                'root/req' => '1.*',
            ],
        ]);
        $this->createComposerLock([self::getPackage('root/req', '1.0.0')]);
        self::expectExceptionMessage('Could not find any package with new versions available');

        $appTester = $this->getApplicationTester();
        $appTester->setInputs(['']);
        $appTester->run(['command' => 'update', '--interactive' => true]);
    }

    public function testInteractiveModeThrowsIfNoPackageEntered(): void
    {
        $this->initTempComposer([
            'repositories' => [
                'packages' => [
                    'type' => 'package',
                    'package' => [
                        ['name' => 'root/req', 'version' => '1.0.0'],
                        ['name' => 'root/req', 'version' => '1.0.1'],
                    ],
                ],
            ],
            'require' => [
                'root/req' => '1.*',
            ],
        ]);
        $this->createComposerLock([self::getPackage('root/req', '1.0.0')]);
        self::expectExceptionMessage('No package named "" is installed.');

        $appTester = $this->getApplicationTester();
        $appTester->setInputs(['']);
        $appTester->run(['command' => 'update', '--interactive' => true]);
    }

    /**
     * @dataProvider provideInteractiveUpdates
     * @param array<mixed> $packageNames
     */
    public function testInteractiveTmp(array $packageNames, string $expected): void
    {
        $this->initTempComposer([
            'repositories' => [
                'packages' => [
                    'type' => 'package',
                    'package' => [
                        ['name' => 'root/req', 'version' => '1.0.0', 'require' => ['dep/pkg' => '^1']],
                        ['name' => 'dep/pkg', 'version' => '1.0.0'],
                        ['name' => 'dep/pkg', 'version' => '1.0.1'],
                        ['name' => 'dep/pkg', 'version' => '1.0.2'],
                        ['name' => 'another-dep/pkg', 'version' => '1.0.2'],
                    ],
                ],
            ],
            'require' => [
                'root/req' => '1.*',
            ],
        ]);

        $rootPackage = self::getPackage('root/req');
        $packages = [$rootPackage];

        foreach ($packageNames as $pkg => $ver) {
            $currentPkg = self::getPackage($pkg, $ver);
            array_push($packages, $currentPkg);
        }

        $rootPackage->setRequires([
            'dep/pkg' => new Link(
                'root/req',
                'dep/pkg',
                new MatchAllConstraint(),
                Link::TYPE_REQUIRE,
                '^1'
            ),
            'another-dep/pkg' => new Link(
                'root/req',
                'another-dep/pkg',
                new MatchAllConstraint(),
                Link::TYPE_REQUIRE,
                '^1'
            ),
        ]);

        $this->createComposerLock($packages);
        $this->createInstalledJson($packages);

        $appTester = $this->getApplicationTester();
        $appTester->setInputs(array_merge(array_keys($packageNames), ['', 'yes']));
        $appTester->run([
            'command' => 'update', '--interactive' => true,
            '--no-audit' => true,
            '--dry-run' => true,
        ]);

        self::assertStringEndsWith(
            trim($expected),
            trim($appTester->getDisplay(true))
        );
    }

    public function provideInteractiveUpdates(): \Generator
    {
        yield [
            ['dep/pkg' => '1.0.1'],
            <<<OUTPUT
Lock file operations: 1 install, 1 update, 0 removals
  - Locking another-dep/pkg (1.0.2)
  - Upgrading dep/pkg (1.0.1 => 1.0.2)
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 1 update, 0 removals
  - Upgrading dep/pkg (1.0.1 => 1.0.2)
  - Installing another-dep/pkg (1.0.2)
OUTPUT
        ];

        yield [
            ['dep/pkg' => '1.0.1', 'another-dep/pkg' => '1.0.2'],
            <<<OUTPUT
Lock file operations: 0 installs, 1 update, 0 removals
  - Upgrading dep/pkg (1.0.1 => 1.0.2)
Installing dependencies from lock file (including require-dev)
Package operations: 0 installs, 1 update, 0 removals
  - Upgrading dep/pkg (1.0.1 => 1.0.2)
OUTPUT
        ];
    }
}