<?php declare(strict_types=1);
/*
 * This file is part of PHPUnit.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace PHPUnit\TextUI\XmlConfiguration;

use const DIRECTORY_SEPARATOR;
use const PHP_EOL;
use const PHP_VERSION;
use function define;
use function file_put_contents;
use function iterator_to_array;
use function realpath;
use function sys_get_temp_dir;
use function uniqid;
use function unlink;
use PHPUnit\Framework\Attributes\CoversNamespace;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\IgnorePhpunitDeprecations;
use PHPUnit\Framework\Attributes\Medium;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\TestSuiteSorter;
use PHPUnit\TextUI\Configuration\Configuration;
use SebastianBergmann\CodeCoverage\Report\Html\Colors;
use SebastianBergmann\CodeCoverage\Report\Thresholds;

#[CoversNamespace('PHPUnit\TextUI\XmlConfiguration')]
#[Medium]
#[Group('textui')]
#[Group('textui/configuration')]
#[Group('textui/configuration/xml')]
final class LoaderTest extends TestCase
{
    /**
     * @return array<string, array{string, string, bool|int|string}>
     */
    public static function configurationRootOptionsProvider(): array
    {
        return [
            'executionOrder default'             => ['executionOrder', 'default', TestSuiteSorter::ORDER_DEFAULT],
            'executionOrder random'              => ['executionOrder', 'random', TestSuiteSorter::ORDER_RANDOMIZED],
            'executionOrder reverse'             => ['executionOrder', 'reverse', TestSuiteSorter::ORDER_REVERSED],
            'executionOrder duration-ascending'  => ['executionOrder', 'duration-ascending', TestSuiteSorter::ORDER_DURATION_ASCENDING],
            'executionOrder duration-descending' => ['executionOrder', 'duration-descending', TestSuiteSorter::ORDER_DURATION_DESCENDING],
            'executionOrder size-ascending'      => ['executionOrder', 'size-ascending', TestSuiteSorter::ORDER_SIZE_ASCENDING],
            'executionOrder size-descending'     => ['executionOrder', 'size-descending', TestSuiteSorter::ORDER_SIZE_DESCENDING],
            'cacheDirectory absolute path'       => ['cacheDirectory', '/path/to/cache', '/path/to/cache'],
            'recordTestRunHistory=false'         => ['recordTestRunHistory', 'false', false],
            'recordTestRunHistory=true'          => ['recordTestRunHistory', 'true', true],
            'columns'                            => ['columns', 'max', 'max'],
            'stopOnFailure'                      => ['stopOnFailure', 'true', true],
            'stopOnWarning'                      => ['stopOnWarning', 'true', true],
            'stopOnIncomplete'                   => ['stopOnIncomplete', 'true', true],
            'stopOnRisky'                        => ['stopOnRisky', 'true', true],
            'stopOnSkipped'                      => ['stopOnSkipped', 'true', true],
            'failOnEmptyTestSuite'               => ['failOnEmptyTestSuite', 'true', true],
            'failOnWarning'                      => ['failOnWarning', 'true', true],
            'failOnRisky'                        => ['failOnRisky', 'true', true],
            'processIsolation'                   => ['processIsolation', 'true', true],
            'reverseDefectList'                  => ['reverseDefectList', 'true', true],
            'diffContext'                        => ['diffContext', '5', 5],
        ];
    }

    public function testExceptionIsThrownForNotExistingConfigurationFile(): void
    {
        $this->expectException(Exception::class);

        /* @noinspection UnusedFunctionResultInspection */
        $this->configuration('not_existing_file.xml');
    }

    public function testExceptionIsThrownForConfigurationFileThatDoesNotValidateAgainstAnySupportedSchema(): void
    {
        $this->expectException(Exception::class);
        $this->expectExceptionMessage('does not validate against any supported PHPUnit schema');

        /* @noinspection UnusedFunctionResultInspection */
        $this->configuration('configuration_invalid_root_element.xml');
    }

    public function testShouldReadColorsWhenTrueInConfigurationFile(): void
    {
        $phpunit = $this->configuration('configuration.colors.true.xml')->phpunit();

        $this->assertEquals(Configuration::COLOR_ALWAYS, $phpunit->colors());
    }

    public function testShouldReadColorsWhenFalseInConfigurationFile(): void
    {
        $phpunit = $this->configuration('configuration.colors.false.xml')->phpunit();

        $this->assertEquals(Configuration::COLOR_NEVER, $phpunit->colors());
    }

    public function testShouldReadColorsWhenEmptyInConfigurationFile(): void
    {
        $phpunit = $this->configuration('configuration.colors.empty.xml')->phpunit();

        $this->assertEquals(Configuration::COLOR_NEVER, $phpunit->colors());
    }

    public function testShouldReadColorsWhenInvalidInConfigurationFile(): void
    {
        $phpunit = $this->configuration('configuration.colors.invalid.xml')->phpunit();

        $this->assertEquals(Configuration::COLOR_NEVER, $phpunit->colors());
    }

    public function testInvalidConfigurationGeneratesValidationErrors(): void
    {
        $configuration = $this->configuration('configuration.colors.invalid.xml');

        $this->assertTrue($configuration->hasValidationErrors());
    }

    public function testShouldUseDefaultValuesForInvalidIntegers(): void
    {
        $phpunit = $this->configuration('configuration.columns.default.xml')->phpunit();

        $this->assertEquals(80, $phpunit->columns());
    }

    #[DataProvider('configurationRootOptionsProvider')]
    #[IgnorePhpunitDeprecations]
    public function testShouldParseXmlConfigurationRootAttributes(string $optionName, string $optionValue, bool|int|string $expected): void
    {
        $tmpFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit.' . $optionName . uniqid('', true) . '.xml';
        $xml         = "<phpunit {$optionName}='{$optionValue}'></phpunit>" . PHP_EOL;
        file_put_contents($tmpFilename, $xml);

        $configuration = (new Loader)->load($tmpFilename);

        $this->assertFalse($configuration->hasValidationErrors());

        $this->assertEquals($expected, $configuration->phpunit()->{$optionName}());

        @unlink($tmpFilename);
    }

    #[IgnorePhpunitDeprecations]
    public function testShouldParseDeprecatedExecutionOrderDuration(): void
    {
        $tmpFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit.' . uniqid('', true) . '.xml';
        $xml         = "<phpunit executionOrder='duration'></phpunit>" . PHP_EOL;
        file_put_contents($tmpFilename, $xml);

        $configuration = (new Loader)->load($tmpFilename);

        $this->assertTrue($configuration->hasValidationErrors());
        $this->assertEquals(TestSuiteSorter::ORDER_DURATION_ASCENDING, $configuration->phpunit()->executionOrder());

        @unlink($tmpFilename);
    }

    #[IgnorePhpunitDeprecations]
    public function testShouldParseDeprecatedExecutionOrderSize(): void
    {
        $tmpFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit.' . uniqid('', true) . '.xml';
        $xml         = "<phpunit executionOrder='size'></phpunit>" . PHP_EOL;
        file_put_contents($tmpFilename, $xml);

        $configuration = (new Loader)->load($tmpFilename);

        $this->assertTrue($configuration->hasValidationErrors());
        $this->assertEquals(TestSuiteSorter::ORDER_SIZE_ASCENDING, $configuration->phpunit()->executionOrder());

        @unlink($tmpFilename);
    }

    public function testShouldParseXmlConfigurationExecutionOrderCombined(): void
    {
        $tmpFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit.' . uniqid('', true) . '.xml';
        $xml         = "<phpunit executionOrder='depends,defects'></phpunit>" . PHP_EOL;
        file_put_contents($tmpFilename, $xml);

        $configuration = (new Loader)->load($tmpFilename);

        $this->assertFalse($configuration->hasValidationErrors());

        $this->assertTrue($configuration->phpunit()->defectsFirst());
        $this->assertTrue($configuration->phpunit()->resolveDependencies());

        @unlink($tmpFilename);
    }

    public function testSourceConfigurationIsReadCorrectly(): void
    {
        $source = $this->configuration('configuration_codecoverage.xml')->source();

        $this->assertTrue($source->hasBaseline());
        $this->assertSame(realpath(__DIR__ . '/../../../../_files') . DIRECTORY_SEPARATOR . '.phpunit/baseline.xml', $source->baseline());

        $directory = iterator_to_array($source->includeDirectories(), false)[0];

        $this->assertSame('/path/to/files', $directory->path());
        $this->assertSame('', $directory->prefix());
        $this->assertSame('.php', $directory->suffix());

        $file = iterator_to_array($source->includeFiles(), false)[0];
        $this->assertSame('/path/to/file', $file->path());

        $file = iterator_to_array($source->includeFiles(), false)[1];
        $this->assertSame('/path/to/file', $file->path());

        $directory = iterator_to_array($source->excludeDirectories(), false)[0];
        $this->assertSame('/path/to/files', $directory->path());
        $this->assertSame('', $directory->prefix());
        $this->assertSame('.php', $directory->suffix());

        $file = iterator_to_array($source->excludeFiles(), false)[0];
        $this->assertSame('/path/to/file', $file->path());

        $this->assertSame(
            [
                'functions' => [
                    'PHPUnit\TestFixture\DeprecationTrigger\trigger_deprecation',
                ],
                'methods' => [
                    'PHPUnit\TestFixture\DeprecationTrigger\DeprecationTrigger::triggerDeprecation',
                ],
                'ignoreUndefinedTriggers' => false,
            ],
            $source->deprecationTriggers(),
        );

        $this->assertTrue($source->ignoreSelfDeprecations());
        $this->assertTrue($source->ignoreDirectDeprecations());
        $this->assertTrue($source->ignoreIndirectDeprecations());

        $this->assertSame(
            [
                'PHPUnit\TestFixture\IssueTriggerResolver\FirstResolver',
                'PHPUnit\TestFixture\IssueTriggerResolver\SecondResolver',
            ],
            $source->issueTriggerResolvers(),
        );
    }

    public function testDeprecationFiltersAreReadCorrectlyAndEmptyClassNamesAreIgnored(): void
    {
        $source = $this->configuration('configuration_deprecation_filters.xml')->source();

        $this->assertSame(
            [
                'PHPUnit\TestFixture\DeprecationFilter\FilterA',
            ],
            $source->deprecationFilters(),
        );
    }

    public function testBranchCoverageConfigurationIsReadCorrectly(): void
    {
        $codeCoverage = $this->configuration('configuration_codecoverage_branchcoverage.xml')->codeCoverage();

        $this->assertTrue($codeCoverage->branchCoverage());
    }

    public function testClassViewForHtmlCodeCoverageReportCanBeDisabled(): void
    {
        $codeCoverage = $this->configuration('configuration_codecoverage_html_classview.xml')->codeCoverage();

        $this->assertTrue($codeCoverage->hasHtml());
        $this->assertFalse($codeCoverage->html()->classView());
        $this->assertTrue($codeCoverage->html()->fileView());
    }

    public function testFileViewForHtmlCodeCoverageReportCanBeDisabled(): void
    {
        $codeCoverage = $this->configuration('configuration_codecoverage_html_fileview.xml')->codeCoverage();

        $this->assertTrue($codeCoverage->hasHtml());
        $this->assertTrue($codeCoverage->html()->classView());
        $this->assertFalse($codeCoverage->html()->fileView());
    }

    public function testCodeCoverageConfigurationIsReadCorrectly(): void
    {
        $codeCoverage = $this->configuration('configuration_codecoverage.xml')->codeCoverage();

        $this->assertFalse($codeCoverage->hasDriver());
        $this->assertTrue($codeCoverage->pathCoverage());
        $this->assertTrue($codeCoverage->includeUncoveredFiles());
        $this->assertTrue($codeCoverage->ignoreDeprecatedCodeUnits());
        $this->assertTrue($codeCoverage->disableCodeCoverageIgnore());

        $this->assertTrue($codeCoverage->hasClover());
        $this->assertSame(TEST_FILES_PATH . 'clover.xml', $codeCoverage->clover()->target()->path());

        $this->assertTrue($codeCoverage->hasOpenClover());
        $this->assertSame(TEST_FILES_PATH . 'openclover.xml', $codeCoverage->openClover()->target()->path());

        $this->assertTrue($codeCoverage->hasCobertura());
        $this->assertSame(TEST_FILES_PATH . 'cobertura.xml', $codeCoverage->cobertura()->target()->path());

        $this->assertTrue($codeCoverage->hasCrap4j());
        $this->assertSame(TEST_FILES_PATH . 'crap4j.xml', $codeCoverage->crap4j()->target()->path());

        $defaultColors     = Colors::default();
        $defaultThresholds = Thresholds::default();

        $this->assertTrue($codeCoverage->hasHtml());
        $this->assertSame(TEST_FILES_PATH . 'coverage', $codeCoverage->html()->target()->path());
        $this->assertTrue($codeCoverage->html()->classView());
        $this->assertTrue($codeCoverage->html()->fileView());
        $this->assertSame($defaultThresholds->lowUpperBound(), $codeCoverage->html()->lowUpperBound());
        $this->assertSame($defaultThresholds->highLowerBound(), $codeCoverage->html()->highLowerBound());
        $this->assertSame($defaultColors->successLow(), $codeCoverage->html()->colorSuccessLow());
        $this->assertSame($defaultColors->successLowDark(), $codeCoverage->html()->colorSuccessLowDark());
        $this->assertSame($defaultColors->successMedium(), $codeCoverage->html()->colorSuccessMedium());
        $this->assertSame($defaultColors->successMediumDark(), $codeCoverage->html()->colorSuccessMediumDark());
        $this->assertSame($defaultColors->successHigh(), $codeCoverage->html()->colorSuccessHigh());
        $this->assertSame($defaultColors->successHighDark(), $codeCoverage->html()->colorSuccessHighDark());
        $this->assertSame($defaultColors->successBar(), $codeCoverage->html()->colorSuccessBar());
        $this->assertSame($defaultColors->successBarDark(), $codeCoverage->html()->colorSuccessBarDark());
        $this->assertSame($defaultColors->warning(), $codeCoverage->html()->colorWarning());
        $this->assertSame($defaultColors->warningDark(), $codeCoverage->html()->colorWarningDark());
        $this->assertSame($defaultColors->warningBar(), $codeCoverage->html()->colorWarningBar());
        $this->assertSame($defaultColors->warningBarDark(), $codeCoverage->html()->colorWarningBarDark());
        $this->assertSame($defaultColors->danger(), $codeCoverage->html()->colorDanger());
        $this->assertSame($defaultColors->dangerDark(), $codeCoverage->html()->colorDangerDark());
        $this->assertSame($defaultColors->dangerBar(), $codeCoverage->html()->colorDangerBar());
        $this->assertSame($defaultColors->dangerBarDark(), $codeCoverage->html()->colorDangerBarDark());
        $this->assertSame($defaultColors->breadcrumbs(), $codeCoverage->html()->colorBreadcrumbs());
        $this->assertSame($defaultColors->breadcrumbsDark(), $codeCoverage->html()->colorBreadcrumbsDark());
        $this->assertFalse($codeCoverage->html()->hasCustomCssFile());

        $this->assertTrue($codeCoverage->hasPhp());
        $this->assertSame(TEST_FILES_PATH . 'coverage.php', $codeCoverage->php()->target()->path());

        $this->assertTrue($codeCoverage->hasText());
        $this->assertSame(TEST_FILES_PATH . 'coverage.txt', $codeCoverage->text()->target()->path());
        $this->assertFalse($codeCoverage->text()->showUncoveredFiles());
        $this->assertTrue($codeCoverage->text()->showOnlySummary());

        $this->assertTrue($codeCoverage->hasXml());
        $this->assertSame(TEST_FILES_PATH . 'coverage', $codeCoverage->xml()->target()->path());
    }

    public function testCodeCoverageDriverConfigurationIsReadCorrectly(): void
    {
        $codeCoverage = $this->configuration('configuration_codecoverage_driver.xml')->codeCoverage();

        $this->assertTrue($codeCoverage->hasDriver());
        $this->assertSame('My\Custom\Driver', $codeCoverage->driver());
    }

    public function testCodeCoverageDriverAccessorThrowsWhenNotConfigured(): void
    {
        $codeCoverage = $this->configuration('configuration_codecoverage.xml')->codeCoverage();

        $this->assertFalse($codeCoverage->hasDriver());

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Code Coverage driver has not been configured');

        $codeCoverage->driver();
    }

    public function testEmptyElementsAndAttributesAreIgnored(): void
    {
        $configuration = $this->configuration('configuration_edge_case_values.xml');

        $source = $configuration->source();

        $this->assertCount(1, $source->includeDirectories());
        $this->assertSame('Fixture', $source->includeDirectories()->asArray()[0]->prefix());

        $this->assertSame(
            ['PHPUnit\TestFixture\DeprecationTrigger\trigger_deprecation'],
            $source->deprecationTriggers()['functions'],
        );

        $this->assertSame(
            ['PHPUnit\TestFixture\DeprecationTrigger\DeprecationTrigger::triggerDeprecation'],
            $source->deprecationTriggers()['methods'],
        );

        $this->assertSame([], $source->issueTriggerResolvers());

        $this->assertCount(1, $configuration->groups()->include());
        $this->assertCount(1, $configuration->groups()->exclude());

        $this->assertCount(1, $configuration->php()->iniSettings());
        $this->assertCount(1, $configuration->php()->constants());
        $this->assertCount(1, $configuration->php()->globalVariables());

        $testSuite = $configuration->testSuite()->asArray()[0];

        $this->assertCount(1, $testSuite->directories());
        $this->assertCount(1, $testSuite->files());
        $this->assertSame(['first', 'second'], $testSuite->directories()->asArray()[0]->groups());
        $this->assertSame(['first', 'second'], $testSuite->files()->asArray()[0]->groups());
    }

    public function testInvalidValuesForNumericAttributesAreReplacedWithDefaultValues(): void
    {
        $configuration = $this->configuration('configuration_edge_case_values.xml');

        $this->assertSame(50, $configuration->codeCoverage()->html()->lowUpperBound());
        $this->assertSame(10, $configuration->phpunit()->timeoutForMediumTests());
        $this->assertSame(0, $configuration->phpunit()->shortenArraysForExportThreshold());
    }

    public function testEmptyColorAttributeIsReplacedWithDefaultValue(): void
    {
        $html = $this->configuration('configuration_edge_case_values.xml')->codeCoverage()->html();

        $this->assertSame('#123456', $html->colorSuccessLow());
        $this->assertNotSame('', $html->colorSuccessLowDark());
    }

    public function testEmptyCustomCssFileAttributeIsIgnored(): void
    {
        $this->assertFalse(
            $this->configuration('configuration_edge_case_values.xml')->codeCoverage()->html()->hasCustomCssFile(),
        );
    }

    public function testDependencyResolutionCanBeDisabled(): void
    {
        $this->assertFalse(
            $this->configuration('configuration_edge_case_values.xml')->phpunit()->resolveDependencies(),
        );
    }

    public function testBackupOfStaticPropertiesCanBeEnabled(): void
    {
        $this->assertTrue(
            $this->configuration('configuration_edge_case_values.xml')->phpunit()->backupStaticProperties(),
        );
    }

    public function testNonNumericValuesForNumericAttributesAreReplacedWithDefaultValues(): void
    {
        $configuration = $this->configuration('configuration_non_numeric_values.xml');

        $this->assertSame(1, $configuration->phpunit()->defaultTimeLimit());
        $this->assertSame(1, $configuration->phpunit()->timeoutForSmallTests());
    }

    #[RunInSeparateProcess]
    public function testAbsolutePathOnWindowsIsNotResolvedRelativeToConfigurationFile(): void
    {
        define('PHP_WINDOWS_VERSION_BUILD', 1);

        $this->assertSame(
            'C:\\Temp\\clover.xml',
            $this->configuration('configuration_windows_path.xml')->codeCoverage()->clover()->target()->path(),
        );
    }

    public function testConfigurationLoadedFromFileIsNotTheDefaultConfiguration(): void
    {
        $configuration = $this->configuration('configuration.xml');

        $this->assertFalse($configuration->isDefault());
        $this->assertTrue($configuration->wasLoadedFromFile());
    }

    public function testDefaultConfigurationIsTheDefaultConfiguration(): void
    {
        $configuration = DefaultConfiguration::create();

        $this->assertTrue($configuration->isDefault());
        $this->assertFalse($configuration->wasLoadedFromFile());
    }

    public function testGroupConfigurationIsReadCorrectly(): void
    {
        $groups = $this->configuration('configuration.xml')->groups();

        $this->assertTrue($groups->hasInclude());
        $this->assertSame(['name'], $groups->include()->asArrayOfStrings());

        $this->assertTrue($groups->hasExclude());
        $this->assertSame(['name'], $groups->exclude()->asArrayOfStrings());
    }

    public function testExtensionConfigurationIsReadCorrectly(): void
    {
        $extensions = $this->configuration('configuration.xml')->extensions();

        $this->assertCount(1, $extensions->asArray());

        $extension = $extensions->asArray()[0];

        $this->assertSame('MyExtension', $extension->className());

        $this->assertSame(
            [
                'foo' => 'bar',
                'bar' => 'foo',
            ],
            $extension->parameters(),
        );
    }

    public function testLoggingConfigurationIsReadCorrectly(): void
    {
        $logging = $this->configuration('configuration_logging.xml')->logging();

        $this->assertTrue($logging->hasJunit());
        $this->assertSame(TEST_FILES_PATH . 'junit.xml', $logging->junit()->target()->path());

        $this->assertTrue($logging->hasOtr());
        $this->assertSame(TEST_FILES_PATH . 'otr.xml', $logging->otr()->target()->path());
        $this->assertTrue($logging->otr()->includeGitInformation());

        $this->assertTrue($logging->hasTeamCity());
        $this->assertSame(TEST_FILES_PATH . 'teamcity.txt', $logging->teamCity()->target()->path());

        $this->assertTrue($logging->hasTestDoxHtml());
        $this->assertSame(TEST_FILES_PATH . 'testdox.html', $logging->testDoxHtml()->target()->path());

        $this->assertTrue($logging->hasTestDoxText());
        $this->assertSame(TEST_FILES_PATH . 'testdox.txt', $logging->testDoxText()->target()->path());
    }

    public function testPHPConfigurationIsReadCorrectly(): void
    {
        $php = $this->configuration('configuration.xml')->php();

        $this->assertSame(TEST_FILES_PATH . '.', $php->includePaths()->asArray()[0]->path());
        $this->assertSame('/path/to/lib', $php->includePaths()->asArray()[1]->path());

        $this->assertSame('highlight.keyword', $php->iniSettings()->asArray()[0]->name());
        $this->assertSame('#123456', $php->iniSettings()->asArray()[0]->value());

        $this->assertSame('FOO', $php->constants()->asArray()[0]->name());
        $this->assertFalse($php->constants()->asArray()[0]->value());
        $this->assertSame('BAR', $php->constants()->asArray()[1]->name());
        $this->assertTrue($php->constants()->asArray()[1]->value());

        $this->assertSame('foo', $php->globalVariables()->asArray()[0]->name());
        $this->assertFalse($php->globalVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->postVariables()->asArray()[0]->name());
        $this->assertSame('bar', $php->postVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->getVariables()->asArray()[0]->name());
        $this->assertSame('bar', $php->getVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->cookieVariables()->asArray()[0]->name());
        $this->assertSame('bar', $php->cookieVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->serverVariables()->asArray()[0]->name());
        $this->assertSame('bar', $php->serverVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->filesVariables()->asArray()[0]->name());
        $this->assertSame('bar', $php->filesVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->requestVariables()->asArray()[0]->name());
        $this->assertSame('bar', $php->requestVariables()->asArray()[0]->value());

        $this->assertSame('foo', $php->envVariables()->asArray()[0]->name());
        $this->assertTrue($php->envVariables()->asArray()[0]->value());
        $this->assertFalse($php->envVariables()->asArray()[0]->force());

        $this->assertSame('foo_force', $php->envVariables()->asArray()[1]->name());
        $this->assertSame('forced', $php->envVariables()->asArray()[1]->value());
        $this->assertTrue($php->envVariables()->asArray()[1]->force());

        $this->assertSame('bar', $php->envVariables()->asArray()[2]->name());
        $this->assertSame('true', $php->envVariables()->asArray()[2]->value());
        $this->assertFalse($php->envVariables()->asArray()[2]->force());
    }

    public function testPHPUnitConfigurationIsReadCorrectly(): void
    {
        $phpunit = $this->configuration('configuration.xml')->phpunit();

        $this->assertTrue($phpunit->backupGlobals());
        $this->assertFalse($phpunit->backupStaticProperties());
        $this->assertFalse($phpunit->beStrictAboutChangesToGlobalState());
        $this->assertSame('/path/to/bootstrap.php', $phpunit->bootstrap());
        $this->assertSame(80, $phpunit->columns());
        $this->assertSame('never', $phpunit->colors());
        $this->assertFalse($phpunit->stderr());
        $this->assertFalse($phpunit->requireCoverageMetadata());
        $this->assertSame(0, $phpunit->stopOnFailure());
        $this->assertSame(0, $phpunit->stopOnWarning());
        $this->assertFalse($phpunit->beStrictAboutTestsThatDoNotTestAnything());
        $this->assertFalse($phpunit->beStrictAboutCoverageMetadata());
        $this->assertFalse($phpunit->beStrictAboutOutputDuringTests());
        $this->assertSame(123, $phpunit->defaultTimeLimit());
        $this->assertSame(7, $phpunit->diffContext());
        $this->assertFalse($phpunit->enforceTimeLimit());
        $this->assertSame('/tmp', $phpunit->extensionsDirectory());
        $this->assertSame('My Test Suite', $phpunit->defaultTestSuite());
        $this->assertSame(1, $phpunit->timeoutForSmallTests());
        $this->assertSame(10, $phpunit->timeoutForMediumTests());
        $this->assertSame(60, $phpunit->timeoutForLargeTests());
        $this->assertFalse($phpunit->failOnEmptyTestSuite());
        $this->assertFalse($phpunit->failOnIncomplete());
        $this->assertFalse($phpunit->failOnRisky());
        $this->assertFalse($phpunit->failOnSkipped());
        $this->assertFalse($phpunit->failOnWarning());
        $this->assertSame(TestSuiteSorter::ORDER_DEFAULT, $phpunit->executionOrder());
        $this->assertFalse($phpunit->defectsFirst());
        $this->assertTrue($phpunit->resolveDependencies());
        $this->assertTrue($phpunit->controlGarbageCollector());
        $this->assertSame(1000, $phpunit->numberOfTestsBeforeGarbageCollection());
        $this->assertSame(10, $phpunit->shortenArraysForExportThreshold());
        $this->assertTrue($phpunit->warnWhenPhpIsNotConfiguredForDevelopment());
    }

    public function test_TestDox_configuration_is_parsed_correctly(): void
    {
        $configuration = $this->configuration('configuration_testdox.xml')->phpunit();

        $this->assertTrue($configuration->testdoxPrinter());
        $this->assertTrue($configuration->testdoxPrinterSummary());
    }

    public function testConfigurationForSingleTestSuiteCanBeLoaded(): void
    {
        $testSuites = $this->configuration('configuration_testsuite.xml')->testSuite();

        $this->assertCount(1, $testSuites);

        $first = $testSuites->asArray()[0];
        $this->assertSame('first', $first->name());
        $this->assertCount(1, $first->directories());
        $this->assertSame(TEST_FILES_PATH . 'tests/first', $first->directories()->asArray()[0]->path());
        $this->assertSame('', $first->directories()->asArray()[0]->prefix());
        $this->assertSame('Test.php', $first->directories()->asArray()[0]->suffix());
        $this->assertSame(PHP_VERSION, $first->directories()->asArray()[0]->phpVersion());
        $this->assertSame('>=', $first->directories()->asArray()[0]->phpVersionOperator()->asString());
        $this->assertCount(0, $first->files());
        $this->assertCount(0, $first->exclude());
    }

    public function testConfigurationForMultipleTestSuitesCanBeLoaded(): void
    {
        $testSuites = $this->configuration('configuration_testsuites.xml')->testSuite();

        $this->assertCount(2, $testSuites);

        $first = $testSuites->asArray()[0];
        $this->assertSame('first', $first->name());
        $this->assertCount(1, $first->directories());
        $this->assertSame(TEST_FILES_PATH . 'tests/first', $first->directories()->asArray()[0]->path());
        $this->assertSame('', $first->directories()->asArray()[0]->prefix());
        $this->assertSame('Test.php', $first->directories()->asArray()[0]->suffix());
        $this->assertSame(PHP_VERSION, $first->directories()->asArray()[0]->phpVersion());
        $this->assertSame('>=', $first->directories()->asArray()[0]->phpVersionOperator()->asString());
        $this->assertCount(0, $first->files());
        $this->assertCount(0, $first->exclude());
        $this->assertSame(['foo'], $first->directories()->asArray()[0]->groups());

        $second = $testSuites->asArray()[1];
        $this->assertSame('second', $second->name());
        $this->assertSame(TEST_FILES_PATH . 'tests/second', $second->directories()->asArray()[0]->path());
        $this->assertSame('test', $second->directories()->asArray()[0]->prefix());
        $this->assertSame('.phpt', $second->directories()->asArray()[0]->suffix());
        $this->assertSame('1.2.3', $second->directories()->asArray()[0]->phpVersion());
        $this->assertSame('==', $second->directories()->asArray()[0]->phpVersionOperator()->asString());
        $this->assertCount(1, $second->files());
        $this->assertSame(TEST_FILES_PATH . 'tests/file.php', $second->files()->asArray()[0]->path());
        $this->assertSame('4.5.6', $second->files()->asArray()[0]->phpVersion());
        $this->assertSame('!=', $second->files()->asArray()[0]->phpVersionOperator()->asString());
        $this->assertCount(1, $second->exclude());
        $this->assertSame(TEST_FILES_PATH . 'tests/second/_files', $second->exclude()->asArray()[0]->path());
        $this->assertSame(['bar'], $second->directories()->asArray()[0]->groups());
        $this->assertSame(['baz'], $second->files()->asArray()[0]->groups());
    }

    private function configuration(string $filename): LoadedFromFileConfiguration
    {
        return (new Loader)->load(TEST_FILES_PATH . $filename);
    }
}
