<?php

declare(strict_types=1);

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Migration\MigrationCollection;
use App\Framework\Database\Migration\MigrationRunner;
use App\Framework\Database\Platform\MySqlPlatform;
use App\Framework\Database\ResultInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\DateTime\SystemClock;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\OperationTracker;

beforeEach(function () {
    $this->connection = new TestConnection();
    $this->platform = new MySqlPlatform();
    $this->clock = new SystemClock();
    $this->memoryMonitor = new MemoryMonitor();
    $this->operationTracker = new OperationTracker();

    $this->migrationRunner = new MigrationRunner(
        $this->connection,
        $this->platform,
        $this->clock,
        null, // tableConfig
        null, // logger
        $this->operationTracker,
        $this->memoryMonitor
    );
});

test('constructor creates migrations table', function () {
    // Constructor should have already been called in beforeEach
    $queries = $this->connection->getQueries();

    expect($queries)->toHaveCount(1)
        ->and($queries[0]['type'])->toBe('execute')
        ->and($queries[0]['sql'])->toContain('CREATE TABLE IF NOT EXISTS test_migrations');
});

test('migrate runs pending migrations', function () {
    // Mock migration
    $migration = new TestMigration();
    $migrations = new MigrationCollection($migration);

    // Set no applied migrations initially
    $this->connection->setAppliedMigrations([]);

    $result = $this->migrationRunner->migrate($migrations);

    expect($result)->toContain('2024_01_01_000000');
    expect($migration->wasExecuted())->toBeTrue();

    // Verify the migration was recorded
    $queries = $this->connection->getQueries();
    $insertQueries = array_filter(
        $queries,
        fn ($q) =>
        $q['type'] === 'execute' && str_contains($q['sql'], 'INSERT INTO')
    );
    expect($insertQueries)->toHaveCount(1);
});

test('migrate skips already applied migrations', function () {
    $migration = new TestMigration();
    $migrationData = (object) [
        'version' => '2024_01_01_000000',
        'description' => 'Test Migration',
        'instance' => $migration,
    ];

    // Set migration as already applied
    $this->connection->setAppliedMigrations([['version' => '2024_01_01_000000']]);

    $result = $this->migrationRunner->migrate([$migrationData]);

    expect($result)->toBeEmpty();
    expect($migration->wasExecuted())->toBeFalse();
});

test('migrate rolls back on failure', function () {
    // Mock failing migration
    $failingMigration = new FailingTestMigration();
    $migrationData = (object) [
        'version' => '2024_01_01_000000',
        'description' => 'Failing Migration',
        'instance' => $failingMigration,
    ];

    // Set no applied migrations initially
    $this->connection->setAppliedMigrations([]);
    $this->connection->setShouldFail(true);

    expect(fn () => $this->migrationRunner->migrate([$migrationData]))
        ->toThrow(DatabaseException::class);

    // Verify transaction was used (inTransaction was called)
    expect($this->connection->inTransaction())->toBeFalse(); // Should be rolled back
});

test('rollback reverts applied migration', function () {
    $migration = new TestMigration();
    $migrationData = (object) [
        'version' => '2024_01_01_000000',
        'description' => 'Test Migration',
        'instance' => $migration,
    ];

    // Set migration as already applied
    $this->connection->setAppliedMigrations([['version' => '2024_01_01_000000']]);

    $result = $this->migrationRunner->rollback([$migrationData], 1);

    expect($result)->toContain('2024_01_01_000000');
    expect($migration->wasRolledBack())->toBeTrue();

    // Verify the migration record was deleted
    $queries = $this->connection->getQueries();
    $deleteQueries = array_filter(
        $queries,
        fn ($q) =>
        $q['type'] === 'execute' && str_contains($q['sql'], 'DELETE FROM test_migrations')
    );
    expect($deleteQueries)->toHaveCount(1);
});

test('get status returns migration status', function () {
    $migration1Data = (object) [
        'version' => '2024_01_01_000000',
        'description' => 'Applied Migration',
        'instance' => new TestMigration(),
    ];
    $migration2Data = (object) [
        'version' => '2024_01_02_000000',
        'description' => 'Pending Migration',
        'instance' => new TestMigration(),
    ];

    // Set only first migration as applied
    $this->connection->setAppliedMigrations([['version' => '2024_01_01_000000']]);

    $status = $this->migrationRunner->getStatus([$migration1Data, $migration2Data]);

    expect($status)->toHaveCount(2)
        ->and($status[0]['applied'])->toBeTrue()
        ->and($status[1]['applied'])->toBeFalse();
});

// Test fixtures
class TestMigration implements Migration
{
    private bool $executed = false;

    private bool $rolledBack = false;

    public function up(ConnectionInterface $connection): void
    {
        $this->executed = true;
        // Simulate migration execution
        $connection->execute('CREATE TABLE test_table (id INT)');
    }

    public function down(ConnectionInterface $connection): void
    {
        $this->rolledBack = true;
        // Simulate migration rollback
        $connection->execute('DROP TABLE test_table');
    }

    public function getDescription(): string
    {
        return 'Test Migration';
    }

    public function getVersion(): \App\Framework\Database\Migration\MigrationVersion
    {
        return \App\Framework\Database\Migration\MigrationVersion::fromTimestamp('2024_01_01_000000');
    }

    public function wasExecuted(): bool
    {
        return $this->executed;
    }

    public function wasRolledBack(): bool
    {
        return $this->rolledBack;
    }
}

class FailingTestMigration implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        throw new \Exception('Migration failed');
    }

    public function down(ConnectionInterface $connection): void
    {
        // No-op
    }

    public function getDescription(): string
    {
        return 'Failing Migration';
    }

    public function getVersion(): \App\Framework\Database\Migration\MigrationVersion
    {
        return \App\Framework\Database\Migration\MigrationVersion::fromTimestamp('2024_01_01_000000');
    }
}

class TestConnection implements ConnectionInterface
{
    private array $queries = [];

    private array $appliedMigrations = [];

    private bool $inTransaction = false;

    private bool $shouldFail = false;

    public function setAppliedMigrations(array $migrations): void
    {
        $this->appliedMigrations = $migrations;
    }

    public function setShouldFail(bool $fail): void
    {
        $this->shouldFail = $fail;
    }

    public function execute(SqlQuery $query): int
    {
        $this->queries[] = ['type' => 'execute', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];

        if ($this->shouldFail && strpos($query->sql, 'INSERT INTO') !== false) {
            throw new DatabaseException('Simulated database failure');
        }

        return 1;
    }

    public function query(SqlQuery $query): ResultInterface
    {
        $this->queries[] = ['type' => 'query', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];

        return new TestResult($this->appliedMigrations);
    }

    public function queryOne(SqlQuery $query): ?array
    {
        return null;
    }

    public function queryColumn(SqlQuery $query): array
    {
        $this->queries[] = ['type' => 'queryColumn', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];

        // Return the versions from applied migrations
        return array_column($this->appliedMigrations, 'version');
    }

    public function queryScalar(SqlQuery $query): mixed
    {
        return null;
    }

    public function beginTransaction(): void
    {
        $this->inTransaction = true;
    }

    public function commit(): void
    {
        $this->inTransaction = false;
    }

    public function rollback(): void
    {
        $this->inTransaction = false;
    }

    public function inTransaction(): bool
    {
        return $this->inTransaction;
    }

    public function lastInsertId(): string
    {
        return '1';
    }

    public function getPdo(): \PDO
    {
        return new class () extends \PDO {
            public function __construct()
            {
                // Skip parent constructor to avoid actual DB connection
            }

            public function getAttribute(int $attribute): mixed
            {
                if ($attribute === \PDO::ATTR_DRIVER_NAME) {
                    return 'sqlite';
                }

                return null;
            }
        };
    }

    public function getQueries(): array
    {
        return $this->queries;
    }
}

class TestResult implements ResultInterface
{
    private array $data;

    private int $position = 0;

    public function __construct(array $data = [])
    {
        $this->data = $data;
    }

    public function fetch(): ?array
    {
        if ($this->position >= count($this->data)) {
            return null;
        }

        return $this->data[$this->position++];
    }

    public function fetchAll(): array
    {
        return $this->data;
    }

    public function fetchColumn(int $column = 0): array
    {
        return array_column($this->data, $column);
    }

    public function fetchScalar(): mixed
    {
        $row = $this->fetch();

        return $row ? reset($row) : null;
    }

    public function rowCount(): int
    {
        return count($this->data);
    }

    public function getIterator(): \Iterator
    {
        return new \ArrayIterator($this->data);
    }

    public function count(): int
    {
        return count($this->data);
    }
}
