<?php

declare(strict_types=1);

namespace Tests\Framework\Database\Schema\Comparison;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ResultInterface;
use App\Framework\Database\Schema\Comparison\SchemaComparator;
use App\Framework\Database\Schema\Comparison\SchemaDifference;
use Mockery;

beforeEach(function () {
    $this->sourceConnection = Mockery::mock(ConnectionInterface::class);
    $this->targetConnection = Mockery::mock(ConnectionInterface::class);
    $this->comparator = new SchemaComparator($this->sourceConnection, $this->targetConnection);
});

afterEach(function () {
    Mockery::close();
});

test('compares schemas with different tables', function () {
    // Mock source tables
    $sourceTables = Mockery::mock(ResultInterface::class);
    $sourceTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
        ['table_name' => 'posts'],
        ['table_name' => 'comments'],
    ]);

    // Mock target tables
    $targetTables = Mockery::mock(ResultInterface::class);
    $targetTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
        ['table_name' => 'posts'],
        ['table_name' => 'categories'], // Different table
    ]);

    // Mock table structure queries
    $this->sourceConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->targetConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->sourceConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($sourceTables);

    $this->targetConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($targetTables);

    // Mock column queries for each table
    $this->mockTableStructure('users', true, true);
    $this->mockTableStructure('posts', true, true);
    $this->mockTableStructure('comments', true, false);
    $this->mockTableStructure('categories', false, true);

    $difference = $this->comparator->compare();

    expect($difference)->toBeInstanceOf(SchemaDifference::class);
    expect($difference->hasDifferences())->toBeTrue();
    expect($difference->missingTables)->toHaveCount(1);
    expect($difference->extraTables)->toHaveCount(1);
    expect(array_keys($difference->missingTables))->toBe(['comments']);
    expect(array_keys($difference->extraTables))->toBe(['categories']);
});

test('compares schemas with identical tables', function () {
    // Mock source tables
    $sourceTables = Mockery::mock(ResultInterface::class);
    $sourceTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
        ['table_name' => 'posts'],
    ]);

    // Mock target tables
    $targetTables = Mockery::mock(ResultInterface::class);
    $targetTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
        ['table_name' => 'posts'],
    ]);

    // Mock table structure queries
    $this->sourceConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->targetConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->sourceConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($sourceTables);

    $this->targetConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($targetTables);

    // Mock identical column structure for each table
    $this->mockIdenticalTableStructure('users');
    $this->mockIdenticalTableStructure('posts');

    $difference = $this->comparator->compare();

    expect($difference)->toBeInstanceOf(SchemaDifference::class);
    expect($difference->hasDifferences())->toBeFalse();
    expect($difference->missingTables)->toBeEmpty();
    expect($difference->extraTables)->toBeEmpty();
    expect($difference->tableDifferences)->toBeEmpty();
});

test('compares schemas with different column definitions', function () {
    // Mock source tables
    $sourceTables = Mockery::mock(ResultInterface::class);
    $sourceTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
    ]);

    // Mock target tables
    $targetTables = Mockery::mock(ResultInterface::class);
    $targetTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
    ]);

    // Mock table structure queries
    $this->sourceConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->targetConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->sourceConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($sourceTables);

    $this->targetConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($targetTables);

    // Mock source columns
    $sourceColumns = Mockery::mock(ResultInterface::class);
    $sourceColumns->shouldReceive('fetchAll')->andReturn([
        [
            'column_name' => 'id',
            'data_type' => 'integer',
            'is_nullable' => 'NO',
            'column_default' => 'nextval(\'users_id_seq\'::regclass)',
        ],
        [
            'column_name' => 'name',
            'data_type' => 'character varying',
            'is_nullable' => 'NO',
            'column_default' => null,
        ],
        [
            'column_name' => 'email',
            'data_type' => 'character varying',
            'is_nullable' => 'NO',
            'column_default' => null,
        ],
    ]);

    // Mock target columns with differences
    $targetColumns = Mockery::mock(ResultInterface::class);
    $targetColumns->shouldReceive('fetchAll')->andReturn([
        [
            'column_name' => 'id',
            'data_type' => 'integer',
            'is_nullable' => 'NO',
            'column_default' => 'nextval(\'users_id_seq\'::regclass)',
        ],
        [
            'column_name' => 'name',
            'data_type' => 'character varying',
            'is_nullable' => 'YES', // Changed to nullable
            'column_default' => null,
        ],
        [
            'column_name' => 'username', // Different column
            'data_type' => 'character varying',
            'is_nullable' => 'NO',
            'column_default' => null,
        ],
    ]);

    $this->sourceConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.columns.*users/'), [])
        ->andReturn($sourceColumns);

    $this->targetConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.columns.*users/'), [])
        ->andReturn($targetColumns);

    // Mock empty indexes and foreign keys
    $this->mockEmptyIndexes('users');
    $this->mockEmptyForeignKeys('users');

    $difference = $this->comparator->compare();

    expect($difference)->toBeInstanceOf(SchemaDifference::class);
    expect($difference->hasDifferences())->toBeTrue();
    expect($difference->tableDifferences)->toHaveCount(1);
    expect($difference->tableDifferences['users']->missingColumns)->toHaveKey('email');
    expect($difference->tableDifferences['users']->extraColumns)->toHaveKey('username');
    expect($difference->tableDifferences['users']->modifiedColumns)->toHaveKey('name');
});

test('compares schemas with different indexes', function () {
    // Mock source tables
    $sourceTables = Mockery::mock(ResultInterface::class);
    $sourceTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
    ]);

    // Mock target tables
    $targetTables = Mockery::mock(ResultInterface::class);
    $targetTables->shouldReceive('fetchAll')->andReturn([
        ['table_name' => 'users'],
    ]);

    // Mock table structure queries
    $this->sourceConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->targetConnection->shouldReceive('getPdo->getAttribute')
        ->with(\PDO::ATTR_DRIVER_NAME)
        ->andReturn('pgsql');

    $this->sourceConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($sourceTables);

    $this->targetConnection->shouldReceive('query')
        ->with(Mockery::pattern('/information_schema.tables/'), [])
        ->andReturn($targetTables);

    // Mock identical columns
    $this->mockIdenticalColumns('users');

    // Mock source indexes
    $sourceIndexes = Mockery::mock(ResultInterface::class);
    $sourceIndexes->shouldReceive('fetchAll')->andReturn([
        [
            'indexname' => 'users_pkey',
            'indexdef' => 'CREATE UNIQUE INDEX users_pkey ON users USING btree (id)',
        ],
        [
            'indexname' => 'users_email_idx',
            'indexdef' => 'CREATE UNIQUE INDEX users_email_idx ON users USING btree (email)',
        ],
    ]);

    // Mock target indexes with differences
    $targetIndexes = Mockery::mock(ResultInterface::class);
    $targetIndexes->shouldReceive('fetchAll')->andReturn([
        [
            'indexname' => 'users_pkey',
            'indexdef' => 'CREATE UNIQUE INDEX users_pkey ON users USING btree (id)',
        ],
        [
            'indexname' => 'users_name_idx', // Different index
            'indexdef' => 'CREATE INDEX users_name_idx ON users USING btree (name)',
        ],
    ]);

    $this->sourceConnection->shouldReceive('query')
        ->with(Mockery::pattern('/pg_indexes.*users/'), [])
        ->andReturn($sourceIndexes);

    $this->targetConnection->shouldReceive('query')
        ->with(Mockery::pattern('/pg_indexes.*users/'), [])
        ->andReturn($targetIndexes);

    // Mock empty foreign keys
    $this->mockEmptyForeignKeys('users');

    $difference = $this->comparator->compare();

    expect($difference)->toBeInstanceOf(SchemaDifference::class);
    expect($difference->hasDifferences())->toBeTrue();
    expect($difference->tableDifferences)->toHaveCount(1);
    expect($difference->tableDifferences['users']->missingIndexes)->toHaveKey('users_email_idx');
    expect($difference->tableDifferences['users']->extraIndexes)->toHaveKey('users_name_idx');
});

// Helper functions for tests
beforeEach(function () {
    // Define helper methods in the test context
    $this->mockTableStructure = function (string $tableName, bool $inSource, bool $inTarget): void {
        if ($inSource) {
            $sourceColumns = Mockery::mock(ResultInterface::class);
            $sourceColumns->shouldReceive('fetchAll')->andReturn([
                [
                    'column_name' => 'id',
                    'data_type' => 'integer',
                    'is_nullable' => 'NO',
                    'column_default' => 'nextval(\'' . $tableName . '_id_seq\'::regclass)',
                ],
                [
                    'column_name' => 'name',
                    'data_type' => 'character varying',
                    'is_nullable' => 'NO',
                    'column_default' => null,
                ],
            ]);

            $this->sourceConnection->shouldReceive('query')
                ->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
                ->andReturn($sourceColumns);

            $this->mockEmptyIndexes($tableName, true, false);
            $this->mockEmptyForeignKeys($tableName, true, false);
        }

        if ($inTarget) {
            $targetColumns = Mockery::mock(ResultInterface::class);
            $targetColumns->shouldReceive('fetchAll')->andReturn([
                [
                    'column_name' => 'id',
                    'data_type' => 'integer',
                    'is_nullable' => 'NO',
                    'column_default' => 'nextval(\'' . $tableName . '_id_seq\'::regclass)',
                ],
                [
                    'column_name' => 'name',
                    'data_type' => 'character varying',
                    'is_nullable' => 'NO',
                    'column_default' => null,
                ],
            ]);

            $this->targetConnection->shouldReceive('query')
                ->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
                ->andReturn($targetColumns);

            $this->mockEmptyIndexes($tableName, false, true);
            $this->mockEmptyForeignKeys($tableName, false, true);
        }
    };

    $this->mockIdenticalTableStructure = function (string $tableName): void {
        $this->mockIdenticalColumns($tableName);
        $this->mockEmptyIndexes($tableName);
        $this->mockEmptyForeignKeys($tableName);
    };

    $this->mockIdenticalColumns = function (string $tableName): void {
        $columns = Mockery::mock(ResultInterface::class);
        $columns->shouldReceive('fetchAll')->andReturn([
            [
                'column_name' => 'id',
                'data_type' => 'integer',
                'is_nullable' => 'NO',
                'column_default' => 'nextval(\'' . $tableName . '_id_seq\'::regclass)',
            ],
            [
                'column_name' => 'name',
                'data_type' => 'character varying',
                'is_nullable' => 'NO',
                'column_default' => null,
            ],
        ]);

        $this->sourceConnection->shouldReceive('query')
            ->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
            ->andReturn($columns);

        $this->targetConnection->shouldReceive('query')
            ->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
            ->andReturn($columns);
    };

    $this->mockEmptyIndexes = function (string $tableName, bool $source = true, bool $target = true): void {
        $emptyIndexes = Mockery::mock(ResultInterface::class);
        $emptyIndexes->shouldReceive('fetchAll')->andReturn([]);

        if ($source) {
            $this->sourceConnection->shouldReceive('query')
                ->with(Mockery::pattern('/pg_indexes.*' . $tableName . '/'), [])
                ->andReturn($emptyIndexes);
        }

        if ($target) {
            $this->targetConnection->shouldReceive('query')
                ->with(Mockery::pattern('/pg_indexes.*' . $tableName . '/'), [])
                ->andReturn($emptyIndexes);
        }
    };

    $this->mockEmptyForeignKeys = function (string $tableName, bool $source = true, bool $target = true): void {
        $emptyForeignKeys = Mockery::mock(ResultInterface::class);
        $emptyForeignKeys->shouldReceive('fetchAll')->andReturn([]);

        if ($source) {
            $this->sourceConnection->shouldReceive('query')
                ->with(Mockery::pattern('/information_schema.table_constraints.*' . $tableName . '/'), [])
                ->andReturn($emptyForeignKeys);
        }

        if ($target) {
            $this->targetConnection->shouldReceive('query')
                ->with(Mockery::pattern('/information_schema.table_constraints.*' . $tableName . '/'), [])
                ->andReturn($emptyForeignKeys);
        }
    };
});
