<?php

namespace Doctrine\Tests\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\DriverException;
use Doctrine\DBAL\Driver\ExceptionConverterDriver;
use Doctrine\DBAL\VersionAwarePlatformDriver;
use Doctrine\Tests\DbalTestCase;
use Throwable;

abstract class AbstractDriverTest extends DbalTestCase
{
    const EXCEPTION_CONNECTION = 'Doctrine\DBAL\Exception\ConnectionException';
    const EXCEPTION_CONSTRAINT_VIOLATION = 'Doctrine\DBAL\Exception\ConstraintViolationException';
    const EXCEPTION_DATABASE_OBJECT_EXISTS = 'Doctrine\DBAL\Exception\DatabaseObjectExistsException';
    const EXCEPTION_DATABASE_OBJECT_NOT_FOUND = 'Doctrine\DBAL\Exception\DatabaseObjectNotFoundException';
    const EXCEPTION_DRIVER = 'Doctrine\DBAL\Exception\DriverException';
    const EXCEPTION_FOREIGN_KEY_CONSTRAINT_VIOLATION = 'Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException';
    const EXCEPTION_INVALID_FIELD_NAME = 'Doctrine\DBAL\Exception\InvalidFieldNameException';
    const EXCEPTION_NON_UNIQUE_FIELD_NAME = 'Doctrine\DBAL\Exception\NonUniqueFieldNameException';
    const EXCEPTION_NOT_NULL_CONSTRAINT_VIOLATION = 'Doctrine\DBAL\Exception\NotNullConstraintViolationException';
    const EXCEPTION_READ_ONLY = 'Doctrine\DBAL\Exception\ReadOnlyException';
    const EXCEPTION_SERVER = 'Doctrine\DBAL\Exception\ServerException';
    const EXCEPTION_SYNTAX_ERROR = 'Doctrine\DBAL\Exception\SyntaxErrorException';
    const EXCEPTION_TABLE_EXISTS = 'Doctrine\DBAL\Exception\TableExistsException';
    const EXCEPTION_TABLE_NOT_FOUND = 'Doctrine\DBAL\Exception\TableNotFoundException';
    const EXCEPTION_UNIQUE_CONSTRAINT_VIOLATION = 'Doctrine\DBAL\Exception\UniqueConstraintViolationException';
    const EXCEPTION_DEADLOCK = 'Doctrine\DBAL\Exception\DeadlockException';
    const EXCEPTION_LOCK_WAIT_TIMEOUT = 'Doctrine\DBAL\Exception\LockWaitTimeoutException';

    /**
     * The driver mock under test.
     *
     * @var \Doctrine\DBAL\Driver
     */
    protected $driver;

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

        $this->driver = $this->createDriver();
    }

    public function testConvertsException()
    {
        if ( ! $this->driver instanceof ExceptionConverterDriver) {
            $this->markTestSkipped('This test is only intended for exception converter drivers.');
        }

        $data = $this->getExceptionConversions();

        if (empty($data)) {
            $this->fail(
                sprintf(
                    'No test data found for test %s. You have to return test data from %s.',
                    get_class($this) . '::' . __FUNCTION__,
                    get_class($this) . '::getExceptionConversionData'
                )
            );
        }

        $driverException = new class extends \Exception implements DriverException
        {
            public function __construct()
            {
                parent::__construct('baz');
            }

            /**
             * {@inheritDoc}
             */
            public function getErrorCode()
            {
                return 'foo';
            }

            /**
             * {@inheritDoc}
             */
            public function getSQLState()
            {
                return 'bar';
            }
        };

        $data[] = array($driverException, self::EXCEPTION_DRIVER);

        $message = 'DBAL exception message';

        foreach ($data as $item) {
            /** @var $driverException \Doctrine\DBAL\Driver\DriverException */
            list($driverException, $convertedExceptionClassName) = $item;

            $convertedException = $this->driver->convertException($message, $driverException);

            $this->assertSame($convertedExceptionClassName, get_class($convertedException));

            $this->assertSame($driverException->getErrorCode(), $convertedException->getErrorCode());
            $this->assertSame($driverException->getSQLState(), $convertedException->getSQLState());
            $this->assertSame($message, $convertedException->getMessage());
        }
    }

    public function testCreatesDatabasePlatformForVersion()
    {
        if ( ! $this->driver instanceof VersionAwarePlatformDriver) {
            $this->markTestSkipped('This test is only intended for version aware platform drivers.');
        }

        $data = $this->getDatabasePlatformsForVersions();

        if (empty($data)) {
            $this->fail(
                sprintf(
                    'No test data found for test %s. You have to return test data from %s.',
                    get_class($this) . '::' . __FUNCTION__,
                    get_class($this) . '::getDatabasePlatformsForVersions'
                )
            );
        }

        foreach ($data as $item) {
            $this->assertSame($item[1], get_class($this->driver->createDatabasePlatformForVersion($item[0])));
        }
    }

    /**
     * @expectedException \Doctrine\DBAL\DBALException
     */
    public function testThrowsExceptionOnCreatingDatabasePlatformsForInvalidVersion()
    {
        if ( ! $this->driver instanceof VersionAwarePlatformDriver) {
            $this->markTestSkipped('This test is only intended for version aware platform drivers.');
        }

        $this->driver->createDatabasePlatformForVersion('foo');
    }

    public function testReturnsDatabaseName()
    {
        $params = array(
            'user'     => 'foo',
            'password' => 'bar',
            'dbname'   => 'baz',
        );

        $connection = $this->getConnectionMock();

        $connection->expects($this->once())
            ->method('getParams')
            ->will($this->returnValue($params));

        $this->assertSame($params['dbname'], $this->driver->getDatabase($connection));
    }

    public function testReturnsDatabasePlatform()
    {
        $this->assertEquals($this->createPlatform(), $this->driver->getDatabasePlatform());
    }

    public function testReturnsSchemaManager()
    {
        $connection    = $this->getConnectionMock();
        $schemaManager = $this->driver->getSchemaManager($connection);

        $this->assertEquals($this->createSchemaManager($connection), $schemaManager);
        $this->assertAttributeSame($connection, '_conn', $schemaManager);
    }

    /**
     * Factory method for creating the driver instance under test.
     *
     * @return \Doctrine\DBAL\Driver
     */
    abstract protected function createDriver();

    /**
     * Factory method for creating the the platform instance return by the driver under test.
     *
     * The platform instance returned by this method must be the same as returned by
     * the driver's getDatabasePlatform() method.
     *
     * @return \Doctrine\DBAL\Platforms\AbstractPlatform
     */
    abstract protected function createPlatform();

    /**
     * Factory method for creating the the schema manager instance return by the driver under test.
     *
     * The schema manager instance returned by this method must be the same as returned by
     * the driver's getSchemaManager() method.
     *
     * @param Connection $connection The underlying connection to use.
     *
     * @return \Doctrine\DBAL\Schema\AbstractSchemaManager
     */
    abstract protected function createSchemaManager(Connection $connection);

    protected function getConnectionMock()
    {
        return $this->getMockBuilder('Doctrine\DBAL\Connection')
            ->disableOriginalConstructor()
            ->getMock();
    }

    protected function getDatabasePlatformsForVersions()
    {
        return array();
    }

    protected function getExceptionConversionData()
    {
        return array();
    }

    private function getExceptionConversions()
    {
        $data = array();

        foreach ($this->getExceptionConversionData() as $convertedExceptionClassName => $errors) {
            foreach ($errors as $error) {
                $driverException = new class ($error[0], $error[1], $error[2])
                    extends \Exception
                    implements DriverException
                {
                    /**
                     * @var mixed
                     */
                    private $errorCode;

                    /**
                     * @var mixed
                     */
                    private $sqlState;

                    public function __construct($errorCode, $sqlState, $message)
                    {
                        parent::__construct($message);

                        $this->errorCode = $errorCode;
                        $this->sqlState  = $sqlState;
                    }

                    /**
                     * {@inheritDoc}
                     */
                    public function getErrorCode()
                    {
                        return $this->errorCode;
                    }

                    /**
                     * {@inheritDoc}
                     */
                    public function getSQLState()
                    {
                        return $this->sqlState;
                    }
                };

                $data[] = array($driverException, $convertedExceptionClassName);
            }
        }

        return $data;
    }
}