<?php defined('PHPREDIS_TESTRUN') or die("Use TestRedis.php to run tests!\n");
require_once(dirname($_SERVER['PHP_SELF'])."/RedisTest.php");

/**
 * Most RedisCluster tests should work the same as the standard Redis object
 * so we only override specific functions where the prototype is different or
 * where we're validating specific cluster mechanisms
 */
class Redis_Cluster_Test extends Redis_Test {
    private static $_arr_node_map = Array();

    private $_arr_redis_types = Array(
        Redis::REDIS_STRING,
        Redis::REDIS_SET,
        Redis::REDIS_LIST,  
        Redis::REDIS_ZSET,
        Redis::REDIS_HASH
    );

    private $_arr_failover_types = Array(
        RedisCluster::FAILOVER_NONE,
        RedisCluster::FAILOVER_ERROR,
        RedisCluster::FAILOVER_DISTRIBUTE
    );

    /**
     * @var string
     */
    protected $sessionPrefix = 'PHPREDIS_CLUSTER_SESSION:';

    /**
     * @var string
     */
    protected $sessionSaveHandler = 'rediscluster';

    /* Tests we'll skip all together in the context of RedisCluster.  The 
     * RedisCluster class doesn't implement specialized (non-redis) commands
     * such as sortAsc, or sortDesc and other commands such as SELECT are
     * simply invalid in Redis Cluster */
    public function testSortAsc()  { return $this->markTestSkipped(); }
    public function testSortDesc() { return $this->markTestSkipped(); }
    public function testWait()     { return $this->markTestSkipped(); }
    public function testSelect()   { return $this->markTestSkipped(); }
    public function testReconnectSelect() { return $this->markTestSkipped(); }
    public function testMultipleConnect() { return $this->markTestSkipped(); }
    public function testDoublePipeNoOp() { return $this->markTestSkipped(); }
    public function testSwapDB() { return $this->markTestSkipped(); }
    public function testConnectException() { return $this->markTestSkipped(); }

    /* Session locking feature is currently not supported in in context of Redis Cluster.
       The biggest issue for this is the distribution nature of Redis cluster */
    public function testSession_lockKeyCorrect() { return $this->markTestSkipped(); }
    public function testSession_lockingDisabledByDefault() { return $this->markTestSkipped(); }
    public function testSession_lockReleasedOnClose() { return $this->markTestSkipped(); }
    public function testSession_ttlMaxExecutionTime() { return $this->markTestSkipped(); }
    public function testSession_ttlLockExpire() { return $this->markTestSkipped(); }
    public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock() { return $this->markTestSkipped(); }
    public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock() { return $this->markTestSkipped(); }
    public function testSession_correctLockRetryCount() { return $this->markTestSkipped(); }
    public function testSession_defaultLockRetryCount() { return $this->markTestSkipped(); }
    public function testSession_noUnlockOfOtherProcess() { return $this->markTestSkipped(); }
    public function testSession_lockWaitTime() { return $this->markTestSkipped(); }

    /* Load our seeds on construction */
    public function __construct() {
        $str_nodemap_file = dirname($_SERVER['PHP_SELF']) . '/nodes/nodemap';

        if (!file_exists($str_nodemap_file)) {
            fprintf(STDERR, "Error:  Can't find nodemap file for seeds!\n");
            exit(1);
        }

        /* Store our node map */
        if (!self::$_arr_node_map) {
            self::$_arr_node_map = array_filter(
                explode("\n", file_get_contents($str_nodemap_file)
            ));
        }
    }

    /* Override setUp to get info from a specific node */
    public function setUp() {
        $this->redis = $this->newInstance();
        $info = $this->redis->info(uniqid());
        $this->version = (isset($info['redis_version'])?$info['redis_version']:'0.0.0');
    }

    /* Override newInstance as we want a RedisCluster object */
    protected function newInstance() {
        return new RedisCluster(NULL, self::$_arr_node_map);
    }

    /* Overrides for RedisTest where the function signature is different.  This
     * is only true for a few commands, which by definition have to be directed
     * at a specific node */

    public function testPing() {
        for ($i = 0; $i < 100; $i++) {
            $this->assertTrue($this->redis->ping("key:$i"));
        }
    }

    public function testRandomKey() {
        /* Ensure some keys are present to test */
        for ($i = 0; $i < 1000; $i++) {
            if (rand(1, 2) == 1) {
                $this->redis->set("key:$i", "val:$i");
            }
        }

        for ($i = 0; $i < 1000; $i++) {
            $k = $this->redis->randomKey("key:$i");
            $this->assertTrue($this->redis->exists($k));
        }
    }

    public function testEcho() {
        $this->assertEquals($this->redis->echo('k1', 'hello'), 'hello');
        $this->assertEquals($this->redis->echo('k2', 'world'), 'world');
        $this->assertEquals($this->redis->echo('k3', " 0123 "), " 0123 ");
    }

    public function testSortPrefix() {
        $this->redis->setOption(Redis::OPT_PREFIX, 'some-prefix:');
        $this->redis->del('some-item');
        $this->redis->sadd('some-item', 1);
        $this->redis->sadd('some-item', 2);
        $this->redis->sadd('some-item', 3);

        $this->assertEquals(array('1','2','3'), $this->redis->sort('some-item'));

        // Kill our set/prefix
        $this->redis->del('some-item');
        $this->redis->setOption(Redis::OPT_PREFIX, '');
    }

    public function testDBSize() {
        for ($i = 0; $i < 10; $i++) {
            $str_key = "key:$i";
            $this->assertTrue($this->redis->flushdb($str_key));
            $this->redis->set($str_key, "val:$i");
            $this->assertEquals(1, $this->redis->dbsize($str_key));
        }
    }

    public function testInfo() {
        $arr_check_keys = Array(
            "redis_version", "arch_bits", "uptime_in_seconds", "uptime_in_days",
            "connected_clients", "connected_slaves", "used_memory",
            "total_connections_received", "total_commands_processed",
            "role"
        );

        for ($i = 0; $i < 3; $i++) {
            $arr_info = $this->redis->info("k:$i");
            foreach ($arr_check_keys as $str_check_key) {
                $this->assertTrue(isset($arr_info[$str_check_key]));
            }
        }
    }

    public function testClient() {
        $str_key = 'key-' . rand(1,100);

        $this->assertTrue($this->redis->client($str_key, 'setname', 'cluster_tests'));

        $arr_clients = $this->redis->client($str_key, 'list');
        $this->assertTrue(is_array($arr_clients));

        /* Find us in the list */
        $str_addr = NULL;
        foreach ($arr_clients as $arr_client) {
            if ($arr_client['name'] == 'cluster_tests') {
                $str_addr = $arr_client['addr'];
                break;
            }
        }

        /* We should be in there */
        $this->assertFalse(empty($str_addr));

        /* Kill our own client! */
        $this->assertTrue($this->redis->client($str_key, 'kill', $str_addr));
    }

    public function testTime() {
        $time_arr = $this->redis->time("k:" . rand(1,100));
        $this->assertTrue(is_array($time_arr) && count($time_arr) == 2 &&
                          strval(intval($time_arr[0])) === strval($time_arr[0]) &&
                          strval(intval($time_arr[1])) === strval($time_arr[1]));
    }

    public function testScan() {
        $i_key_count = 0;
        $i_scan_count = 0;

        /* Have scan retry for us */
        $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

        /* Iterate over our masters, scanning each one */
        foreach ($this->redis->_masters() as $arr_master) {
            /* Grab the number of keys we have */
            $i_key_count += $this->redis->dbsize($arr_master);

            /* Scan the keys here */
            $it = NULL;
            while ($arr_keys = $this->redis->scan($it, $arr_master)) {
                $i_scan_count += count($arr_keys);
            }
        }

        /* Our total key count should match */
        $this->assertEquals($i_scan_count, $i_key_count);
    }

    // Run some simple tests against the PUBSUB command.  This is problematic, as we
    // can't be sure what's going on in the instance, but we can do some things.
    public function testPubSub() {
        // PUBSUB CHANNELS ...
        $result = $this->redis->pubsub("somekey", "channels", "*");
        $this->assertTrue(is_array($result));
        $result = $this->redis->pubsub("somekey", "channels");
        $this->assertTrue(is_array($result));

        // PUBSUB NUMSUB

        $c1 = '{pubsub}-' . rand(1,100);
        $c2 = '{pubsub}-' . rand(1,100);

        $result = $this->redis->pubsub("{pubsub}", "numsub", $c1, $c2);

        // Should get an array back, with two elements
        $this->assertTrue(is_array($result));
        $this->assertEquals(count($result), 4);

        $arr_zipped = Array();
        for ($i = 0; $i <= count($result) / 2; $i+=2) {
            $arr_zipped[$result[$i]] = $result[$i+1];
        }
        $result = $arr_zipped;

        // Make sure the elements are correct, and have zero counts
        foreach(Array($c1,$c2) as $channel) {
            $this->assertTrue(isset($result[$channel]));
            $this->assertEquals($result[$channel], 0);
        }

        // PUBSUB NUMPAT
        $result = $this->redis->pubsub("somekey", "numpat");
        $this->assertTrue(is_int($result));

        // Invalid call
        $this->assertFalse($this->redis->pubsub("somekey", "notacommand"));
    }

    /* Unlike Redis proper, MsetNX won't always totally fail if all keys can't
     * be set, but rather will only fail per-node when that is the case */
    public function testMSetNX() {
        /* All of these keys should get set */
        $this->redis->del('x','y','z');
        $ret = $this->redis->msetnx(Array('x'=>'a','y'=>'b','z'=>'c'));
        $this->assertTrue(is_array($ret));
        $this->assertEquals(array_sum($ret),count($ret));

        /* Delete one key */
        $this->redis->del('x');
        $ret = $this->redis->msetnx(Array('x'=>'a','y'=>'b','z'=>'c'));
        $this->assertTrue(is_array($ret));
        $this->assertEquals(array_sum($ret),1);
        
        $this->assertFalse($this->redis->msetnx(array())); // set ø → FALSE
    }

    /* Slowlog needs to take a key or Array(ip, port), to direct it to a node */
    public function testSlowlog() {
        $str_key = uniqid() . '-' . rand(1, 1000);

        $this->assertTrue(is_array($this->redis->slowlog($str_key, 'get')));
        $this->assertTrue(is_array($this->redis->slowlog($str_key, 'get', 10)));
        $this->assertTrue(is_int($this->redis->slowlog($str_key, 'len')));
        $this->assertTrue($this->redis->slowlog($str_key, 'reset'));
        $this->assertFalse($this->redis->slowlog($str_key, 'notvalid'));
    }

    /* INFO COMMANDSTATS requires a key or ip:port for node direction */
    public function testInfoCommandStats() {
        $str_key = uniqid() . '-' . rand(1,1000);
        $arr_info = $this->redis->info($str_key, "COMMANDSTATS");

        $this->assertTrue(is_array($arr_info));
        if (is_array($arr_info)) {
            foreach($arr_info as $k => $str_value) {
                $this->assertTrue(strpos($k, 'cmdstat_') !== false);
            }
        }
    }

    /* RedisCluster will always respond with an array, even if transactions
     * failed, because the commands could be coming from multiple nodes */
    public function testFailedTransactions() {
        $this->redis->set('x', 42);

        // failed transaction
        $this->redis->watch('x');

        $r = $this->newInstance(); // new instance, modifying `x'.
        $r->incr('x');

        // This transaction should fail because the other client changed 'x'
        $ret = $this->redis->multi()->get('x')->exec();
        $this->assertTrue($ret === Array(FALSE));
        // watch and unwatch
        $this->redis->watch('x');
        $r->incr('x'); // other instance
        $this->redis->unwatch('x'); // cancel transaction watch

        // This should succeed as the watch has been cancelled
        $ret = $this->redis->multi()->get('x')->exec();
        $this->assertTrue($ret === array('44'));
    }

    public function testDiscard()
    {
        /* start transaction */
        $this->redis->multi();

        /* Set and get in our transaction */
        $this->redis->set('pipecount','over9000')->get('pipecount');

        $this->assertTrue($this->redis->discard());
    }

    /* RedisCluster::script() is a 'raw' command, which requires a key such that
     * we can direct it to a given node */
    public function testScript() {
        $str_key = uniqid() . '-' . rand(1,1000);

        // Flush any scripts we have
        $this->assertTrue($this->redis->script($str_key, 'flush'));

        // Silly scripts to test against
        $s1_src = 'return 1';
        $s1_sha = sha1($s1_src);
        $s2_src = 'return 2';
        $s2_sha = sha1($s2_src);
        $s3_src = 'return 3';
        $s3_sha = sha1($s3_src);

        // None should exist
        $result = $this->redis->script($str_key, 'exists', $s1_sha, $s2_sha, $s3_sha);
        $this->assertTrue(is_array($result) && count($result) == 3);
        $this->assertTrue(is_array($result) && count(array_filter($result)) == 0);

        // Load them up
        $this->assertTrue($this->redis->script($str_key, 'load', $s1_src) == $s1_sha);
        $this->assertTrue($this->redis->script($str_key, 'load', $s2_src) == $s2_sha);
        $this->assertTrue($this->redis->script($str_key, 'load', $s3_src) == $s3_sha);

        // They should all exist
        $result = $this->redis->script($str_key, 'exists', $s1_sha, $s2_sha, $s3_sha);
        $this->assertTrue(is_array($result) && count(array_filter($result)) == 3);
    }

    /* RedisCluster::EVALSHA needs a 'key' to let us know which node we want to
     * direct the command at */
    public function testEvalSHA() {
        $str_key = uniqid() . '-' . rand(1,1000);

        // Flush any loaded scripts
        $this->redis->script($str_key, 'flush');

        // Non existant script (but proper sha1), and a random (not) sha1 string
        $this->assertFalse($this->redis->evalsha(sha1(uniqid()),Array($str_key), 1));
        $this->assertFalse($this->redis->evalsha('some-random-data'),Array($str_key), 1);

        // Load a script
        $cb  = uniqid(); // To ensure the script is new
        $scr = "local cb='$cb' return 1";
        $sha = sha1($scr);

        // Run it when it doesn't exist, run it with eval, and then run it with sha1
        $this->assertTrue(false === $this->redis->evalsha($scr,Array($str_key), 1));
        $this->assertTrue(1 === $this->redis->eval($scr,Array($str_key), 1));
        $this->assertTrue(1 === $this->redis->evalsha($sha,Array($str_key), 1));
    }
    
    public function testEvalBulkResponse() {
        $str_key1 = uniqid() . '-' . rand(1,1000) . '{hash}';
        $str_key2 = uniqid() . '-' . rand(1,1000) . '{hash}';

        $this->redis->script($str_key1, 'flush');
        $this->redis->script($str_key2, 'flush');

        $scr = "return {KEYS[1],KEYS[2]}";

        $result = $this->redis->eval($scr,Array($str_key1, $str_key2), 2);

        $this->assertTrue($str_key1 === $result[0]);
        $this->assertTrue($str_key2 === $result[1]);
    }

    public function testEvalBulkResponseMulti() {
        $str_key1 = uniqid() . '-' . rand(1,1000) . '{hash}';
        $str_key2 = uniqid() . '-' . rand(1,1000) . '{hash}';

        $this->redis->script($str_key1, 'flush');
        $this->redis->script($str_key2, 'flush');

        $scr = "return {KEYS[1],KEYS[2]}";

        $this->redis->multi();
        $this->redis->eval($scr,Array($str_key1, $str_key2), 2);

        $result = $this->redis->exec();

        $this->assertTrue($str_key1 === $result[0][0]);
        $this->assertTrue($str_key2 === $result[0][1]);
    }

    public function testEvalBulkEmptyResponse() {
        $str_key1 = uniqid() . '-' . rand(1,1000) . '{hash}';
        $str_key2 = uniqid() . '-' . rand(1,1000) . '{hash}';

        $this->redis->script($str_key1, 'flush');
        $this->redis->script($str_key2, 'flush');

        $scr = "for _,key in ipairs(KEYS) do redis.call('SET', key, 'value') end";

        $result = $this->redis->eval($scr,Array($str_key1, $str_key2), 2);

        $this->assertTrue(null === $result);
    }

    public function testEvalBulkEmptyResponseMulti() {
        $str_key1 = uniqid() . '-' . rand(1,1000) . '{hash}';
        $str_key2 = uniqid() . '-' . rand(1,1000) . '{hash}';

        $this->redis->script($str_key1, 'flush');
        $this->redis->script($str_key2, 'flush');

        $scr = "for _,key in ipairs(KEYS) do redis.call('SET', key, 'value') end";

        $this->redis->multi();
        $this->redis->eval($scr,Array($str_key1, $str_key2), 2);
        $result = $this->redis->exec();

        $this->assertTrue(null === $result[0]);
    }

    /* Cluster specific introspection stuff */
    public function testIntrospection() {
        $arr_masters = $this->redis->_masters();
        $this->assertTrue(is_array($arr_masters));
        
        foreach ($arr_masters as $arr_info) {
            $this->assertTrue(is_array($arr_info));
            $this->assertTrue(is_string($arr_info[0]));
            $this->assertTrue(is_long($arr_info[1]));
        }
    }

    protected function genKeyName($i_key_idx, $i_type) {
        switch ($i_type) { 
            case Redis::REDIS_STRING: 
                return "string-$i_key_idx";
            case Redis::REDIS_SET: 
                return "set-$i_key_idx";
            case Redis::REDIS_LIST:
                return "list-$i_key_idx";
            case Redis::REDIS_ZSET:
                return "zset-$i_key_idx";
            case Redis::REDIS_HASH:
                return "hash-$i_key_idx";
            default:
                return "unknown-$i_key_idx";
        }
    }

    protected function setKeyVals($i_key_idx, $i_type, &$arr_ref) {
        $str_key = $this->genKeyName($i_key_idx, $i_type);

        $this->redis->del($str_key);

        switch ($i_type) {
            case Redis::REDIS_STRING:
                $value = "$str_key-value";
                $this->redis->set($str_key, $value); 
                break;
            case Redis::REDIS_SET:
                $value = Array(
                    $str_key . '-mem1', $str_key . '-mem2', $str_key . '-mem3',
                    $str_key . '-mem4', $str_key . '-mem5', $str_key . '-mem6'
                );
                $arr_args = $value;
                array_unshift($arr_args, $str_key);
                call_user_func_array(Array($this->redis, 'sadd'), $arr_args);
                break;
            case Redis::REDIS_HASH:
                $value = Array(
                    $str_key . '-mem1' => $str_key . '-val1',
                    $str_key . '-mem2' => $str_key . '-val2',
                    $str_key . '-mem3' => $str_key . '-val3'
                );
                $this->redis->hmset($str_key, $value);
                break; 
            case Redis::REDIS_LIST:
                $value = Array(
                    $str_key . '-ele1', $str_key . '-ele2', $str_key . '-ele3',
                    $str_key . '-ele4', $str_key . '-ele5', $str_key . '-ele6'
                );
                $arr_args = $value;
                array_unshift($arr_args, $str_key);
                call_user_func_array(Array($this->redis, 'rpush'), $arr_args);
                break;
            case Redis::REDIS_ZSET:
                $i_score = 1;
                $value = Array(
                    $str_key . '-mem1' => 1, $str_key . '-mem2' => 2,
                    $str_key . '-mem3' => 3, $str_key . '-mem3' => 3
                );
                foreach ($value as $str_mem => $i_score) {
                    $this->redis->zadd($str_key, $i_score, $str_mem);
                }
                break; 
        }

        /* Update our reference array so we can verify values */
        $arr_ref[$str_key] = $value;
        return $str_key;
    }

    /* Verify that our ZSET values are identical */
    protected function checkZSetEquality($a, $b) {
        /* If the count is off, the array keys are different or the sums are
         * different, we know there is something off */
        $boo_diff = count($a) != count($b) ||
            count(array_diff(array_keys($a), array_keys($b))) != 0 ||
            array_sum($a) != array_sum($b);

        if ($boo_diff) {
            $this->assertEquals($a,$b);
            return;
        }
    }

    protected function checkKeyValue($str_key, $i_type, $value) {
        switch ($i_type) {
            case Redis::REDIS_STRING:
                $this->assertEquals($value, $this->redis->get($str_key));
                break;
            case Redis::REDIS_SET:
                $arr_r_values = $this->redis->sMembers($str_key);
                $arr_l_values = $value;
                sort($arr_r_values);
                sort($arr_l_values);
                $this->assertEquals($arr_r_values, $arr_l_values);
                break;
            case Redis::REDIS_LIST:
                $this->assertEquals($value, $this->redis->lrange($str_key,0,-1));
                break;
            case Redis::REDIS_HASH:
                $this->assertEquals($value, $this->redis->hgetall($str_key));
                break;
            case Redis::REDIS_ZSET:
                $this->checkZSetEquality($value, $this->redis->zrange($str_key,0,-1,true));
                break;
            default:
                throw new Exception("Unknown type " . $i_type);
        }
    }

    /* Test automatic load distributor */
    public function testFailOver() {
        $arr_value_ref = Array();
        $arr_type_ref  = Array();

        /* Set a bunch of keys of various redis types*/
        for ($i = 0; $i < 200; $i++) {
            foreach ($this->_arr_redis_types as $i_type) {
                $str_key = $this->setKeyVals($i, $i_type, $arr_value_ref);
                $arr_type_ref[$str_key] = $i_type;                
            }
        }

        /* Iterate over failover options */
        foreach ($this->_arr_failover_types as $i_opt) {
            $this->redis->setOption(RedisCluster::OPT_SLAVE_FAILOVER, $i_opt);

            foreach ($arr_value_ref as $str_key => $value) {
                $this->checkKeyValue($str_key, $arr_type_ref[$str_key], $value);
            }

            break;
        }    
    }

    /* Test a 'raw' command */
    public function testRawCommand() {
        $this->redis->rawCommand('mykey', 'set', 'mykey', 'my-value');
        $this->assertEquals($this->redis->get('mykey'), 'my-value');

        $this->redis->del('mylist');
        $this->redis->rpush('mylist', 'A','B','C','D');
        $this->assertEquals($this->redis->lrange('mylist', 0, -1), Array('A','B','C','D'));
    }

    protected function rawCommandArray($key, $args) {
        array_unshift($args, $key);
        return call_user_func_array(Array($this->redis, 'rawCommand'), $args);
    }

    public function testSession()
    {
        @ini_set('session.save_handler', 'rediscluster');
        @ini_set('session.save_path', implode('&', array_map(function ($seed) {
            return 'seed[]=' . $seed;
        }, self::$_arr_node_map)) . '&failover=error');
        if (!@session_start()) {
            return $this->markTestSkipped();
        }
        session_write_close();
        $this->assertTrue($this->redis->exists('PHPREDIS_CLUSTER_SESSION:' . session_id()));
    }

    /**
     * @inheritdoc
     */
    protected function getFullHostPath()
    {
        $hosts = array_map(function ($host) {
            return 'seed[]=' . $host . '';
        }, self::$_arr_node_map);

        return implode('&', $hosts);
    }
}
?>
