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

define('REDIS_ARRAY_DATA_SIZE', 1000);

function custom_hash($str) {
    // str has the following format: $APPID_fb$FACEBOOKID_$key.
    $pos = strpos($str, '_fb');
    if(preg_match("#\w+_fb(?<facebook_id>\d+)_\w+#", $str, $out)) {
            return $out['facebook_id'];
    }
    return $str;
}

function parseHostPort($str, &$host, &$port) {
    $pos = strrpos($str, ':');
    $host = substr($str, 0, $pos);
    $port = substr($str, $pos+1);
}

function getRedisVersion($obj_r) {
    $arr_info = $obj_r->info();
    if (!$arr_info || !isset($arr_info['redis_version'])) {
        return "0.0.0";
    }
    return $arr_info['redis_version'];
}

/* Determine the lowest redis version attached to this RedisArray object */
function getMinVersion($obj_ra) {
    $min_version = "0.0.0";
    foreach ($obj_ra->_hosts() as $host) {
        $version = getRedisVersion($obj_ra->_instance($host));
        if (version_compare($version, $min_version) > 0) {
            $min_version = $version;
        }
    }

    return $min_version;
}

class Redis_Array_Test extends TestSuite
{
    private $min_version;
    private $strings;
    public $ra = NULL;
    private $data = NULL;

    public function setUp() {
        // initialize strings.
        $n = REDIS_ARRAY_DATA_SIZE;
        $this->strings = array();
        for($i = 0; $i < $n; $i++) {
            $this->strings['key-'.$i] = 'val-'.$i;
        }

        global $newRing, $oldRing, $useIndex;
        $this->ra = new RedisArray($newRing, array('previous' => $oldRing, 'index' => $useIndex));
        $this->min_version = getMinVersion($this->ra);
    }

    public function testMSet() {
        // run mset
        $this->assertTrue(TRUE === $this->ra->mset($this->strings));

        // check each key individually using the array
        foreach($this->strings as $k => $v) {
            $this->assertTrue($v === $this->ra->get($k));
        }

        // check each key individually using a new connection
        foreach($this->strings as $k => $v) {
            parseHostPort($this->ra->_target($k), $host, $port);

            $target = $this->ra->_target($k);
            $pos = strrpos($target, ':');

            $host = substr($target, 0, $pos);
            $port = substr($target, $pos+1);

            $r = new Redis;
            $r->pconnect($host, (int)$port);
            $this->assertTrue($v === $r->get($k));
        }
    }

    public function testMGet() {
        $this->assertTrue(array_values($this->strings) === $this->ra->mget(array_keys($this->strings)));
    }

    private function addData($commonString) {
        $this->data = array();
        for($i = 0; $i < REDIS_ARRAY_DATA_SIZE; $i++) {
            $k = rand().'_'.$commonString.'_'.rand();
            $this->data[$k] = rand();
        }
        $this->ra->mset($this->data);
    }

    private function checkCommonLocality() {
        // check that they're all on the same node.
        $lastNode = NULL;
        foreach($this->data as $k => $v) {
                $node = $this->ra->_target($k);
                if($lastNode) {
                    $this->assertTrue($node === $lastNode);
                }
                $this->assertTrue($this->ra->get($k) == $v);
                $lastNode = $node;
        }
    }

    public function testKeyLocality() {

        // basic key locality with default hash
        $this->addData('{hashed part of the key}');
        $this->checkCommonLocality();

        // with common hashing function
        global $newRing, $oldRing, $useIndex;
        $this->ra = new RedisArray($newRing, array('previous' => $oldRing,
                'index' => $useIndex,
                'function' => 'custom_hash'));

        // basic key locality with custom hash
        $this->addData('fb'.rand());
        $this->checkCommonLocality();
    }

    public function customDistributor($key)
    {
        $a = unpack("N*", md5($key, true));
        global $newRing;
        $pos = abs($a[1]) % count($newRing);

        return $pos;
    }

    public function testKeyDistributor()
    {
        global $newRing, $useIndex;
        $this->ra = new RedisArray($newRing, array(
                'index' => $useIndex,
                'function' => 'custom_hash',
                'distributor' => array($this, "customDistributor")));

        // custom key distribution function.
        $this->addData('fb'.rand());

        // check that they're all on the expected node.
        $lastNode = NULL;
        foreach($this->data as $k => $v) {
            $node = $this->ra->_target($k);
            $pos = $this->customDistributor($k);
            $this->assertTrue($node === $newRing[$pos]);
        }
    }

}

class Redis_Rehashing_Test extends TestSuite
{

    public $ra = NULL;
    private $useIndex;

    private $min_version;

    // data
    private $strings;
    private $sets;
    private $lists;
    private $hashes;
    private $zsets;

    public function setUp() {

        // initialize strings.
        $n = REDIS_ARRAY_DATA_SIZE;
        $this->strings = array();
        for($i = 0; $i < $n; $i++) {
            $this->strings['key-'.$i] = 'val-'.$i;
        }

        // initialize sets
        for($i = 0; $i < $n; $i++) {
            // each set has 20 elements
            $this->sets['set-'.$i] = range($i, $i+20);
        }

        // initialize lists
        for($i = 0; $i < $n; $i++) {
            // each list has 20 elements
            $this->lists['list-'.$i] = range($i, $i+20);
        }

        // initialize hashes
        for($i = 0; $i < $n; $i++) {
            // each hash has 5 keys
            $this->hashes['hash-'.$i] = array('A' => $i, 'B' => $i+1, 'C' => $i+2, 'D' => $i+3, 'E' => $i+4);
        }

        // initialize sorted sets
        for($i = 0; $i < $n; $i++) {
            // each sorted sets has 5 elements
            $this->zsets['zset-'.$i] = array($i, 'A', $i+1, 'B', $i+2, 'C', $i+3, 'D', $i+4, 'E');
        }

        global $newRing, $oldRing, $useIndex;

        // create array
        $this->ra = new RedisArray($newRing, array('previous' => $oldRing, 'index' => $useIndex));
        $this->min_version = getMinVersion($this->ra);
    }

    public function testFlush() {
        // flush all servers first.
        global $serverList;
        foreach($serverList as $s) {
            parseHostPort($s, $host, $port);

            $r = new Redis();
            $r->pconnect($host, (int)$port, 0);
            $r->flushdb();
        }
    }


    private function distributeKeys() {

        // strings
        foreach($this->strings as $k => $v) {
            $this->ra->set($k, $v);
        }

        // sets
        foreach($this->sets as $k => $v) {
            call_user_func_array(array($this->ra, 'sadd'), array_merge(array($k), $v));
        }

        // lists
        foreach($this->lists as $k => $v) {
            call_user_func_array(array($this->ra, 'rpush'), array_merge(array($k), $v));
        }

        // hashes
        foreach($this->hashes as $k => $v) {
            $this->ra->hmset($k, $v);
        }

        // sorted sets
        foreach($this->zsets as $k => $v) {
            call_user_func_array(array($this->ra, 'zadd'), array_merge(array($k), $v));
        }
    }

    public function testDistribution() {
        $this->distributeKeys();
    }

    public function testSimpleRead() {
        $this->readAllvalues();
    }

    private function readAllvalues() {

        // strings
        foreach($this->strings as $k => $v) {
            $this->assertTrue($this->ra->get($k) === $v);
        }

        // sets
        foreach($this->sets as $k => $v) {
            $ret = $this->ra->smembers($k); // get values

            // sort sets
            sort($v);
            sort($ret);

            $this->assertTrue($ret == $v);
        }

        // lists
        foreach($this->lists as $k => $v) {
            $ret = $this->ra->lrange($k, 0, -1);
            $this->assertTrue($ret == $v);
        }

        // hashes
        foreach($this->hashes as $k => $v) {
            $ret = $this->ra->hgetall($k); // get values
            $this->assertTrue($ret == $v);
        }

        // sorted sets
        foreach($this->zsets as $k => $v) {
            $ret = $this->ra->zrange($k, 0, -1, TRUE); // get values with scores

            // create assoc array from local dataset
            $tmp = array();
            for($i = 0; $i < count($v); $i += 2) {
                $tmp[$v[$i+1]] = $v[$i];
            }

            // compare to RA value
            $this->assertTrue($ret == $tmp);
        }
    }

    // add a new node.
    public function testCreateSecondRing() {

        global $newRing, $oldRing, $serverList;
        $oldRing = $newRing; // back up the original.
        $newRing = $serverList; // add a new node to the main ring.
    }

    public function testReadUsingFallbackMechanism() {
        $this->readAllvalues(); // some of the reads will fail and will go to another target node.
    }

    public function testRehash() {
        $this->ra->_rehash(); // this will redistribute the keys
    }

    public function testRehashWithCallback() {
        $total = 0;
        $this->ra->_rehash(function ($host, $count) use (&$total) {
            $total += $count;
        });
        $this->assertTrue($total > 0);
    }

    public function testReadRedistributedKeys() {
        $this->readAllvalues(); // we shouldn't have any missed reads now.
    }
}

// Test auto-migration of keys
class Redis_Auto_Rehashing_Test extends TestSuite {

    public $ra = NULL;
    private $min_version;

    // data
    private $strings;

    public function setUp() {
        // initialize strings.
        $n = REDIS_ARRAY_DATA_SIZE;
        $this->strings = array();
        for($i = 0; $i < $n; $i++) {
            $this->strings['key-'.$i] = 'val-'.$i;
        }

        global $newRing, $oldRing, $useIndex;

        // create array
        $this->ra = new RedisArray($newRing, array('previous' => $oldRing, 'index' => $useIndex, 'autorehash' => TRUE));
        $this->min_version = getMinVersion($this->ra);
    }

    public function testDistribute() {
        // strings
        foreach($this->strings as $k => $v) {
            $this->ra->set($k, $v);
        }
    }

    private function readAllvalues() {
        foreach($this->strings as $k => $v) {
            $this->assertTrue($this->ra->get($k) === $v);
        }
    }


    public function testReadAll() {
        $this->readAllvalues();
    }

    // add a new node.
    public function testCreateSecondRing() {
        global $newRing, $oldRing, $serverList;
        $oldRing = $newRing; // back up the original.
        $newRing = $serverList; // add a new node to the main ring.
    }

    // Read and migrate keys on fallback, causing the whole ring to be rehashed.
    public function testReadAndMigrateAll() {
        $this->readAllvalues();
    }

    // Read and migrate keys on fallback, causing the whole ring to be rehashed.
    public function testAllKeysHaveBeenMigrated() {
        foreach($this->strings as $k => $v) {
            parseHostPort($this->ra->_target($k), $host, $port);

            $r = new Redis;
            $r->pconnect($host, $port);

            $this->assertTrue($v === $r->get($k));  // check that the key has actually been migrated to the new node.
        }
    }
}

// Test node-specific multi/exec
class Redis_Multi_Exec_Test extends TestSuite {
    public $ra = NULL;
    private $min_version;

    public function setUp() {
        global $newRing, $oldRing, $useIndex;

        // create array
        $this->ra = new RedisArray($newRing, array('previous' => $oldRing, 'index' => $useIndex));
        $this->min_version = getMinVersion($this->ra);
    }

    public function testInit() {
        $this->ra->set('{groups}:managers', 2);
        $this->ra->set('{groups}:executives', 3);

        $this->ra->set('1_{employee:joe}_name', 'joe');
        $this->ra->set('1_{employee:joe}_group', 2);
        $this->ra->set('1_{employee:joe}_salary', 2000);
    }

    public function testKeyDistribution() {
        // check that all of joe's keys are on the same instance
        $lastNode = NULL;
        foreach(array('name', 'group', 'salary') as $field) {
            $node = $this->ra->_target('1_{employee:joe}_'.$field);
            if($lastNode) {
                $this->assertTrue($node === $lastNode);
            }
            $lastNode = $node;
        }
    }

    public function testMultiExec() {

        // Joe gets a promotion
        $newGroup = $this->ra->get('{groups}:executives');
        $newSalary = 4000;

        // change both in a transaction.
        $host = $this->ra->_target('{employee:joe}');   // transactions are per-node, so we need a reference to it.
        $tr = $this->ra->multi($host)
            ->set('1_{employee:joe}_group', $newGroup)
            ->set('1_{employee:joe}_salary', $newSalary)
            ->exec();

        // check that the group and salary have been changed
        $this->assertTrue($this->ra->get('1_{employee:joe}_group') === $newGroup);
        $this->assertTrue($this->ra->get('1_{employee:joe}_salary') == $newSalary);

    }

    public function testMultiExecMSet() {

        global $newGroup, $newSalary;
        $newGroup = 1;
        $newSalary = 10000;

        // test MSET, making Joe a top-level executive
        $out = $this->ra->multi($this->ra->_target('{employee:joe}'))
                ->mset(array('1_{employee:joe}_group' => $newGroup, '1_{employee:joe}_salary' => $newSalary))
                ->exec();

        $this->assertTrue($out[0] === TRUE);
    }

    public function testMultiExecMGet() {

        global $newGroup, $newSalary;

        // test MGET
        $out = $this->ra->multi($this->ra->_target('{employee:joe}'))
                ->mget(array('1_{employee:joe}_group', '1_{employee:joe}_salary'))
                ->exec();

        $this->assertTrue($out[0][0] == $newGroup);
        $this->assertTrue($out[0][1] == $newSalary);
    }

    public function testMultiExecDel() {

        // test DEL
        $out = $this->ra->multi($this->ra->_target('{employee:joe}'))
            ->del('1_{employee:joe}_group', '1_{employee:joe}_salary')
            ->exec();

        $this->assertTrue($out[0] === 2);
        $this->assertEquals(0, $this->ra->exists('1_{employee:joe}_group'));
        $this->assertEquals(0, $this->ra->exists('1_{employee:joe}_salary'));
    }

    public function testMutliExecUnlink() {
        if (version_compare($this->min_version, "4.0.0", "lt")) {
            $this->markTestSkipped();
        }

        $this->ra->set('{unlink}:key1', 'bar');
        $this->ra->set('{unlink}:key2', 'bar');

        $out = $this->ra->multi($this->ra->_target('{unlink}'))
            ->del('{unlink}:key1', '{unlink}:key2')
            ->exec();

        $this->assertTrue($out[0] === 2);
    }

    public function testDiscard() {
        /* phpredis issue #87 */
        $key = 'test_err';

        $this->assertTrue($this->ra->set($key, 'test'));
        $this->assertTrue('test' === $this->ra->get($key));

        $this->ra->watch($key);

        // After watch, same
        $this->assertTrue('test' === $this->ra->get($key));

        // change in a multi/exec block.
        $ret = $this->ra->multi($this->ra->_target($key))->set($key, 'test1')->exec();
        $this->assertTrue($ret === array(true));

        // Get after exec, 'test1':
        $this->assertTrue($this->ra->get($key) === 'test1');

        $this->ra->watch($key);

        // After second watch, still test1.
        $this->assertTrue($this->ra->get($key) === 'test1');

        $ret = $this->ra->multi($this->ra->_target($key))->set($key, 'test2')->discard();
        // Ret after discard: NULL";
        $this->assertTrue($ret === NULL);

        // Get after discard, unchanged:
        $this->assertTrue($this->ra->get($key) === 'test1');
    }

}

// Test custom distribution function
class Redis_Distributor_Test extends TestSuite {

    public $ra = NULL;
    private $min_version;

    public function setUp() {
        global $newRing, $oldRing, $useIndex;
        // create array
        $this->ra = new RedisArray($newRing, array('previous' => $oldRing, 'index' => $useIndex, 'distributor' => array($this, 'distribute')));
        $this->min_version = getMinVersion($this->ra);
    }

    public function testInit() {
        $this->ra->set('{uk}test', 'joe');
        $this->ra->set('{us}test', 'bob');
    }

    public function distribute($key) {
        $matches = array();
        if (preg_match('/{([^}]+)}.*/', $key, $matches) == 1) {
            $countries = array('uk' => 0, 'us' => 1);
            if (array_key_exists($matches[1], $countries)) {
                return $countries[$matches[1]];
            }
        }
        return 2; // default server
    }

    public function testDistribution() {
        $ukServer = $this->ra->_target('{uk}test');
        $usServer = $this->ra->_target('{us}test');
        $deServer = $this->ra->_target('{de}test');
        $defaultServer = $this->ra->_target('unknown');

        $nodes = $this->ra->_hosts();
        $this->assertTrue($ukServer === $nodes[0]);
        $this->assertTrue($usServer === $nodes[1]);
        $this->assertTrue($deServer === $nodes[2]);
        $this->assertTrue($defaultServer === $nodes[2]);
    }
}

function run_tests($className, $str_filter, $str_host) {
        // reset rings
        global $newRing, $oldRing, $serverList;

        $newRing = ["$str_host:6379", "$str_host:6380", "$str_host:6381"];
        $oldRing = [];
        $serverList = ["$str_host:6379", "$str_host:6380", "$str_host:6381", "$str_host:6382"];

        // run
        return TestSuite::run($className, $str_filter);
}

?>
