<?php

namespace Doctrine\Tests\DBAL\Schema;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaConfig;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\Visitor\AbstractVisitor;
use Doctrine\DBAL\Schema\Visitor\Visitor;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;

use function current;
use function strlen;

class SchemaTest extends TestCase
{
    public function testAddTable(): void
    {
        $tableName = 'public.foo';
        $table     = new Table($tableName);

        $schema = new Schema([$table]);

        self::assertTrue($schema->hasTable($tableName));

        $tables = $schema->getTables();
        self::assertArrayHasKey($tableName, $tables);
        self::assertSame($table, $tables[$tableName]);
        self::assertSame($table, $schema->getTable($tableName));
        self::assertTrue($schema->hasTable($tableName));
    }

    public function testTableMatchingCaseInsensitive(): void
    {
        $table = new Table('Foo');

        $schema = new Schema([$table]);
        self::assertTrue($schema->hasTable('foo'));
        self::assertTrue($schema->hasTable('FOO'));

        self::assertSame($table, $schema->getTable('FOO'));
        self::assertSame($table, $schema->getTable('foo'));
        self::assertSame($table, $schema->getTable('Foo'));
    }

    public function testGetUnknownTableThrowsException(): void
    {
        $this->expectException(SchemaException::class);

        $schema = new Schema();
        $schema->getTable('unknown');
    }

    public function testCreateTableTwiceThrowsException(): void
    {
        $this->expectException(SchemaException::class);

        $tableName = 'foo';
        $table     = new Table($tableName);
        $tables    = [$table, $table];

        $schema = new Schema($tables);
    }

    public function testRenameTable(): void
    {
        $tableName = 'foo';
        $table     = new Table($tableName);
        $schema    = new Schema([$table]);

        self::assertTrue($schema->hasTable('foo'));
        $schema->renameTable('foo', 'bar');
        self::assertFalse($schema->hasTable('foo'));
        self::assertTrue($schema->hasTable('bar'));
        self::assertSame($table, $schema->getTable('bar'));
    }

    public function testDropTable(): void
    {
        $tableName = 'foo';
        $table     = new Table($tableName);
        $schema    = new Schema([$table]);

        self::assertTrue($schema->hasTable('foo'));

        $schema->dropTable('foo');

        self::assertFalse($schema->hasTable('foo'));
    }

    public function testCreateTable(): void
    {
        $schema = new Schema();

        self::assertFalse($schema->hasTable('foo'));

        $table = $schema->createTable('foo');

        self::assertInstanceOf(Table::class, $table);
        self::assertEquals('foo', $table->getName());
        self::assertTrue($schema->hasTable('foo'));
    }

    public function testAddSequences(): void
    {
        $sequence = new Sequence('a_seq', 1, 1);

        $schema = new Schema([], [$sequence]);

        self::assertTrue($schema->hasSequence('a_seq'));
        self::assertInstanceOf(Sequence::class, $schema->getSequence('a_seq'));

        $sequences = $schema->getSequences();
        self::assertArrayHasKey('public.a_seq', $sequences);
    }

    public function testSequenceAccessCaseInsensitive(): void
    {
        $sequence = new Sequence('a_Seq');

        $schema = new Schema([], [$sequence]);
        self::assertTrue($schema->hasSequence('a_seq'));
        self::assertTrue($schema->hasSequence('a_Seq'));
        self::assertTrue($schema->hasSequence('A_SEQ'));

        self::assertEquals($sequence, $schema->getSequence('a_seq'));
        self::assertEquals($sequence, $schema->getSequence('a_Seq'));
        self::assertEquals($sequence, $schema->getSequence('A_SEQ'));
    }

    public function testGetUnknownSequenceThrowsException(): void
    {
        $this->expectException(SchemaException::class);

        $schema = new Schema();
        $schema->getSequence('unknown');
    }

    public function testCreateSequence(): void
    {
        $schema   = new Schema();
        $sequence = $schema->createSequence('a_seq', 10, 20);

        self::assertEquals('a_seq', $sequence->getName());
        self::assertEquals(10, $sequence->getAllocationSize());
        self::assertEquals(20, $sequence->getInitialValue());

        self::assertTrue($schema->hasSequence('a_seq'));
        self::assertInstanceOf(Sequence::class, $schema->getSequence('a_seq'));

        $sequences = $schema->getSequences();
        self::assertArrayHasKey('public.a_seq', $sequences);
    }

    public function testDropSequence(): void
    {
        $sequence = new Sequence('a_seq', 1, 1);

        $schema = new Schema([], [$sequence]);

        $schema->dropSequence('a_seq');
        self::assertFalse($schema->hasSequence('a_seq'));
    }

    public function testAddSequenceTwiceThrowsException(): void
    {
        $this->expectException(SchemaException::class);

        $sequence = new Sequence('a_seq', 1, 1);

        $schema = new Schema([], [$sequence, $sequence]);
    }

    public function testConfigMaxIdentifierLength(): void
    {
        $schemaConfig = new SchemaConfig();
        $schemaConfig->setMaxIdentifierLength(5);

        $schema = new Schema([], [], $schemaConfig);
        $table  = $schema->createTable('smalltable');
        $table->addColumn('long_id', 'integer');
        $table->addIndex(['long_id']);

        $index = current($table->getIndexes());
        self::assertEquals(5, strlen($index->getName()));
    }

    public function testDeepClone(): void
    {
        $schema   = new Schema();
        $sequence = $schema->createSequence('baz');

        $tableA = $schema->createTable('foo');
        $tableA->addColumn('id', 'integer');

        $tableB = $schema->createTable('bar');
        $tableB->addColumn('id', 'integer');
        $tableB->addColumn('foo_id', 'integer');
        $tableB->addForeignKeyConstraint($tableA, ['foo_id'], ['id']);

        $schemaNew = clone $schema;

        self::assertNotSame($sequence, $schemaNew->getSequence('baz'));

        self::assertNotSame($tableA, $schemaNew->getTable('foo'));
        self::assertNotSame($tableA->getColumn('id'), $schemaNew->getTable('foo')->getColumn('id'));

        self::assertNotSame($tableB, $schemaNew->getTable('bar'));
        self::assertNotSame($tableB->getColumn('id'), $schemaNew->getTable('bar')->getColumn('id'));

        $fk = $schemaNew->getTable('bar')->getForeignKeys();
        $fk = current($fk);

        $re = new ReflectionProperty($fk, '_localTable');
        $re->setAccessible(true);

        self::assertSame($schemaNew->getTable('bar'), $re->getValue($fk));
    }

    public function testHasTableForQuotedAsset(): void
    {
        $schema = new Schema();

        $tableA = $schema->createTable('foo');
        $tableA->addColumn('id', 'integer');

        self::assertTrue($schema->hasTable('`foo`'));
    }

    public function testHasNamespace(): void
    {
        $schema = new Schema();

        self::assertFalse($schema->hasNamespace('foo'));

        $schema->createTable('foo');

        self::assertFalse($schema->hasNamespace('foo'));

        $schema->createTable('bar.baz');

        self::assertFalse($schema->hasNamespace('baz'));
        self::assertTrue($schema->hasNamespace('bar'));
        self::assertFalse($schema->hasNamespace('tab'));

        $schema->createTable('tab.taz');

        self::assertTrue($schema->hasNamespace('tab'));
    }

    public function testCreatesNamespace(): void
    {
        $schema = new Schema();

        self::assertFalse($schema->hasNamespace('foo'));

        $schema->createNamespace('foo');

        self::assertTrue($schema->hasNamespace('foo'));
        self::assertTrue($schema->hasNamespace('FOO'));
        self::assertTrue($schema->hasNamespace('`foo`'));
        self::assertTrue($schema->hasNamespace('`FOO`'));

        $schema->createNamespace('`bar`');

        self::assertTrue($schema->hasNamespace('bar'));
        self::assertTrue($schema->hasNamespace('BAR'));
        self::assertTrue($schema->hasNamespace('`bar`'));
        self::assertTrue($schema->hasNamespace('`BAR`'));

        self::assertSame(['foo' => 'foo', 'bar' => '`bar`'], $schema->getNamespaces());
    }

    public function testThrowsExceptionOnCreatingNamespaceTwice(): void
    {
        $schema = new Schema();

        $schema->createNamespace('foo');

        $this->expectException(SchemaException::class);

        $schema->createNamespace('foo');
    }

    public function testCreatesNamespaceThroughAddingTableImplicitly(): void
    {
        $schema = new Schema();

        self::assertFalse($schema->hasNamespace('foo'));

        $schema->createTable('baz');

        self::assertFalse($schema->hasNamespace('foo'));
        self::assertFalse($schema->hasNamespace('baz'));

        $schema->createTable('foo.bar');

        self::assertTrue($schema->hasNamespace('foo'));
        self::assertFalse($schema->hasNamespace('bar'));

        $schema->createTable('`baz`.bloo');

        self::assertTrue($schema->hasNamespace('baz'));
        self::assertFalse($schema->hasNamespace('bloo'));

        $schema->createTable('`baz`.moo');

        self::assertTrue($schema->hasNamespace('baz'));
        self::assertFalse($schema->hasNamespace('moo'));
    }

    public function testCreatesNamespaceThroughAddingSequenceImplicitly(): void
    {
        $schema = new Schema();

        self::assertFalse($schema->hasNamespace('foo'));

        $schema->createSequence('baz');

        self::assertFalse($schema->hasNamespace('foo'));
        self::assertFalse($schema->hasNamespace('baz'));

        $schema->createSequence('foo.bar');

        self::assertTrue($schema->hasNamespace('foo'));
        self::assertFalse($schema->hasNamespace('bar'));

        $schema->createSequence('`baz`.bloo');

        self::assertTrue($schema->hasNamespace('baz'));
        self::assertFalse($schema->hasNamespace('bloo'));

        $schema->createSequence('`baz`.moo');

        self::assertTrue($schema->hasNamespace('baz'));
        self::assertFalse($schema->hasNamespace('moo'));
    }

    public function testVisitsVisitor(): void
    {
        $schema  = new Schema();
        $visitor = $this->createMock(Visitor::class);

        $schema->createNamespace('foo');
        $schema->createNamespace('bar');

        $schema->createTable('baz');
        $schema->createTable('bla.bloo');

        $schema->createSequence('moo');
        $schema->createSequence('war');

        $visitor->expects($this->once())
            ->method('acceptSchema')
            ->with($schema);

        $visitor->expects($this->at(1))
            ->method('acceptTable')
            ->with($schema->getTable('baz'));

        $visitor->expects($this->at(2))
            ->method('acceptTable')
            ->with($schema->getTable('bla.bloo'));

        $visitor->expects($this->exactly(2))
            ->method('acceptTable');

        $visitor->expects($this->at(3))
            ->method('acceptSequence')
            ->with($schema->getSequence('moo'));

        $visitor->expects($this->at(4))
            ->method('acceptSequence')
            ->with($schema->getSequence('war'));

        $visitor->expects($this->exactly(2))
            ->method('acceptSequence');

        self::assertNull($schema->visit($visitor));
    }

    public function testVisitsNamespaceVisitor(): void
    {
        $schema  = new Schema();
        $visitor = $this->createMock(AbstractVisitor::class);

        $schema->createNamespace('foo');
        $schema->createNamespace('bar');

        $schema->createTable('baz');
        $schema->createTable('bla.bloo');

        $schema->createSequence('moo');
        $schema->createSequence('war');

        $visitor->expects($this->once())
            ->method('acceptSchema')
            ->with($schema);

        $visitor->expects($this->at(1))
            ->method('acceptNamespace')
            ->with('foo');

        $visitor->expects($this->at(2))
            ->method('acceptNamespace')
            ->with('bar');

        $visitor->expects($this->at(3))
            ->method('acceptNamespace')
            ->with('bla');

        $visitor->expects($this->exactly(3))
            ->method('acceptNamespace');

        $visitor->expects($this->at(4))
            ->method('acceptTable')
            ->with($schema->getTable('baz'));

        $visitor->expects($this->at(5))
            ->method('acceptTable')
            ->with($schema->getTable('bla.bloo'));

        $visitor->expects($this->exactly(2))
            ->method('acceptTable');

        $visitor->expects($this->at(6))
            ->method('acceptSequence')
            ->with($schema->getSequence('moo'));

        $visitor->expects($this->at(7))
            ->method('acceptSequence')
            ->with($schema->getSequence('war'));

        $visitor->expects($this->exactly(2))
            ->method('acceptSequence');

        self::assertNull($schema->visit($visitor));
    }
}