<?php

/*
 * This file is part of the Predis package.
 *
 * (c) 2009-2020 Daniele Alessandri
 * (c) 2021-2026 Till Krüss
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Predis\Command;

use Predis\Command\Processor\ProcessorChain;
use Predis\Command\Processor\ProcessorInterface;
use PredisTestCase;

class RedisFactoryTest extends PredisTestCase
{
    /**
     * @group disconnected
     */
    public function testSupportedCommands(): void
    {
        $factory = new RedisFactory();

        foreach ($this->getExpectedCommands() as $commandID) {
            $this->assertTrue($factory->supports($commandID), "Command factory does not support $commandID");
        }
    }

    /**
     * @group disconnected
     */
    public function testSupportCommand(): void
    {
        $factory = new RedisFactory();

        $this->assertTrue($factory->supports('info'));
        $this->assertTrue($factory->supports('INFO'));

        $this->assertFalse($factory->supports('unknown'));
        $this->assertFalse($factory->supports('UNKNOWN'));
    }

    /**
     * @group disconnected
     */
    public function testSupportCommands(): void
    {
        $factory = new RedisFactory();

        $this->assertTrue($factory->supports('get', 'set'));
        $this->assertTrue($factory->supports('GET', 'SET'));

        $this->assertFalse($factory->supports('get', 'unknown'));

        $this->assertFalse($factory->supports('unknown1', 'unknown2'));
    }

    /**
     * @group disconnected
     */
    public function testGetCommandClass(): void
    {
        $factory = new RedisFactory();

        $this->assertSame('Predis\Command\Redis\PING', $factory->getCommandClass('ping'));
        $this->assertSame('Predis\Command\Redis\PING', $factory->getCommandClass('PING'));

        $this->assertNull($factory->getCommandClass('unknown'));
        $this->assertNull($factory->getCommandClass('UNKNOWN'));
    }

    /**
     * @group disconnected
     */
    public function testDefineCommand(): void
    {
        $factory = new RedisFactory();

        $command = $this->getMockBuilder('Predis\Command\CommandInterface')
            ->getMock();

        $factory->define('mock', get_class($command));

        $this->assertTrue($factory->supports('mock'));
        $this->assertTrue($factory->supports('MOCK'));

        $this->assertInstanceOf($factory->getCommandClass('mock'), $command);
    }

    /**
     * @group disconnected
     */
    public function testUndefineCommandInClassAutoload(): void
    {
        $factory = new RedisFactory();

        $this->assertTrue($factory->supports('PING'));
        $this->assertSame('Predis\Command\Redis\PING', $factory->getCommandClass('PING'));

        $factory->undefine('PING');

        $this->assertFalse($factory->supports('PING'));
        $this->assertNull($factory->getCommandClass('PING'));
    }

    /**
     * @group disconnected
     */
    public function testUndefineCommandInClassMap(): void
    {
        $factory = new RedisFactory();

        $commandClass = get_class($this->getMockBuilder('Predis\Command\CommandInterface')->getMock());
        $factory->define('MOCK', $commandClass);

        $this->assertTrue($factory->supports('MOCK'));
        $this->assertSame($commandClass, $factory->getCommandClass('MOCK'));

        $factory->undefine('MOCK');

        $this->assertFalse($factory->supports('MOCK'));
        $this->assertNull($factory->getCommandClass('MOCK'));
    }

    /**
     * @group disconnected
     */
    public function testDefineInvalidCommand(): void
    {
        $this->expectException('InvalidArgumentException');
        $this->expectExceptionMessage("Class stdClass must implement Predis\Command\CommandInterface");

        $factory = new RedisFactory();

        $factory->define('mock', 'stdClass');
    }

    /**
     * @group disconnected
     */
    public function testCreateCommandWithoutArguments(): void
    {
        $factory = new RedisFactory();

        $command = $factory->create('info');

        $this->assertInstanceOf('Predis\Command\CommandInterface', $command);
        $this->assertEquals('INFO', $command->getId());
        $this->assertEquals([], $command->getArguments());
    }

    /**
     * @group disconnected
     */
    public function testCreateCommandWithArguments(): void
    {
        $factory = new RedisFactory();

        $arguments = ['foo', 'bar'];
        $command = $factory->create('set', $arguments);

        $this->assertInstanceOf('Predis\Command\CommandInterface', $command);
        $this->assertEquals('SET', $command->getId());
        $this->assertEquals($arguments, $command->getArguments());
    }

    /**
     * @group disconnected
     */
    public function testCreateUndefinedCommand(): void
    {
        $this->expectException('Predis\ClientException');
        $this->expectExceptionMessage('Command `UNKNOWN` is not a registered Redis command.');

        $factory = new RedisFactory();

        $factory->create('unknown');
    }

    /**
     * @group disconnected
     */
    public function testGetDefaultProcessor(): void
    {
        $factory = new RedisFactory();

        $this->assertNull($factory->getProcessor());
    }

    /**
     * @group disconnected
     */
    public function testSetProcessor(): void
    {
        /** @var ProcessorInterface */
        $processor = $this
            ->getMockBuilder('Predis\Command\Processor\ProcessorInterface')
            ->getMock();

        $factory = new RedisFactory();
        $factory->setProcessor($processor);

        $this->assertSame($processor, $factory->getProcessor());
    }

    /**
     * @group disconnected
     */
    public function testSetAndUnsetProcessor(): void
    {
        /** @var ProcessorInterface */
        $processor = $this
            ->getMockBuilder('Predis\Command\Processor\ProcessorInterface')
            ->getMock();

        $factory = new RedisFactory();

        $factory->setProcessor($processor);
        $this->assertSame($processor, $factory->getProcessor());

        $factory->setProcessor(null);
        $this->assertNull($factory->getProcessor());
    }

    /**
     * @group disconnected
     */
    public function testSingleProcessor(): void
    {
        // Could it be that objects passed to the return callback of a mocked
        // method are cloned instead of being passed by reference?
        $argsRef = null;

        /** @var ProcessorInterface */
        $processor = $this
            ->getMockBuilder('Predis\Command\Processor\ProcessorInterface')
            ->getMock();
        $processor
            ->expects($this->once())
            ->method('process')
            ->with($this->isInstanceOf('Predis\Command\CommandInterface'))
            ->willReturnCallback(
                static function (CommandInterface $cmd) use (&$argsRef) {
                    $cmd->setRawArguments($argsRef = array_map('strtoupper', $cmd->getArguments()));
                }
            );

        $factory = new RedisFactory();
        $factory->setProcessor($processor);
        $factory->create('set', ['foo', 'bar']);

        $this->assertSame(['FOO', 'BAR'], $argsRef);
    }

    /**
     * @group disconnected
     */
    public function testChainOfProcessors(): void
    {
        /** @var ProcessorInterface */
        $processor = $this
            ->getMockBuilder('Predis\Command\Processor\ProcessorInterface')
            ->getMock();
        $processor
            ->expects($this->exactly(2))
            ->method('process');

        $chain = new ProcessorChain();
        $chain->add($processor);
        $chain->add($processor);

        $factory = new RedisFactory();
        $factory->setProcessor($chain);

        $factory->create('info');
    }

    // ******************************************************************** //
    // ---- HELPER METHODS ------------------------------------------------ //
    // ******************************************************************** //

    /**
     * Returns the expected list of commands supported by the tested factory.
     *
     * @return array List of supported commands.
     */
    protected function getExpectedCommands(): array
    {
        return [
            0 => 'EXISTS',
            1 => 'DEL',
            2 => 'TYPE',
            3 => 'KEYS',
            4 => 'RANDOMKEY',
            5 => 'RENAME',
            6 => 'RENAMENX',
            7 => 'EXPIRE',
            8 => 'EXPIREAT',
            9 => 'TTL',
            10 => 'MOVE',
            11 => 'SORT',
            12 => 'DUMP',
            13 => 'RESTORE',
            14 => 'SET',
            15 => 'SETNX',
            16 => 'MSET',
            17 => 'MSETNX',
            18 => 'GET',
            19 => 'MGET',
            20 => 'GETSET',
            21 => 'INCR',
            22 => 'INCRBY',
            23 => 'DECR',
            24 => 'DECRBY',
            25 => 'RPUSH',
            26 => 'LPUSH',
            27 => 'LLEN',
            28 => 'LRANGE',
            29 => 'LTRIM',
            30 => 'LINDEX',
            31 => 'LSET',
            32 => 'LREM',
            33 => 'LPOP',
            34 => 'RPOP',
            35 => 'RPOPLPUSH',
            36 => 'SADD',
            37 => 'SREM',
            38 => 'SPOP',
            39 => 'SMOVE',
            40 => 'SCARD',
            41 => 'SISMEMBER',
            42 => 'SINTER',
            43 => 'SINTERSTORE',
            44 => 'SUNION',
            45 => 'SUNIONSTORE',
            46 => 'SDIFF',
            47 => 'SDIFFSTORE',
            48 => 'SMEMBERS',
            49 => 'SRANDMEMBER',
            50 => 'ZADD',
            51 => 'ZINCRBY',
            52 => 'ZREM',
            53 => 'ZRANGE',
            54 => 'ZREVRANGE',
            55 => 'ZRANGEBYSCORE',
            56 => 'ZCARD',
            57 => 'ZSCORE',
            58 => 'ZREMRANGEBYSCORE',
            59 => 'PING',
            60 => 'AUTH',
            61 => 'SELECT',
            62 => 'ECHO',
            63 => 'QUIT',
            64 => 'INFO',
            65 => 'SLAVEOF',
            66 => 'MONITOR',
            67 => 'DBSIZE',
            68 => 'FLUSHDB',
            69 => 'FLUSHALL',
            70 => 'SAVE',
            71 => 'BGSAVE',
            72 => 'LASTSAVE',
            73 => 'SHUTDOWN',
            74 => 'BGREWRITEAOF',
            75 => 'SETEX',
            76 => 'APPEND',
            77 => 'SUBSTR',
            78 => 'BLPOP',
            79 => 'BRPOP',
            80 => 'ZUNIONSTORE',
            81 => 'ZINTERSTORE',
            82 => 'ZCOUNT',
            83 => 'ZRANK',
            84 => 'ZREVRANK',
            85 => 'ZREMRANGEBYRANK',
            86 => 'HSET',
            87 => 'HSETNX',
            88 => 'HMSET',
            89 => 'HINCRBY',
            90 => 'HGET',
            91 => 'HMGET',
            92 => 'HDEL',
            93 => 'HEXISTS',
            94 => 'HLEN',
            95 => 'HKEYS',
            96 => 'HVALS',
            97 => 'HGETALL',
            98 => 'MULTI',
            99 => 'EXEC',
            100 => 'DISCARD',
            101 => 'SUBSCRIBE',
            102 => 'UNSUBSCRIBE',
            103 => 'PSUBSCRIBE',
            104 => 'PUNSUBSCRIBE',
            105 => 'PUBLISH',
            106 => 'CONFIG',
            107 => 'PERSIST',
            108 => 'STRLEN',
            109 => 'SETRANGE',
            110 => 'GETRANGE',
            111 => 'SETBIT',
            112 => 'GETBIT',
            113 => 'RPUSHX',
            114 => 'LPUSHX',
            115 => 'LINSERT',
            116 => 'BRPOPLPUSH',
            117 => 'ZREVRANGEBYSCORE',
            118 => 'WATCH',
            119 => 'UNWATCH',
            120 => 'OBJECT',
            121 => 'SLOWLOG',
            122 => 'CLIENT',
            123 => 'PTTL',
            124 => 'PEXPIRE',
            125 => 'PEXPIREAT',
            126 => 'MIGRATE',
            127 => 'PSETEX',
            128 => 'INCRBYFLOAT',
            129 => 'BITOP',
            130 => 'BITCOUNT',
            131 => 'HINCRBYFLOAT',
            132 => 'EVAL',
            133 => 'EVALSHA',
            134 => 'SCRIPT',
            135 => 'TIME',
            136 => 'SENTINEL',
            137 => 'SCAN',
            138 => 'BITPOS',
            139 => 'SSCAN',
            140 => 'ZSCAN',
            141 => 'ZLEXCOUNT',
            142 => 'ZRANGEBYLEX',
            143 => 'ZREMRANGEBYLEX',
            144 => 'ZREVRANGEBYLEX',
            145 => 'HSCAN',
            146 => 'PUBSUB',
            147 => 'PFADD',
            148 => 'PFCOUNT',
            149 => 'PFMERGE',
            150 => 'COMMAND',
            151 => 'HSTRLEN',
            152 => 'BITFIELD',
            153 => 'GEOADD',
            154 => 'GEOHASH',
            155 => 'GEOPOS',
            156 => 'GEODIST',
            157 => 'GEORADIUS',
            158 => 'GEORADIUSBYMEMBER',
            159 => 'JSONSET',
            160 => 'JSONGET',
            161 => 'JSONARRAPPEND',
            162 => 'JSONARRINDEX',
            163 => 'JSONARRINSERT',
            164 => 'JSONARRLEN',
            165 => 'JSONARRPOP',
            166 => 'JSONARRTRIM',
            167 => 'JSONCLEAR',
            168 => 'JSONDEBUG',
            169 => 'JSONDEL',
            170 => 'JSONFORGET',
            171 => 'JSONMGET',
            172 => 'JSONNUMINCRBY',
            173 => 'JSONOBJKEYS',
            174 => 'JSONOBJLEN',
            175 => 'JSONRESP',
            176 => 'JSONSTRAPPEND',
            177 => 'JSONSTRLEN',
            178 => 'JSONTOGGLE',
            179 => 'JSONTYPE',
            180 => 'BFADD',
            181 => 'BFEXISTS',
            182 => 'BFINFO',
            183 => 'BFINSERT',
            184 => 'BFLOADCHUNK',
            185 => 'BFMADD',
            186 => 'BFMEXISTS',
            187 => 'BFRESERVE',
            188 => 'BFSCANDUMP',
            189 => 'CMSINCRBY',
            190 => 'CMSINFO',
            191 => 'CMSINITBYDIM',
            192 => 'CMSINITBYPROB',
            193 => 'CMSMERGE',
            194 => 'CMSQUERY',
            195 => 'CFADD',
            196 => 'CFADDNX',
            197 => 'CFCOUNT',
            198 => 'CFDEL',
            199 => 'CFEXISTS',
            200 => 'CFINFO',
            201 => 'CFINSERT',
            202 => 'CFINSERTNX',
            203 => 'CFLOADCHUNK',
            204 => 'CFMEXISTS',
            205 => 'CFRESERVE',
            206 => 'CFSCANDUMP',
            207 => 'TDIGESTADD',
            208 => 'TDIGESTBYRANK',
            209 => 'TDIGESTBYREVRANK',
            210 => 'TDIGESTCDF',
            211 => 'TDIGESTCREATE',
            212 => 'TDIGESTINFO',
            213 => 'TDIGESTMAX',
            214 => 'TDIGESTMERGE',
            215 => 'TDIGESTMIN',
            216 => 'TDIGESTQUANTILE',
            217 => 'TDIGESTRANK',
            218 => 'TDIGESTRESET',
            219 => 'TDIGESTREVRANK',
            220 => 'TDIGESTTRIMMED_MEAN',
            221 => 'TOPKADD',
            222 => 'TOPKINCRBY',
            223 => 'TOPKINFO',
            224 => 'TOPKLIST',
            225 => 'TOPKQUERY',
            226 => 'TOPKRESERVE',
            227 => 'FTALIASADD',
            228 => 'FTALIASDEL',
            229 => 'FTALIASUPDATE',
            230 => 'FTALTER',
            231 => 'FTCONFIG',
            232 => 'FTCREATE',
            233 => 'FTDICTADD',
            234 => 'FTDICTDEL',
            235 => 'FTDICTDUMP',
            236 => 'FTDROPINDEX',
            237 => 'FTINFO',
            238 => 'FTPROFILE',
            239 => 'FTSEARCH',
            240 => 'FTSPELLCHECK',
            241 => 'FTSUGADD',
            242 => 'FTSUGDEL',
            243 => 'FTSUGGET',
            244 => 'FTSUGLEN',
            245 => 'FTSYNDUMP',
            246 => 'FTSYNUPDATE',
            247 => 'FTTAGVALS',
        ];
    }
}
