<?php

/*
 * @copyright NetMonsters <team@netmonsters.ru>
 * @link http://netmonsters.ru
 * @package Majestic
 * @subpackage UnitTests
 * @since 2011-11-7
 *
 * Unit tests for MongoModel class
 */

require_once dirname(__FILE__) . '/../../Registry.php';
require_once dirname(__FILE__) . '/../../Config.php';
require_once dirname(__FILE__) . '/../../cache/Cacher.php';
require_once dirname(__FILE__) . '/../../exception/GeneralException.php';
require_once dirname(__FILE__) . '/../../model/DbExpr.php';
require_once dirname(__FILE__) . '/../../model/Db.php';
require_once dirname(__FILE__) . '/../../model/MongoDbCommand.php';
require_once dirname(__FILE__) . '/../../model/DbStatement.php';
require_once dirname(__FILE__) . '/../../model/MongoStatement.php';
require_once dirname(__FILE__) . '/../../model/DbDriver.php';
require_once dirname(__FILE__) . '/../../model/NoSqlDbDriver.php';
require_once dirname(__FILE__) . '/../../model/MongoDriver.php';
require_once dirname(__FILE__) . '/../../model/Model.php';
require_once dirname(__FILE__) . '/../../model/MongoModel.php';

class MongoModelTest extends PHPUnit_Framework_TestCase
{

    /**
     * @var MongoModel
     */
    private $model;

    /**
     * @var ReflectionMethod
     */
    private $method_count;

    /**
     * @var ReflectionMethod
     */
    private $method_fetch;

    public function run(PHPUnit_Framework_TestResult $result = NULL)
    {
        $this->setPreserveGlobalState(false);
        return parent::run($result);
    }

    public function setUp()
    {
        $conf = array('default' => array('driver' => 'MongoDriver', 'hostname' => 'localhost', 'database' => 'test', 'username' => 'test', 'password' => '1234', 'port' => 27017));

        $this->dbSetUp($conf);

        Config::set('Db', $conf);
        if (!class_exists('MockModel')) {
            $this->model = $this->getMockForAbstractClass('MongoModel', array(), 'MongoMockModel');
        } else {
            $this->model = new MongoMockModel();
        }

        $model = new ReflectionClass('MongoModel');
        $this->method_count = $model->getMethod('count');
        $this->method_count->setAccessible(true);
        $this->method_fetch = $model->getMethod('fetch');
        $this->method_fetch->setAccessible(true);

        set_new_overload(array($this, 'newCallback'));
    }

    /**
     * @group Mongo
     */
    public function testModel()
    {
        $this->assertInstanceOf('MongoMockModel', $this->model);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testFetch()
    {
        Config::set('DEBUG', false);

        $mock = $this->getMock('CacheKey', array('set', 'get'));
        $mock->expects($this->once())
                ->method('set')
                ->will($this->returnValue(true));
        $mock->expects($this->once())
                ->method('get')
                ->will($this->returnValue(false));

        $result = $this->method_fetch->invoke($this->model, array(), array('order' => array('name' => -1), 'limit' => 2), $mock);
        $this->assertInstanceOf('ArrayObject', $result);
        $this->assertEquals('milk', $result->name);
        $result = $this->method_fetch->invoke($this->model, array(), array('fields' => array('name')));
        $this->assertSame('bread', $result->name);
        $result = $this->method_fetch->invoke($this->model, array());
        $this->setExpectedException('PHPUnit_Framework_Error');
        $this->assertNull($result->pounds);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testFetchField()
    {
        Config::set('DEBUG', false);

        $mock = $this->getMock('CacheKey', array('set', 'get'));
        $mock->expects($this->exactly(2))
                ->method('set')
                ->will($this->returnValue(true));
        $mock->expects($this->exactly(2))
                ->method('get')
                ->will($this->returnValue(false));

        $model = new ReflectionClass('MongoModel');
        $method = $model->getMethod('fetchField');
        $method->setAccessible(true);

        $result = $method->invoke($this->model, array('name' => 'milk'), array(), 'quantity', $mock);
        $this->assertEquals(1, $result);
        $result = $method->invoke($this->model, array(), array('skip' => 2), 'quantity', $mock);
        $this->assertEquals($result, $this->method_fetch->invoke($this->model, array(), array('skip' => 2))->quantity);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testFetchAll()
    {
        Config::set('DEBUG', false);

        $mock = $this->getMock('CacheKey', array('set', 'get'));
        $mock->expects($this->once())
                ->method('set')
                ->will($this->returnValue(true));
        $mock->expects($this->once())
                ->method('get')
                ->will($this->returnValue(false));

        $model = new ReflectionClass('MongoModel');
        $method = $model->getMethod('fetchAll');
        $method->setAccessible(true);

        $result = $method->invoke($this->model, array('name' => 'eggs'), array(), $mock);
        $this->assertEquals(2, count($result));
        $result = $method->invoke($this->model, array(), array('skip' => 2));
        $this->assertEquals(3, count($result));
        $this->assertEquals('fish', $result[0]->name);
        $this->assertEquals('milk', $result[1]->name);
        $this->assertEquals('eggs', $result[2]->name);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testFetchOrderParam()
    {
        Config::set('DEBUG', false);
        $result = $this->method_fetch->invoke($this->model, array(), array('order' => 'name'));
        $this->assertSame('bread', $result->name);
        $result = $this->method_fetch->invoke($this->model, array(), array('order' => array('name' => 1, 'quantity' => -1), 'skip' => 1));
        $this->assertSame(2.1, $result->price);
        $this->setExpectedException('GeneralException', 'Wrong order parameter given to query.');
        $this->method_fetch->invoke($this->model, array(), array('order' => 1));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testFetchSkipLimitParam()
    {
        Config::set('DEBUG', false);
        $result = $this->method_fetch->invoke($this->model, array(), array('order' => 'name'));
        $this->assertSame('bread', $result->name);
        $result = $this->method_fetch->invoke($this->model, array(), array('order' => array('name' => 1, 'quantity' => -1), 'skip' => 1, 'limit' => 1));
        $this->assertSame(2.1, $result->price);

        $model = new ReflectionClass('MongoModel');
        $method = $model->getMethod('fetchAll');
        $method->setAccessible(true);
        $this->assertCount(3, $method->invoke($this->model, array(), array('limit' => 3)));
        $this->assertCount(2, $method->invoke($this->model, array(), array('skip' => 3)));
        $this->assertCount(5, $method->invoke($this->model, array(), array()));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testFetchFieldsParam()
    {
        Config::set('DEBUG', false);
        $result = $this->method_fetch->invoke($this->model, array(), array('fields' => 'name'));
        $this->assertTrue(!isset($result->quantity));
        $result = $this->method_fetch->invoke($this->model, array(), array('fields' => array('name', 'price')));
        $this->assertTrue(!isset($result->quantity));
        $result = $this->method_fetch->invoke($this->model, array(), array('fields' => array('name' => 1, 'price' => 1)));
        $this->assertTrue(!isset($result->quantity));
        $this->setExpectedException('GeneralException', 'Wrong fields parameter given to query.');
        $this->method_fetch->invoke($this->model, array(), array('fields' => 1));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testGet()
    {
        Config::set('DEBUG', false);
        $result = $this->method_fetch->invoke($this->model, array(), array('order' => array('name' => 1)));
        $this->assertEquals('bread', $result->name);
        $id = $result->_id;
        $this->assertEquals(10, $this->model->get($id)->quantity);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testDelete()
    {
        Config::set('DEBUG', false);
        $result = $this->method_fetch->invoke($this->model, array(), array('order' => array('name' => 1)));
        $id = $result->_id;
        $this->assertEquals(1, $this->model->delete($id));
        $this->assertFalse($this->method_fetch->invoke($this->model, array('name' => 'bread')));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testUpdate()
    {
        Config::set('DEBUG', false);
        $this->model->insert(array('name' => 'testbread', 'price' => 3.2, 'quantity' => 10));
        $result = $this->method_fetch->invoke($this->model, array('name' => 'testbread'));
        $this->assertEquals(10, $result->quantity);
        $this->model->update(array('$set' => array('quantity' => 3)), $result->_id);
        $this->assertEquals(3, $this->model->get($result->_id)->quantity);
        $this->model->update(array('$set' => array('quantity' => 13)), (string)$result->_id);
        $this->assertEquals(13, $this->model->get($result->_id)->quantity);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testBatchInsert()
    {
        Config::set('DEBUG', false);
        $data = array(
            array('name' => 'first object'),
            array('name' => 'second object'),
            array('name' => 'equal object'),
            array('name' => 'equal object')
        );
        $this->model->batchInsert($data);
        $this->assertEquals(1, $this->method_count->invoke($this->model, array('name' => 'first object')));
        $this->assertEquals(2, $this->method_count->invoke($this->model, array('name' => 'equal object')));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testCount()
    {
        Config::set('DEBUG', false);
        $this->assertEquals(5, $this->method_count->invoke($this->model));
        $this->assertEquals(2, $this->method_count->invoke($this->model, array('name' => 'eggs')));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testUseMongoId()
    {
        Config::set('DEBUG', false);

        $this->assertAttributeEquals(true, 'useMongoId', $this->model);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testId()
    {
        Config::set('DEBUG', false);
        $class = new ReflectionClass('MongoModel');
        $prop = $class->getProperty('useMongoId');
        $prop->setAccessible(true);

        $this->model->insert(array('_id' => 1, 'name' => 'testbread', 'price' => 3.2, 'quantity' => 10));
        $this->model->insert(array('_id' => 2, 'name' => 'testbread', 'price' => 12, 'quantity' => 2));
        $this->assertSame(2, $this->method_count->invoke($this->model, array('name' => 'testbread')));

        $prop->setValue($this->model, false);
        $this->model->delete(1);
        $this->assertSame(1, $this->method_count->invoke($this->model, array('name' => 'testbread')));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testIdToMongoId()
    {
        Config::set('DEBUG', false);

        $this->model->insert(array('name' => 'testbread', 'price' => 3.2, 'quantity' => 10));
        $this->model->insert(array('name' => 'testbread', 'price' => 12, 'quantity' => 2));
        $this->assertSame(2, $this->method_count->invoke($this->model, array('name' => 'testbread')));
        $id = $this->method_fetch->invoke($this->model, array('name' => 'testbread'))->_id->__toString();
        $this->assertInternalType('string', $id);
        $item = $this->model->get($id);
        $this->assertSame('testbread', $item->name);
        $this->model->delete($id);
        $this->assertSame(1, $this->method_count->invoke($this->model, array('name' => 'testbread')));
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testAddCondition()
    {
        Config::set('DEBUG', false);

        $model = new ReflectionClass('MongoModel');
        $method = $model->getMethod('addCondition');
        $method->setAccessible(true);

        $query = array();
        $result = $method->invokeArgs($this->model, array(&$query, 'name', 'tony'));
        $this->assertSame(array('name' => 'tony'), $query);
        $this->assertSame($this->model, $result);
    }

    /**
     * @runInSeparateProcess
     * @group Mongo
     */
    public function testAddConditionEmptyValue()
    {
        Config::set('DEBUG', false);

        $model = new ReflectionClass('MongoModel');
        $method = $model->getMethod('addCondition');
        $method->setAccessible(true);

        $query = array();
        $method->invokeArgs($this->model, array(&$query, 'name', false));
        $this->assertEmpty($query);
        $method->invokeArgs($this->model, array(&$query, 'name', null));
        $this->assertEmpty($query);
    }
    public function tearDown()
    {
        $conf = array('driver' => 'MongoDriver', 'hostname' => 'localhost', 'database' => 'test', 'username' => 'test', 'password' => '1234', 'port' => 27017);

        $connection = new Mongo('mongodb://' . $conf['hostname'] . ':' . $conf['port']);
        $db = $connection->selectDB($conf['database']);
        $db->authenticate($conf['username'], $conf['password']);
        $collection = 'mongomock';
        $db->selectCollection($collection)->remove(array());
    }

    protected function newCallback($className)
    {
        switch ($className) {
            case 'CacheKey':
                return 'MockCacheKey';
            default:
                return $className;
        }
    }

    public function dbSetUp($conf)
    {
        $data = array(
            array(
                'name' => 'bread',
                'price' => 3.2,
                'quantity' => 10
            ),
            array(
                'name' => 'eggs',
                'price' => 2.1,
                'quantity' => 20
            ),
            array(
                'name' => 'fish',
                'price' => 13.2,
                'quantity' => 2
            ),
            array(
                'name' => 'milk',
                'price' => 3.8,
                'quantity' => 1
            ),
            array(
                'name' => 'eggs',
                'price' => 2.3,
                'quantity' => 5
            )
        );
        $connection = new Mongo('mongodb://' . $conf['default']['hostname'] . ':' . $conf['default']['port']);
        $db = $connection->selectDB($conf['default']['database']);
        $db->authenticate($conf['default']['username'], $conf['default']['password']);
        $collection = 'mongomock';
        $db->dropCollection($collection);
        $collection = $db->selectCollection($collection);
        foreach ($data as $document) {
            $collection->insert($document);
        }
    }
}