<?php
/**
 * Test error handling for the Ruleset.
 *
 * @author    Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
 * @copyright 2024 PHPCSStandards and contributors
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
 */

namespace PHP_CodeSniffer\Tests\Core\Ruleset;

use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Tests\ConfigDouble;
use PHP_CodeSniffer\Tests\Core\Ruleset\AbstractRulesetTestCase;
use PHP_CodeSniffer\Util\MessageCollector;
use ReflectionMethod;
use ReflectionProperty;

/**
 * Test error handling for the Ruleset.
 *
 * Note: this is purely a unit test of the `displayCachedMessages()` method.
 * The errors themselves are mocked.
 *
 * @covers \PHP_CodeSniffer\Ruleset::displayCachedMessages
 */
final class DisplayCachedMessagesTest extends AbstractRulesetTestCase
{


    /**
     * Test that no exception nor output is generated when there are no cached messsages.
     *
     * @return void
     */
    public function testDisplayCachedMessagesStaysSilentWithoutErrors()
    {
        $ruleset = $this->getPlainRuleset();

        $this->expectOutputString('');

        $this->invokeDisplayCachedMessages($ruleset);

    }//end testDisplayCachedMessagesStaysSilentWithoutErrors()


    /**
     * Verify that blocking errors encountered while loading the ruleset(s) result in an exception being thrown.
     *
     * @param array<string, int> $messages The messages encountered.
     * @param string             $expected The expected function output to screen (via an internally handled exception).
     *
     * @dataProvider dataBlockingErrorsAreDisplayedViaAnException
     *
     * @return void
     */
    public function testBlockingErrorsAreDisplayedViaAnException($messages, $expected)
    {
        $ruleset = $this->getPlainRuleset();
        $this->mockCachedMessages($ruleset, $messages);

        $this->expectRuntimeExceptionMessage($expected);

        $this->invokeDisplayCachedMessages($ruleset);

    }//end testBlockingErrorsAreDisplayedViaAnException()


    /**
     * Data provider.
     *
     * @see testBlockingErrorsAreDisplayedViaAnException()
     *
     * @return array<string, array<string, string|array<string, int>>>
     */
    public static function dataBlockingErrorsAreDisplayedViaAnException()
    {
        return [
            'One error'                               => [
                'messages' => ['This is a serious blocking issue' => MessageCollector::ERROR],
                'expected' => 'ERROR: This is a serious blocking issue'.PHP_EOL.PHP_EOL,
            ],
            'Multiple blocking errors'                => [
                'messages' => [
                    'This is a serious blocking issue'        => MessageCollector::ERROR,
                    'And here is another one'                 => MessageCollector::ERROR,
                    'OMG, why do you think that would work ?' => MessageCollector::ERROR,
                ],
                // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important.
                'expected' => 'ERROR: This is a serious blocking issue'.PHP_EOL
                    . 'ERROR: And here is another one'.PHP_EOL
                    . 'ERROR: OMG, why do you think that would work ?'.PHP_EOL.PHP_EOL,
                // phpcs:enable
            ],
            'Mix of blocking and non-blocking errors' => [
                'messages' => [
                    'This is a serious blocking issue'                              => MessageCollector::ERROR,
                    'Something something deprecated and will be removed in v x.x.x' => MessageCollector::DEPRECATED,
                    'Careful, this may not be correct'                              => MessageCollector::NOTICE,
                ],
                // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important.
                'expected' => 'ERROR: This is a serious blocking issue'.PHP_EOL
                    . 'NOTICE: Careful, this may not be correct'.PHP_EOL
                    . 'DEPRECATED: Something something deprecated and will be removed in v x.x.x'.PHP_EOL.PHP_EOL,
                // phpcs:enable
            ],
        ];

    }//end dataBlockingErrorsAreDisplayedViaAnException()


    /**
     * Test display of non-blocking messages encountered while loading the ruleset(s).
     *
     * @param array<string, int> $messages The messages encountered.
     * @param string             $expected The expected function output to screen.
     *
     * @dataProvider dataNonBlockingErrorsGenerateOutput
     *
     * @return void
     */
    public function testNonBlockingErrorsGenerateOutput($messages, $expected)
    {
        $ruleset = $this->getPlainRuleset();
        $this->mockCachedMessages($ruleset, $messages);

        $this->expectOutputString($expected);

        $this->invokeDisplayCachedMessages($ruleset);

    }//end testNonBlockingErrorsGenerateOutput()


    /**
     * Data provider.
     *
     * @see testNonBlockingErrorsGenerateOutput()
     *
     * @return array<string, array<string, string|array<string, int>>>
     */
    public static function dataNonBlockingErrorsGenerateOutput()
    {
        return [
            'One deprecation'              => [
                'messages' => ['My deprecation message' => MessageCollector::DEPRECATED],
                'expected' => 'DEPRECATED: My deprecation message'.PHP_EOL.PHP_EOL,
            ],
            'One notice'                   => [
                'messages' => ['My notice message' => MessageCollector::NOTICE],
                'expected' => 'NOTICE: My notice message'.PHP_EOL.PHP_EOL,
            ],
            'One warning'                  => [
                'messages' => ['My warning message' => MessageCollector::WARNING],
                'expected' => 'WARNING: My warning message'.PHP_EOL.PHP_EOL,
            ],
            'Multiple non-blocking errors' => [
                'messages' => [
                    'Something something deprecated and will be removed in v x.x.x' => MessageCollector::DEPRECATED,
                    'Something is not supported and support may be removed'         => MessageCollector::WARNING,
                    'Some other deprecation notice'                                 => MessageCollector::DEPRECATED,
                    'Careful, this may not be correct'                              => MessageCollector::NOTICE,
                ],
                // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important.
                'expected' => 'WARNING: Something is not supported and support may be removed'.PHP_EOL
                    .'NOTICE: Careful, this may not be correct'.PHP_EOL
                    .'DEPRECATED: Something something deprecated and will be removed in v x.x.x'.PHP_EOL
                    .'DEPRECATED: Some other deprecation notice'.PHP_EOL.PHP_EOL,
                // phpcs:enable
            ],
        ];

    }//end dataNonBlockingErrorsGenerateOutput()


    /**
     * Test that blocking errors will always show, independently of specific command-line options being used.
     *
     * @param array<string> $configArgs Arguments to pass to the Config.
     *
     * @dataProvider dataSelectiveDisplayOfMessages
     *
     * @return void
     */
    public function testBlockingErrorsAlwaysShow($configArgs)
    {
        $config  = new ConfigDouble($configArgs);
        $ruleset = new Ruleset($config);

        $message = 'Some serious error';
        $errors  = [$message => MessageCollector::ERROR];
        $this->mockCachedMessages($ruleset, $errors);

        $this->expectRuntimeExceptionMessage('ERROR: '.$message.PHP_EOL);

        $this->invokeDisplayCachedMessages($ruleset);

    }//end testBlockingErrorsAlwaysShow()


    /**
     * Test that non-blocking messsages will not show when specific command-line options are being used.
     *
     * @param array<string> $configArgs Arguments to pass to the Config.
     *
     * @dataProvider dataSelectiveDisplayOfMessages
     *
     * @return void
     */
    public function testNonBlockingErrorsDoNotShowUnderSpecificCircumstances($configArgs)
    {
        $config  = new ConfigDouble($configArgs);
        $ruleset = new Ruleset($config);
        $this->mockCachedMessages($ruleset, ['Deprecation notice' => MessageCollector::DEPRECATED]);

        $this->expectOutputString('');

        $this->invokeDisplayCachedMessages($ruleset);

    }//end testNonBlockingErrorsDoNotShowUnderSpecificCircumstances()


    /**
     * Data provider.
     *
     * @see testBlockingErrorsAlwaysShow()
     * @see testNonBlockingErrorsDoNotShow()
     *
     * @return array<string, array<string, string|array<string>>>
     */
    public static function dataSelectiveDisplayOfMessages()
    {
        $data = [
            'Explain mode' => [
                'configArgs' => ['-e'],
            ],
            'Quiet mode'   => [
                'configArgs' => ['-q'],
            ],
        ];

        // Setting the `--generator` arg is only supported when running `phpcs`.
        if (PHP_CODESNIFFER_CBF === false) {
            $data['Documentation is requested'] = [
                'configArgs' => ['--generator=text'],
            ];
        }

        return $data;

    }//end dataSelectiveDisplayOfMessages()


    /**
     * Test Helper.
     *
     * @return \PHP_CodeSniffer\Ruleset
     */
    private function getPlainRuleset()
    {
        static $ruleset;

        if (isset($ruleset) === false) {
            $config  = new ConfigDouble();
            $ruleset = new Ruleset($config);
        }

        return $ruleset;

    }//end getPlainRuleset()


    /**
     * Add mock messages to the message cache.
     *
     * @param \PHP_CodeSniffer\Ruleset $ruleset  The ruleset object.
     * @param array<string, int>       $messages The messages to add to the message cache.
     *
     * @return void
     */
    private function mockCachedMessages(Ruleset $ruleset, $messages)
    {
        $reflProperty = new ReflectionProperty($ruleset, 'msgCache');
        (PHP_VERSION_ID < 80100) && $reflProperty->setAccessible(true);

        $msgCache = $reflProperty->getValue($ruleset);
        foreach ($messages as $msg => $type) {
            $msgCache->add($msg, $type);
        }

        (PHP_VERSION_ID < 80100) && $reflProperty->setAccessible(false);

    }//end mockCachedMessages()


    /**
     * Invoke the display of the cached messages.
     *
     * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset object.
     *
     * @return void
     */
    private function invokeDisplayCachedMessages(Ruleset $ruleset)
    {
        $reflMethod = new ReflectionMethod($ruleset, 'displayCachedMessages');
        (PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true);
        $reflMethod->invoke($ruleset);
        (PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false);

    }//end invokeDisplayCachedMessages()


}//end class
