<?php

/*
 * This file is part of composer/semver.
 *
 * (c) Composer <https://github.com/composer>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Composer\Semver\Constraint;

use Exception;
use LogicException;
use PHPUnit\Framework\TestCase;
use Composer\Semver\Intervals;

class ConstraintTest extends TestCase
{
    /**
     * @var Constraint
     */
    protected $constraint;
    /**
     * @var Constraint
     */
    protected $versionProvide;

    /**
     * @before
     * @return void
     */
    public function setUpTestCase()
    {
        $this->constraint = new Constraint('==', '1');
        $this->versionProvide = new Constraint('==', 'dev-foo');
    }

    public function testVersionCompareInvalidArgumentException()
    {
        $this->doExpectException('InvalidArgumentException');
        /** @phpstan-ignore-next-line */
        $result = $this->constraint->versionCompare('1.1', '1.2', '!==');
    }

    public function testGetPrettyString()
    {
        $expectedString = 'pretty-string';
        $this->constraint->setPrettyString($expectedString);
        $result = $this->constraint->getPrettyString();

        $this->assertSame($expectedString, $result);

        $expectedVersion = '== 1';
        $this->constraint->setPrettyString(null);
        $result = $this->constraint->getPrettyString();

        $this->assertSame($expectedVersion, $result);
    }

    /**
     * @return array<mixed>
     */
    public static function successfulVersionMatches()
    {
        return array(
            //    require    provide
            array('==', '2', '==', '2'),
            array('==', '2', '<', '3'),
            array('==', '2', '<=', '2'),
            array('==', '2', '<=', '3'),
            array('==', '2', '>=', '1'),
            array('==', '2', '>=', '2'),
            array('==', '2', '>', '1'),
            array('==', '2', '!=', '1'),
            array('==', '2', '!=', '3'),

            array('<', '2', '==', '1'),
            array('<', '2', '<', '1'),
            array('<', '2', '<', '2'),
            array('<', '2', '<', '3'),
            array('<', '2', '<=', '1'),
            array('<', '2', '<=', '2'),
            array('<', '2', '<=', '3'),
            array('<', '2', '>=', '1'),
            array('<', '2', '>', '1'),
            array('<', '2', '!=', '1'),
            array('<', '2', '!=', '2'),
            array('<', '2', '!=', '3'),

            array('<=', '2', '==', '1'),
            array('<=', '2', '==', '2'),
            array('<=', '2', '<', '1'),
            array('<=', '2', '<', '2'),
            array('<=', '2', '<', '3'),
            array('<=', '2', '<=', '1'),
            array('<=', '2', '<=', '2'),
            array('<=', '2', '<=', '3'),
            array('<=', '2', '>=', '1'),
            array('<=', '2', '>=', '2'),
            array('<=', '2', '>', '1'),
            array('<=', '2', '!=', '1'),
            array('<=', '2', '!=', '2'),
            array('<=', '2', '!=', '3'),

            array('>=', '2', '==', '2'),
            array('>=', '2', '==', '3'),
            array('>=', '2', '<', '3'),
            array('>=', '2', '<=', '2'),
            array('>=', '2', '<=', '3'),
            array('>=', '2', '>=', '1'),
            array('>=', '2', '>=', '2'),
            array('>=', '2', '>=', '3'),
            array('>=', '2', '>', '1'),
            array('>=', '2', '>', '2'),
            array('>=', '2', '>', '3'),
            array('>=', '2', '!=', '1'),
            array('>=', '2', '!=', '2'),
            array('>=', '2', '!=', '3'),

            array('>', '2', '==', '3'),
            array('>', '2', '<', '3'),
            array('>', '2', '<=', '3'),
            array('>', '2', '>=', '1'),
            array('>', '2', '>=', '2'),
            array('>', '2', '>=', '3'),
            array('>', '2', '>', '1'),
            array('>', '2', '>', '2'),
            array('>', '2', '>', '3'),
            array('>', '2', '!=', '1'),
            array('>', '2', '!=', '2'),
            array('>', '2', '!=', '3'),

            array('!=', '2', '!=', '1'),
            array('!=', '2', '!=', '2'),
            array('!=', '2', '!=', '3'),
            array('!=', '2', '==', '1'),
            array('!=', '2', '==', '3'),
            array('!=', '2', '<', '1'),
            array('!=', '2', '<', '2'),
            array('!=', '2', '<', '3'),
            array('!=', '2', '<=', '1'),
            array('!=', '2', '<=', '2'),
            array('!=', '2', '<=', '3'),
            array('!=', '2', '>=', '1'),
            array('!=', '2', '>=', '2'),
            array('!=', '2', '>=', '3'),
            array('!=', '2', '>', '1'),
            array('!=', '2', '>', '2'),
            array('!=', '2', '>', '3'),

            // branch names
            array('==', 'dev-foo-bar', '==', 'dev-foo-bar'),
            array('==', 'dev-events+issue-17', '==', 'dev-events+issue-17'),
            array('==', 'dev-foo-bar', '!=', 'dev-foo-xyz'),

            array('!=', 'dev-foo-bar', '!=', 'dev-foo-xyz'),

            // numbers vs branches
            array('==', '0.12', '!=', 'dev-foo'),
            array('<', '0.12', '!=', 'dev-foo'),
            array('<=', '0.12', '!=', 'dev-foo'),
            array('>=', '0.12', '!=', 'dev-foo'),
            array('>', '0.12', '!=', 'dev-foo'),
            array('!=', '0.12', '==', 'dev-foo'),
            array('!=', '0.12', '!=', 'dev-foo'),
        );
    }

    /**
     * @dataProvider successfulVersionMatches
     * @param Constraint::STR_OP_* $requireOperator
     * @param string $requireVersion
     * @param Constraint::STR_OP_* $provideOperator
     * @param string $provideVersion
     */
    public function testVersionMatchSucceeds($requireOperator, $requireVersion, $provideOperator, $provideVersion)
    {
        $versionRequire = new Constraint($requireOperator, $requireVersion);
        $versionProvide = new Constraint($provideOperator, $provideVersion);

        $this->assertTrue($versionRequire->matches($versionProvide));
        $this->assertTrue($this->matchCompiled($versionRequire, $provideOperator, $provideVersion));
        $this->assertTrue(Intervals::haveIntersections($versionRequire, $versionProvide));
        $this->assertTrue(Intervals::compactConstraint($versionRequire)->matches(Intervals::compactConstraint($versionProvide)));
        // the operation should be commutative
        $this->assertTrue($versionProvide->matches($versionRequire));
        $this->assertTrue($this->matchCompiled($versionProvide, $requireOperator, $requireVersion));
        $this->assertTrue(Intervals::haveIntersections($versionProvide, $versionRequire));
        $this->assertTrue(Intervals::compactConstraint($versionProvide)->matches(Intervals::compactConstraint($versionRequire)));
    }

    /**
     * @return array<mixed>
     */
    public static function failingVersionMatches()
    {
        return array(
            //    require    provide
            array('==', '2', '==', '1'),
            array('==', '2', '==', '3'),
            array('==', '2', '<', '1'),
            array('==', '2', '<', '2'),
            array('==', '2', '<=', '1'),
            array('==', '2', '>=', '3'),
            array('==', '2', '>', '2'),
            array('==', '2', '>', '3'),
            array('==', '2', '!=', '2'),

            array('<', '2', '==', '2'),
            array('<', '2', '==', '3'),
            array('<', '2', '>=', '2'),
            array('<', '2', '>=', '3'),
            array('<', '2', '>', '2'),
            array('<', '2', '>', '3'),

            array('<=', '2', '==', '3'),
            array('<=', '2', '>=', '3'),
            array('<=', '2', '>', '2'),
            array('<=', '2', '>', '3'),

            array('>=', '2', '==', '1'),
            array('>=', '2', '<', '1'),
            array('>=', '2', '<', '2'),
            array('>=', '2', '<=', '1'),

            array('>', '2', '==', '1'),
            array('>', '2', '==', '2'),
            array('>', '2', '<', '1'),
            array('>', '2', '<', '2'),
            array('>', '2', '<=', '1'),
            array('>', '2', '<=', '2'),

            array('!=', '2', '==', '2'),

            array('==', '2.0-b2', '<', '2.0-beta2'),
            array('==', 'dev-foo-dist', '==', 'dev-foo-zist'),

            // different branch names
            array('==', 'dev-foo-bar', '==', 'dev-foo-xyz'),
            array('==', 'dev-foo-bar', '<', 'dev-foo-xyz'),
            array('==', 'dev-foo-bar', '<=', 'dev-foo-xyz'),
            array('==', 'dev-foo-bar', '>=', 'dev-foo-xyz'),
            array('==', 'dev-foo-bar', '>', 'dev-foo-xyz'),

            array('<', 'dev-foo-bar', '==', 'dev-foo-xyz'),
            array('<', 'dev-foo-bar', '<', 'dev-foo-xyz'),
            array('<', 'dev-foo-bar', '<=', 'dev-foo-xyz'),
            array('<', 'dev-foo-bar', '>=', 'dev-foo-xyz'),
            array('<', 'dev-foo-bar', '>', 'dev-foo-xyz'),
            array('<', 'dev-foo-bar', '!=', 'dev-foo-xyz'),

            array('<=', 'dev-foo-bar', '==', 'dev-foo-xyz'),
            array('<=', 'dev-foo-bar', '<', 'dev-foo-xyz'),
            array('<=', 'dev-foo-bar', '<=', 'dev-foo-xyz'),
            array('<=', 'dev-foo-bar', '>=', 'dev-foo-xyz'),
            array('<=', 'dev-foo-bar', '>', 'dev-foo-xyz'),
            array('<=', 'dev-foo-bar', '!=', 'dev-foo-xyz'),

            array('>=', 'dev-foo-bar', '==', 'dev-foo-xyz'),
            array('>=', 'dev-foo-bar', '<', 'dev-foo-xyz'),
            array('>=', 'dev-foo-bar', '<=', 'dev-foo-xyz'),
            array('>=', 'dev-foo-bar', '>=', 'dev-foo-xyz'),
            array('>=', 'dev-foo-bar', '>', 'dev-foo-xyz'),
            array('>=', 'dev-foo-bar', '!=', 'dev-foo-xyz'),

            array('>', 'dev-foo-bar', '==', 'dev-foo-xyz'),
            array('>', 'dev-foo-bar', '<', 'dev-foo-xyz'),
            array('>', 'dev-foo-bar', '<=', 'dev-foo-xyz'),
            array('>', 'dev-foo-bar', '>=', 'dev-foo-xyz'),
            array('>', 'dev-foo-bar', '>', 'dev-foo-xyz'),
            array('>', 'dev-foo-bar', '!=', 'dev-foo-xyz'),

            // same branch names
            array('==', 'dev-foo-bar', '<', 'dev-foo-bar'),
            array('==', 'dev-foo-bar', '<=', 'dev-foo-bar'),
            array('==', 'dev-foo-bar', '>=', 'dev-foo-bar'),
            array('==', 'dev-foo-bar', '>', 'dev-foo-bar'),
            array('==', 'dev-foo-bar', '!=', 'dev-foo-bar'),

            array('<', 'dev-foo-bar', '==', 'dev-foo-bar'),
            array('<', 'dev-foo-bar', '<', 'dev-foo-bar'),
            array('<', 'dev-foo-bar', '<=', 'dev-foo-bar'),
            array('<', 'dev-foo-bar', '>=', 'dev-foo-bar'),
            array('<', 'dev-foo-bar', '>', 'dev-foo-bar'),
            array('<', 'dev-foo-bar', '!=', 'dev-foo-bar'),

            array('<=', 'dev-foo-bar', '==', 'dev-foo-bar'),
            array('<=', 'dev-foo-bar', '<', 'dev-foo-bar'),
            array('<=', 'dev-foo-bar', '<=', 'dev-foo-bar'),
            array('<=', 'dev-foo-bar', '>=', 'dev-foo-bar'),
            array('<=', 'dev-foo-bar', '>', 'dev-foo-bar'),
            array('<=', 'dev-foo-bar', '!=', 'dev-foo-bar'),

            array('>=', 'dev-foo-bar', '==', 'dev-foo-bar'),
            array('>=', 'dev-foo-bar', '<', 'dev-foo-bar'),
            array('>=', 'dev-foo-bar', '<=', 'dev-foo-bar'),
            array('>=', 'dev-foo-bar', '>=', 'dev-foo-bar'),
            array('>=', 'dev-foo-bar', '>', 'dev-foo-bar'),
            array('>=', 'dev-foo-bar', '!=', 'dev-foo-bar'),

            array('>', 'dev-foo-bar', '==', 'dev-foo-bar'),
            array('>', 'dev-foo-bar', '<', 'dev-foo-bar'),
            array('>', 'dev-foo-bar', '<=', 'dev-foo-bar'),
            array('>', 'dev-foo-bar', '>=', 'dev-foo-bar'),
            array('>', 'dev-foo-bar', '>', 'dev-foo-bar'),
            array('>', 'dev-foo-bar', '!=', 'dev-foo-bar'),

            // branch vs number, not comparable so mostly false
            array('==', '0.12', '==', 'dev-foo'),
            array('==', '0.12', '<', 'dev-foo'),
            array('==', '0.12', '<=', 'dev-foo'),
            array('==', '0.12', '>=', 'dev-foo'),
            array('==', '0.12', '>', 'dev-foo'),

            array('<', '0.12', '==', 'dev-foo'),
            array('<', '0.12', '<', 'dev-foo'),
            array('<', '0.12', '<=', 'dev-foo'),
            array('<', '0.12', '>=', 'dev-foo'),
            array('<', '0.12', '>', 'dev-foo'),

            array('<=', '0.12', '==', 'dev-foo'),
            array('<=', '0.12', '<', 'dev-foo'),
            array('<=', '0.12', '<=', 'dev-foo'),
            array('<=', '0.12', '>=', 'dev-foo'),
            array('<=', '0.12', '>', 'dev-foo'),

            array('>=', '0.12', '==', 'dev-foo'),
            array('>=', '0.12', '<', 'dev-foo'),
            array('>=', '0.12', '<=', 'dev-foo'),
            array('>=', '0.12', '>=', 'dev-foo'),
            array('>=', '0.12', '>', 'dev-foo'),

            array('>', '0.12', '==', 'dev-foo'),
            array('>', '0.12', '<', 'dev-foo'),
            array('>', '0.12', '<=', 'dev-foo'),
            array('>', '0.12', '>=', 'dev-foo'),
            array('>', '0.12', '>', 'dev-foo'),

            array('!=', '0.12', '<', 'dev-foo'),
            array('!=', '0.12', '<=', 'dev-foo'),
            array('!=', '0.12', '>=', 'dev-foo'),
            array('!=', '0.12', '>', 'dev-foo'),
        );
    }

    /**
     * @dataProvider failingVersionMatches
     * @param Constraint::STR_OP_* $requireOperator
     * @param string $requireVersion
     * @param Constraint::STR_OP_* $provideOperator
     * @param string $provideVersion
     */
    public function testVersionMatchFails($requireOperator, $requireVersion, $provideOperator, $provideVersion)
    {
        $versionRequire = new Constraint($requireOperator, $requireVersion);
        $versionProvide = new Constraint($provideOperator, $provideVersion);

        $this->assertFalse($versionRequire->matches($versionProvide));
        $this->assertFalse($this->matchCompiled($versionRequire, $provideOperator, $provideVersion));
        $this->assertFalse(Intervals::compactConstraint($versionRequire)->matches(Intervals::compactConstraint($versionProvide)));
        // the operation should be commutative
        $this->assertFalse($versionProvide->matches($versionRequire));
        $this->assertFalse($this->matchCompiled($versionProvide, $requireOperator, $requireVersion));
        $this->assertFalse(Intervals::compactConstraint($versionProvide)->matches(Intervals::compactConstraint($versionRequire)));

        // do not test intersections with >/</>=/<= for dev versions as these are not supported
        if (substr($requireVersion, 0, 4) === 'dev-' && $requireOperator !== '==' && $requireOperator !== '!=') {
            return;
        }
        if (substr($provideVersion, 0, 4) === 'dev-' && $provideOperator !== '==' && $provideOperator !== '!=') {
            return;
        }
        $this->assertFalse(Intervals::haveIntersections($versionRequire, $versionProvide));
        $this->assertFalse(Intervals::haveIntersections($versionProvide, $versionRequire));
    }

    public function testInverseMatchingOtherConstraints()
    {
        $constraint = new Constraint('>', '1.0.0');
        $otherConstraintClasses = array(
            'Composer\Semver\Constraint\MultiConstraint',
            'Composer\Semver\Constraint\MatchAllConstraint'
        );

        foreach ($otherConstraintClasses as $otherConstraintClass) {
            $otherConstraintMockBuilder =  $this->getMockBuilder($otherConstraintClass);
            $otherConstraintMockBuilder->disableOriginalConstructor();
            if (method_exists($otherConstraintMockBuilder, 'onlyMethods')) {
                $otherConstraintMockBuilder->onlyMethods(array('matches'));
            } elseif (method_exists($otherConstraintMockBuilder, 'setMethods')) {
                $otherConstraintMockBuilder->setMethods(array('matches'));
            }
            $otherConstraintMock = $otherConstraintMockBuilder->getMock();
            $otherConstraintMock
                ->expects($this->once())
                ->method('matches')
                ->with($constraint)
                ->willReturn(true)
            ;
            /** @var ConstraintInterface $otherConstraintMock */
            $this->assertTrue($constraint->matches($otherConstraintMock));
        }
    }

    public function testComparableBranches()
    {
        $versionRequire = new Constraint('>', '0.12');

        $this->assertFalse($versionRequire->matches($this->versionProvide));
        $this->assertFalse($this->matchCompiled($versionRequire, '==', 'dev-foo'));
        $this->assertFalse(Intervals::haveIntersections($versionRequire, $this->versionProvide));
        $this->assertFalse(Intervals::compactConstraint($versionRequire)->matches(Intervals::compactConstraint($this->versionProvide)));
        $this->assertFalse($versionRequire->matchSpecific($this->versionProvide, true));

        $versionRequire = new Constraint('<', '0.12');

        $this->assertFalse($versionRequire->matches($this->versionProvide));
        $this->assertFalse($this->matchCompiled($versionRequire, '==', 'dev-foo'));
        $this->assertFalse(Intervals::haveIntersections($versionRequire, $this->versionProvide));
        $this->assertFalse(Intervals::compactConstraint($versionRequire)->matches(Intervals::compactConstraint($this->versionProvide)));
        $this->assertTrue($versionRequire->matchSpecific($this->versionProvide, true));
    }

    /**
     * @dataProvider invalidOperators
     *
     * @param string $version
     * @param Constraint::STR_OP_* $operator
     * @param class-string<Exception> $expected
     */
    public function testInvalidOperators($version, $operator, $expected)
    {
        $this->doExpectException($expected);

        new Constraint($operator, $version);
    }

    /**
     * @return array<mixed>
     */
    public static function invalidOperators()
    {
        return array(
            array('1.2.3', 'invalid', 'InvalidArgumentException'),
            array('1.2.3', '!', 'InvalidArgumentException'),
            array('1.2.3', 'equals', 'InvalidArgumentException'),
        );
    }

    /**
     * @dataProvider bounds
     *
     * @param Constraint::STR_OP_* $operator
     * @param string $normalizedVersion
     * @param Bound  $expectedLower
     * @param Bound  $expectedUpper
     */
    public function testBounds($operator, $normalizedVersion, Bound $expectedLower, Bound $expectedUpper)
    {
        $constraint = new Constraint($operator, $normalizedVersion);

        $this->assertEquals($expectedLower, $constraint->getLowerBound(), 'Expected lower bound does not match');
        $this->assertEquals($expectedUpper, $constraint->getUpperBound(), 'Expected upper bound does not match');
    }

    /**
     * @return array<mixed>
     */
    public static function bounds()
    {
        return array(
            'equal to 1.0.0.0' => array('==', '1.0.0.0', new Bound('1.0.0.0', true), new Bound('1.0.0.0', true)),
            'equal to 1.0.0.0-rc3' => array('==', '1.0.0.0-rc3', new Bound('1.0.0.0-rc3', true), new Bound('1.0.0.0-rc3', true)),
            'equal to dev-feature-branch' => array('>=', 'dev-feature-branch', Bound::zero(), Bound::positiveInfinity()),

            'lower than 0.0.4.0' => array('<', '0.0.4.0', Bound::zero(), new Bound('0.0.4.0', false)),
            'lower than 1.0.0.0' => array('<', '1.0.0.0', Bound::zero(), new Bound('1.0.0.0', false)),
            'lower than 2.0.0.0' => array('<', '2.0.0.0', Bound::zero(), new Bound('2.0.0.0', false)),
            'lower than 3.0.3.0' => array('<', '3.0.3.0', Bound::zero(), new Bound('3.0.3.0', false)),
            'lower than 3.0.3.0-rc3' => array('<', '3.0.3.0-rc3', Bound::zero(), new Bound('3.0.3.0-rc3', false)),
            'lower than dev-feature-branch' => array('<', 'dev-feature-branch', Bound::zero(), Bound::positiveInfinity()),

            'greater than 0.0.4.0' => array('>', '0.0.4.0', new Bound('0.0.4.0', false), Bound::positiveInfinity()),
            'greater than 1.0.0.0' => array('>', '1.0.0.0', new Bound('1.0.0.0', false), Bound::positiveInfinity()),
            'greater than 2.0.0.0' => array('>', '2.0.0.0', new Bound('2.0.0.0', false), Bound::positiveInfinity()),
            'greater than 3.0.3.0' => array('>', '3.0.3.0', new Bound('3.0.3.0', false), Bound::positiveInfinity()),
            'greater than 3.0.3.0-rc3' => array('>', '3.0.3.0-rc3', new Bound('3.0.3.0-rc3', false), Bound::positiveInfinity()),
            'greater than dev-feature-branch' => array('>', 'dev-feature-branch', Bound::zero(), Bound::positiveInfinity()),

            'lower than or equal to 0.0.4.0' => array('<=', '0.0.4.0', Bound::zero(), new Bound('0.0.4.0', true)),
            'lower than or equal to 1.0.0.0' => array('<=', '1.0.0.0', Bound::zero(), new Bound('1.0.0.0', true)),
            'lower than or equal to 2.0.0.0' => array('<=', '2.0.0.0', Bound::zero(), new Bound('2.0.0.0', true)),
            'lower than or equal to 3.0.3.0' => array('<=', '3.0.3.0', Bound::zero(), new Bound('3.0.3.0', true)),
            'lower than or equal to 3.0.3.0-rc3' => array('<=', '3.0.3.0-rc3', Bound::zero(), new Bound('3.0.3.0-rc3', true)),
            'lower than or equal to dev-feature-branch' => array('<=', 'dev-feature-branch', Bound::zero(), Bound::positiveInfinity()),

            'greater than or equal to 0.0.4.0' => array('>=', '0.0.4.0', new Bound('0.0.4.0', true), Bound::positiveInfinity()),
            'greater than or equal to 1.0.0.0' => array('>=', '1.0.0.0', new Bound('1.0.0.0', true), Bound::positiveInfinity()),
            'greater than or equal to 2.0.0.0' => array('>=', '2.0.0.0', new Bound('2.0.0.0', true), Bound::positiveInfinity()),
            'greater than or equal to 3.0.3.0' => array('>=', '3.0.3.0', new Bound('3.0.3.0', true), Bound::positiveInfinity()),
            'greater than or equal to 3.0.3.0-rc3' => array('>=', '3.0.3.0-rc3', new Bound('3.0.3.0-rc3', true), Bound::positiveInfinity()),
            'greater than or equal to dev-feature-branch' => array('>=', 'dev-feature-branch', Bound::zero(), Bound::positiveInfinity()),

            'not equal to 1.0.0.0' => array('<>', '1.0.0.0', Bound::zero(), Bound::positiveInfinity()),
        );
    }

    /**
     * @dataProvider matrix
     * @param Constraint::STR_OP_* $requireOperator
     * @param string $requireVersion
     * @param Constraint::STR_OP_* $provideOperator
     * @param string $provideVersion
     */
    public function testCompile($requireOperator, $requireVersion, $provideOperator, $provideVersion)
    {
        $require = new Constraint($requireOperator, $requireVersion);
        $provide = new Constraint($provideOperator, $provideVersion);

        // Asserts Compiled version returns the same result than standard
        $this->assertSame($m = $require->matches($provide), $this->matchCompiled($require, $provideOperator, $provideVersion));
        $this->assertSame($m, Intervals::compactConstraint($require)->matches(Intervals::compactConstraint($provide)));
        $this->assertSame($m, $this->matchCompiled(Intervals::compactConstraint($require), $provideOperator, $provideVersion));

        // do not test >/</>=/<= for dev versions as these are not supported
        if (substr($requireVersion, 0, 4) === 'dev-' && $requireOperator !== '==' && $requireOperator !== '!=') {
            return;
        }
        if (substr($provideVersion, 0, 4) === 'dev-' && $provideOperator !== '==' && $provideOperator !== '!=') {
            return;
        }
        $this->assertSame($m, Intervals::haveIntersections($require, $provide));
    }

    /**
     * @return array<mixed>
     */
    public static function matrix()
    {
        $versions = array('1.0', '2.0', 'dev-master', 'dev-foo', '3.0-b2', '3.0-beta2');
        $operators = array('==', '!=', '>', '<', '>=', '<=');

        $matrix = array();
        foreach ($versions as $requireVersion) {
            foreach ($operators as $requireOperator) {
                foreach ($versions as $provideVersion) {
                    foreach ($operators as $provideOperator) {
                        $matrix[] = array($requireOperator, $requireVersion, $provideOperator, $provideVersion);
                    }
                }
            }
        }

        return $matrix;
    }

    /**
     * @param Constraint::STR_OP_* $operator
     * @param string $version
     * @return bool
     */
    private function matchCompiled(ConstraintInterface $constraint, $operator, $version)
    {
        $map = array(
            '=' => Constraint::OP_EQ,
            '==' => Constraint::OP_EQ,
            '<' => Constraint::OP_LT,
            '<=' => Constraint::OP_LE,
            '>' => Constraint::OP_GT,
            '>=' => Constraint::OP_GE,
            '<>' => Constraint::OP_NE,
            '!=' => Constraint::OP_NE,
        );

        $code = $constraint->compile($map[$operator]);
        $v = $version;
        $b = 'dev-' === substr($v, 0, 4);

        return eval("return $code;");
    }

    /**
     * @param  class-string<Exception> $class
     * @return void
     */
    private function doExpectException($class)
    {
        if (method_exists($this, 'expectException')) {
            $this->expectException($class);
        } elseif (method_exists($this, 'setExpectedException')) {
            $this->setExpectedException($class);
        } else {
            throw new LogicException('Expected method "expectException" or "setExpectedException" to exist.');
        }
    }
}
