<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional;

use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\IBMDB2\DB2Driver;
use Doctrine\DBAL\Driver\PDOMySql\Driver as PDOMySQLDriver;
use Doctrine\DBAL\Driver\PDOOracle\Driver as PDOOracleDriver;
use Doctrine\DBAL\Driver\PDOSqlsrv\Driver as PDOSQLSRVDriver;
use Doctrine\DBAL\Driver\SQLSrv\Driver as SQLSRVDriver;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Types\Type;
use function base64_decode;
use function get_class;
use function sprintf;
use function stream_get_contents;

class StatementTest extends FunctionalTestCase
{
    protected function setUp() : void
    {
        parent::setUp();

        $table = new Table('stmt_test');
        $table->addColumn('id', 'integer');
        $table->addColumn('name', 'text', ['notnull' => false]);
        $this->connection->getSchemaManager()->dropAndCreateTable($table);
    }

    public function testStatementIsReusableAfterClosingCursor() : void
    {
        if ($this->connection->getDriver() instanceof PDOOracleDriver) {
            self::markTestIncomplete('See https://bugs.php.net/bug.php?id=77181');
        }

        $this->connection->insert('stmt_test', ['id' => 1]);
        $this->connection->insert('stmt_test', ['id' => 2]);

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test ORDER BY id');

        $stmt->execute();

        $id = $stmt->fetchOne();
        self::assertEquals(1, $id);

        $stmt->closeCursor();

        $stmt->execute();
        $id = $stmt->fetchOne();
        self::assertEquals(1, $id);
        $id = $stmt->fetchOne();
        self::assertEquals(2, $id);
    }

    public function testReuseStatementWithLongerResults() : void
    {
        if ($this->connection->getDriver() instanceof PDOOracleDriver) {
            self::markTestIncomplete('PDO_OCI doesn\'t support fetching blobs via PDOStatement::fetchAll()');
        }

        $sm    = $this->connection->getSchemaManager();
        $table = new Table('stmt_longer_results');
        $table->addColumn('param', 'string', ['length' => 24]);
        $table->addColumn('val', 'text');
        $sm->createTable($table);

        $row1 = [
            'param' => 'param1',
            'val' => 'X',
        ];
        $this->connection->insert('stmt_longer_results', $row1);

        $stmt = $this->connection->prepare('SELECT param, val FROM stmt_longer_results ORDER BY param');
        $stmt->execute();
        self::assertEquals([
            ['param1', 'X'],
        ], $stmt->fetchAllNumeric());

        $row2 = [
            'param' => 'param2',
            'val' => 'A bit longer value',
        ];
        $this->connection->insert('stmt_longer_results', $row2);

        $stmt->execute();
        self::assertEquals([
            ['param1', 'X'],
            ['param2', 'A bit longer value'],
        ], $stmt->fetchAllNumeric());
    }

    public function testFetchLongBlob() : void
    {
        if ($this->connection->getDriver() instanceof PDOOracleDriver) {
            // inserting BLOBs as streams on Oracle requires Oracle-specific SQL syntax which is currently not supported
            // see http://php.net/manual/en/pdo.lobs.php#example-1035
            self::markTestSkipped('DBAL doesn\'t support storing LOBs represented as streams using PDO_OCI');
        }

        // make sure memory limit is large enough to not cause false positives,
        // but is still not enough to store a LONGBLOB of the max possible size
        $this->iniSet('memory_limit', '4G');

        $sm    = $this->connection->getSchemaManager();
        $table = new Table('stmt_long_blob');
        $table->addColumn('contents', 'blob', ['length' => 0xFFFFFFFF]);
        $sm->createTable($table);

        $contents = base64_decode(<<<EOF
H4sICJRACVgCA2RvY3RyaW5lLmljbwDtVNtLFHEU/ia1i9fVzVWxvJSrZmoXS6pd0zK7QhdNc03z
lrpppq1pWqJCFERZkUFEDybYBQqJhB6iUOqhh+whgl4qkF6MfGh+s87O7GVmO6OlBfUfdIZvznxn
fpzznW9gAI4unQ50XwirH2AAkEygEuIwU58ODnPBzXGv14sEq4BrwzKKL4sY++SGTz6PodcutN5x
IPvsFCa+K9CXMfS/cOL5OxesN0Wceygho0WAXVLwcUJBdDVDaqOAij4Rrz640XlXQmAxQ16PHU63
iqdvXbg4JOHLpILBUSdM7XZEVDDcfuZEbI2ASaYguUGAroSh97GMngcSeFFFerMdI+/dyGy1o+GW
Ax5FxfAbFwoviajuc+DCIwn+RTwGRmRIThXxdQJyu+z4/NUDYz2DKCsILuERWsoQfoQhqpLhyhMZ
XfcknBmU0NLvQArpTm0SsI5mqKqKuFoGc8cUcjrtqLohom1AgtujQnapmJJU+BbwCLIwhJXyiKlh
MB4TkFgvIK3JjrRmAefJm+77Eiqvi+SvCq/qJahQyWuVuEpcIa7QLh7Kbsourb9b66/pZdAd1voz
fCNfwsp46OnZQPojSX9UFcNy+mYJNDeJPHtJfqeR/nSaPTzmwlXar5dQ1adpd+B//I9/hi0xuCPQ
Nkvb5um37Wtc+auQXZsVxEVYD5hnCilxTaYYjsuxLlsxXUitzd2hs3GWHLM5UOM7Fy8t3xiat4fb
sneNxmNb/POO1pRXc7vnF2nc13Rq0cFWiyXkuHmzxuOtzUYfC7fEmK/3mx4QZd5u4E7XJWz6+dey
Za4tXHUiPyB8Vm781oaT+3fN6Y/eUFDfPkcNWetNxb+tlxEZsPqPdZMOzS4rxwJ8CDC+ABj1+Tu0
d+N0hqezcjblboJ3Bj8ARJilHX4FAAA=
EOF
        , true);

        $this->connection->insert('stmt_long_blob', ['contents' => $contents], [ParameterType::LARGE_OBJECT]);

        $stmt = $this->connection->prepare('SELECT contents FROM stmt_long_blob');
        $stmt->execute();

        $stream = Type::getType('blob')
            ->convertToPHPValue(
                $stmt->fetchOne(),
                $this->connection->getDatabasePlatform()
            );

        self::assertSame($contents, stream_get_contents($stream));
    }

    public function testIncompletelyFetchedStatementDoesNotBlockConnection() : void
    {
        $this->connection->insert('stmt_test', ['id' => 1]);
        $this->connection->insert('stmt_test', ['id' => 2]);

        $stmt1 = $this->connection->prepare('SELECT id FROM stmt_test');
        $stmt1->execute();
        $stmt1->fetchAssociative();
        $stmt1->execute();
        // fetching only one record out of two
        $stmt1->fetchAssociative();

        $stmt2 = $this->connection->prepare('SELECT id FROM stmt_test WHERE id = ?');
        $stmt2->execute([1]);
        self::assertEquals(1, $stmt2->fetchOne());
    }

    public function testReuseStatementAfterClosingCursor() : void
    {
        if ($this->connection->getDriver() instanceof PDOOracleDriver) {
            self::markTestIncomplete('See https://bugs.php.net/bug.php?id=77181');
        }

        $this->connection->insert('stmt_test', ['id' => 1]);
        $this->connection->insert('stmt_test', ['id' => 2]);

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test WHERE id = ?');

        $stmt->execute([1]);
        $id = $stmt->fetchOne();
        self::assertEquals(1, $id);

        $stmt->closeCursor();

        $stmt->execute([2]);
        $id = $stmt->fetchOne();
        self::assertEquals(2, $id);
    }

    public function testReuseStatementWithParameterBoundByReference() : void
    {
        $this->connection->insert('stmt_test', ['id' => 1]);
        $this->connection->insert('stmt_test', ['id' => 2]);

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test WHERE id = ?');
        $stmt->bindParam(1, $id);

        $id = 1;
        $stmt->execute();
        self::assertEquals(1, $stmt->fetchOne());

        $id = 2;
        $stmt->execute();
        self::assertEquals(2, $stmt->fetchOne());
    }

    public function testReuseStatementWithReboundValue() : void
    {
        $this->connection->insert('stmt_test', ['id' => 1]);
        $this->connection->insert('stmt_test', ['id' => 2]);

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test WHERE id = ?');

        $stmt->bindValue(1, 1);
        $stmt->execute();
        self::assertEquals(1, $stmt->fetchOne());

        $stmt->bindValue(1, 2);
        $stmt->execute();
        self::assertEquals(2, $stmt->fetchOne());
    }

    public function testReuseStatementWithReboundParam() : void
    {
        $this->connection->insert('stmt_test', ['id' => 1]);
        $this->connection->insert('stmt_test', ['id' => 2]);

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test WHERE id = ?');

        $x = 1;
        $stmt->bindParam(1, $x);
        $stmt->execute();
        self::assertEquals(1, $stmt->fetchOne());

        $y = 2;
        $stmt->bindParam(1, $y);
        $stmt->execute();
        self::assertEquals(2, $stmt->fetchOne());
    }

    /**
     * @param mixed $expected
     *
     * @dataProvider emptyFetchProvider
     */
    public function testFetchFromNonExecutedStatement(callable $fetch, $expected) : void
    {
        $stmt = $this->connection->prepare('SELECT id FROM stmt_test');

        self::assertSame($expected, $fetch($stmt));
    }

    public function testCloseCursorOnNonExecutedStatement() : void
    {
        $this->expectNotToPerformAssertions();

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test');

        $stmt->closeCursor();
    }

    /**
     * @group DBAL-2637
     */
    public function testCloseCursorAfterCursorEnd() : void
    {
        $this->expectNotToPerformAssertions();

        $stmt = $this->connection->prepare('SELECT name FROM stmt_test');

        $stmt->execute();
        $stmt->fetchAssociative();

        $stmt->closeCursor();
    }

    public function testCloseCursorAfterClosingCursor() : void
    {
        $this->expectNotToPerformAssertions();

        $stmt = $this->connection->executeQuery('SELECT name FROM stmt_test');
        $stmt->closeCursor();
        $stmt->closeCursor();
    }

    /**
     * @param mixed $expected
     *
     * @dataProvider emptyFetchProvider
     */
    public function testFetchFromNonExecutedStatementWithClosedCursor(callable $fetch, $expected) : void
    {
        $stmt = $this->connection->prepare('SELECT id FROM stmt_test');
        $stmt->closeCursor();

        self::assertSame($expected, $fetch($stmt));
    }

    /**
     * @param mixed $expected
     *
     * @dataProvider emptyFetchProvider
     */
    public function testFetchFromExecutedStatementWithClosedCursor(callable $fetch, $expected) : void
    {
        $this->connection->insert('stmt_test', ['id' => 1]);

        $stmt = $this->connection->prepare('SELECT id FROM stmt_test');
        $stmt->execute();
        $stmt->closeCursor();

        self::assertSame($expected, $fetch($stmt));
    }

    /**
     * @return mixed[][]
     */
    public static function emptyFetchProvider() : iterable
    {
        return [
            'fetch' => [
                static function (Statement $stmt) {
                    return $stmt->fetchAssociative();
                },
                false,
            ],
            'fetch-column' => [
                static function (Statement $stmt) {
                    return $stmt->fetchOne();
                },
                false,
            ],
            'fetch-all' => [
                static function (Statement $stmt) : array {
                    return $stmt->fetchAllAssociative();
                },
                [],
            ],
        ];
    }

    public function testFetchInColumnMode() : void
    {
        $platform = $this->connection->getDatabasePlatform();
        $query    = $platform->getDummySelectSQL();
        $result   = $this->connection->executeQuery($query)->fetchOne();

        self::assertEquals(1, $result);
    }

    public function testExecWithRedundantParameters() : void
    {
        $driver = $this->connection->getDriver();

        if ($driver instanceof PDOMySQLDriver
            || $driver instanceof PDOOracleDriver
            || $driver instanceof PDOSQLSRVDriver
        ) {
            self::markTestSkipped(sprintf(
                'The underlying implementation of the "%s" driver does not report redundant parameters',
                get_class($driver)
            ));
        }

        if ($driver instanceof DB2Driver) {
            self::markTestSkipped('db2_execute() does not report redundant parameters');
        }

        if ($driver instanceof SQLSRVDriver) {
            self::markTestSkipped('sqlsrv_prepare() does not report redundant parameters');
        }

        $platform = $this->connection->getDatabasePlatform();
        $query    = $platform->getDummySelectSQL();
        $stmt     = $this->connection->prepare($query);

        // we want to make sure the exception is thrown by the DBAL code, not by PHPUnit due to a PHP-level error,
        // but the wrapper connection wraps everything in a DBAL exception
        $this->iniSet('error_reporting', '0');

        $this->expectException(DBALException::class);
        $stmt->execute([null]);
    }
}