<?php

namespace Doctrine\Tests\DBAL\Functional;

use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\Logging\DebugStack;

/**
 * @group DDC-217
 */
class ResultCacheTest extends \Doctrine\Tests\DbalFunctionalTestCase
{
    /**
     * @var int[][]|string[][]
     */
    private $expectedResult = array(array('test_int' => 100, 'test_string' => 'foo'), array('test_int' => 200, 'test_string' => 'bar'), array('test_int' => 300, 'test_string' => 'baz'));

    /**
     * @var DebugStack
     */
    private $sqlLogger;

    protected function setUp()
    {
        parent::setUp();

        $table = new \Doctrine\DBAL\Schema\Table("caching");
        $table->addColumn('test_int', 'integer');
        $table->addColumn('test_string', 'string', array('notnull' => false));
        $table->setPrimaryKey(array('test_int'));

        $sm = $this->_conn->getSchemaManager();
        $sm->createTable($table);

        foreach ($this->expectedResult as $row) {
            $this->_conn->insert('caching', $row);
        }

        $config = $this->_conn->getConfiguration();
        $config->setSQLLogger($this->sqlLogger = new DebugStack);

        $cache = new \Doctrine\Common\Cache\ArrayCache;
        $config->setResultCacheImpl($cache);
    }

    protected function tearDown()
    {
        $this->_conn->getSchemaManager()->dropTable('caching');

        parent::tearDown();
    }

    public function testCacheFetchAssoc()
    {
        self::assertCacheNonCacheSelectSameFetchModeAreEqual(
            $this->expectedResult,
            FetchMode::ASSOCIATIVE
        );
    }

    public function testFetchNum()
    {
        $expectedResult = array();
        foreach ($this->expectedResult as $v) {
            $expectedResult[] = array_values($v);
        }

        self::assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, FetchMode::NUMERIC);
    }

    public function testFetchBoth()
    {
        $expectedResult = array();
        foreach ($this->expectedResult as $v) {
            $expectedResult[] = array_merge($v, array_values($v));
        }

        self::assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, FetchMode::MIXED);
    }

    public function testFetchColumn()
    {
        $expectedResult = array();
        foreach ($this->expectedResult as $v) {
            $expectedResult[] = array_shift($v);
        }

        self::assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, FetchMode::COLUMN);
    }

    public function testMixingFetch()
    {
        $numExpectedResult = array();
        foreach ($this->expectedResult as $v) {
            $numExpectedResult[] = array_values($v);
        }
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        $data = $this->hydrateStmt($stmt, FetchMode::ASSOCIATIVE);

        self::assertEquals($this->expectedResult, $data);

        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        $data = $this->hydrateStmt($stmt, FetchMode::NUMERIC);

        self::assertEquals($numExpectedResult, $data);
    }

    public function testIteratorFetch()
    {
        self::assertStandardAndIteratorFetchAreEqual(FetchMode::MIXED);
        self::assertStandardAndIteratorFetchAreEqual(FetchMode::ASSOCIATIVE);
        self::assertStandardAndIteratorFetchAreEqual(FetchMode::NUMERIC);
    }

    public function assertStandardAndIteratorFetchAreEqual($fetchMode)
    {
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));
        $data = $this->hydrateStmt($stmt, $fetchMode);

        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));
        $data_iterator = $this->hydrateStmtIterator($stmt, $fetchMode);

        self::assertEquals($data, $data_iterator);
    }

    public function testDontCloseNoCache()
    {
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        $data = array();

        while ($row = $stmt->fetch(FetchMode::ASSOCIATIVE)) {
            $data[] = $row;
        }

        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        $data = array();

        while ($row = $stmt->fetch(FetchMode::NUMERIC)) {
            $data[] = $row;
        }

        self::assertCount(2, $this->sqlLogger->queries);
    }

    public function testDontFinishNoCache()
    {
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        $stmt->fetch(FetchMode::ASSOCIATIVE);
        $stmt->closeCursor();

        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        $this->hydrateStmt($stmt, FetchMode::NUMERIC);

        self::assertCount(2, $this->sqlLogger->queries);
    }

    public function assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, $fetchMode)
    {
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        self::assertEquals(2, $stmt->columnCount());
        $data = $this->hydrateStmt($stmt, $fetchMode);
        self::assertEquals($expectedResult, $data);

        $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey"));

        self::assertEquals(2, $stmt->columnCount());
        $data = $this->hydrateStmt($stmt, $fetchMode);
        self::assertEquals($expectedResult, $data);
        self::assertCount(1, $this->sqlLogger->queries, "just one dbal hit");
    }

    public function testEmptyResultCache()
    {
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey"));
        $data = $this->hydrateStmt($stmt);

        $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey"));
        $data = $this->hydrateStmt($stmt);

        self::assertCount(1, $this->sqlLogger->queries, "just one dbal hit");
    }

    public function testChangeCacheImpl()
    {
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey"));
        $data = $this->hydrateStmt($stmt);

        $secondCache = new \Doctrine\Common\Cache\ArrayCache;
        $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey", $secondCache));
        $data = $this->hydrateStmt($stmt);

        self::assertCount(2, $this->sqlLogger->queries, "two hits");
        self::assertCount(1, $secondCache->fetch("emptycachekey"));
    }

    private function hydrateStmt($stmt, $fetchMode = FetchMode::ASSOCIATIVE)
    {
        $data = array();
        while ($row = $stmt->fetch($fetchMode)) {
            $data[] = is_array($row) ? array_change_key_case($row, CASE_LOWER) : $row;
        }
        $stmt->closeCursor();
        return $data;
    }

    private function hydrateStmtIterator($stmt, $fetchMode = FetchMode::ASSOCIATIVE)
    {
        $data = array();
        $stmt->setFetchMode($fetchMode);
        foreach ($stmt as $row) {
            $data[] = is_array($row) ? array_change_key_case($row, CASE_LOWER) : $row;
        }
        $stmt->closeCursor();
        return $data;
    }
}