<?php

namespace MintyPHP\Tests\App\Module;

use MintyPHP\App\Module\ModulePermissionSynchronizer;
use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\Repository\Access\PermissionRepositoryInterface;
use PHPUnit\Framework\TestCase;

final class ModulePermissionSynchronizerTest extends TestCase
{
    private string $fixturesDir = '';

    protected function setUp(): void
    {
        $this->fixturesDir = sys_get_temp_dir() . '/module-permission-sync-' . uniqid();
        mkdir($this->fixturesDir, 0777, true);
    }

    protected function tearDown(): void
    {
        $this->removeDir($this->fixturesDir);
    }

    public function testSyncCreatesPermissionAndIsIdempotent(): void
    {
        $registry = $this->createRegistryWithPermissions([[
            'key' => 'address_book.view',
            'description' => 'Can view address book',
            'active' => 1,
            'is_system' => 1,
        ]]);

        $repository = new InMemoryPermissionRepository();
        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);

        $first = $synchronizer->sync();
        self::assertSame(1, $first['created']);
        self::assertSame(0, $first['updated']);
        self::assertSame(0, $first['unchanged']);

        $second = $synchronizer->sync();
        self::assertSame(0, $second['created']);
        self::assertSame(0, $second['updated']);
        self::assertSame(1, $second['unchanged']);
    }

    public function testSyncDeactivatesOrphanedModulePermissions(): void
    {
        // mod-perm declares perm_a (enabled). mod-disabled declares perm_b (on disk but not enabled).
        // perm_b is in DB as active — should be deactivated.
        $this->writeModuleManifest('mod-disabled', [[
            'key' => 'mod.perm_b',
            'description' => 'Orphaned module permission',
            'active' => true,
            'is_system' => true,
        ]]);

        $registry = $this->createRegistryWithPermissions([[
            'key' => 'mod.perm_a',
            'description' => 'Active permission',
            'active' => 1,
            'is_system' => 1,
        ]]);

        $repository = new InMemoryPermissionRepository();
        $repository->create(['key' => 'mod.perm_a', 'description' => 'Active', 'active' => 1, 'is_system' => 1]);
        $repository->create(['key' => 'mod.perm_b', 'description' => 'Orphaned', 'active' => 1, 'is_system' => 1]);

        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);
        $result = $synchronizer->sync();

        self::assertSame(1, $result['deactivated']);
        self::assertSame(0, (int) ($repository->findByKey('mod.perm_b')['active'] ?? 1));
        self::assertSame(1, (int) ($repository->findByKey('mod.perm_a')['active'] ?? 0));
    }

    public function testSyncDoesNotDeactivateCoreSystemPermissions(): void
    {
        // Core permission (users.view) is is_system=1 but NOT declared by any module on disk
        $registry = $this->createRegistryWithPermissions([[
            'key' => 'mod.perm_a',
            'description' => 'Module permission',
            'active' => 1,
            'is_system' => 1,
        ]]);

        $repository = new InMemoryPermissionRepository();
        $repository->create(['key' => 'mod.perm_a', 'description' => 'Module', 'active' => 1, 'is_system' => 1]);
        $repository->create(['key' => 'users.view', 'description' => 'Core permission', 'active' => 1, 'is_system' => 1]);

        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);
        $result = $synchronizer->sync();

        self::assertSame(0, $result['deactivated']);
        self::assertSame(1, (int) ($repository->findByKey('users.view')['active'] ?? 0), 'Core permission must not be deactivated');
    }

    public function testSyncDoesNotDeactivateAdminCreatedPermissions(): void
    {
        $registry = $this->createRegistryWithPermissions([]);

        $repository = new InMemoryPermissionRepository();
        $repository->create(['key' => 'admin.custom', 'description' => 'Admin-created', 'active' => 1, 'is_system' => 0]);

        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);
        $result = $synchronizer->sync();

        self::assertSame(0, $result['deactivated']);
        self::assertSame(1, (int) ($repository->findByKey('admin.custom')['active'] ?? 0));
    }

    public function testSyncReactivatesPermissionWhenModuleReEnabled(): void
    {
        $registry = $this->createRegistryWithPermissions([[
            'key' => 'mod.perm_a',
            'description' => 'Reactivated permission',
            'active' => 1,
            'is_system' => 1,
        ]]);

        $repository = new InMemoryPermissionRepository();
        $repository->create(['key' => 'mod.perm_a', 'description' => 'Reactivated', 'active' => 0, 'is_system' => 1]);

        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);
        $result = $synchronizer->sync();

        self::assertSame(0, $result['created']);
        self::assertSame(1, $result['updated']);
        self::assertSame(0, $result['deactivated']);
        self::assertSame(1, (int) ($repository->findByKey('mod.perm_a')['active'] ?? 0));
    }

    public function testSyncSkipsAlreadyDeactivatedOrphans(): void
    {
        $this->writeModuleManifest('mod-old', [[
            'key' => 'mod.old_perm',
            'description' => 'Old',
            'active' => true,
            'is_system' => true,
        ]]);

        $registry = $this->createRegistryWithPermissions([]);

        $repository = new InMemoryPermissionRepository();
        $repository->create(['key' => 'mod.old_perm', 'description' => 'Already deactivated', 'active' => 0, 'is_system' => 1]);

        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);
        $result = $synchronizer->sync();

        self::assertSame(0, $result['deactivated']);
    }

    public function testSyncUpdatesExistingPermissionWhenDefinitionChanged(): void
    {
        $registry = $this->createRegistryWithPermissions([[
            'key' => 'address_book.view',
            'description' => 'Can view address book',
            'active' => 1,
            'is_system' => 1,
        ]]);

        $repository = new InMemoryPermissionRepository();
        $repository->create(['key' => 'address_book.view', 'description' => 'Old description', 'active' => 0, 'is_system' => 0]);

        $synchronizer = new ModulePermissionSynchronizer($registry, $repository, $this->fixturesDir);
        $result = $synchronizer->sync();

        self::assertSame(0, $result['created']);
        self::assertSame(1, $result['updated']);
        self::assertSame(0, $result['unchanged']);

        $permission = $repository->findByKey('address_book.view');
        self::assertSame('Can view address book', $permission['description'] ?? null);
        self::assertSame(1, (int) ($permission['active'] ?? 0));
        self::assertSame(1, (int) ($permission['is_system'] ?? 0));
    }

    /**
     * Write a module manifest with the given permissions to a module directory.
     *
     * @param list<array<string, mixed>> $permissions
     */
    private function writeModuleManifest(string $moduleId, array $permissions): void
    {
        $dir = $this->fixturesDir . '/' . $moduleId;
        if (!is_dir($dir)) {
            mkdir($dir, 0777, true);
        }
        file_put_contents(
            $dir . '/module.php',
            '<?php return ' . var_export(['id' => $moduleId, 'permissions' => $permissions], true) . ';'
        );
    }

    /**
     * @param list<array{key: string, description: string, active: int, is_system: int}> $permissions
     */
    private function createRegistryWithPermissions(array $permissions): ModuleRegistry
    {
        if (!is_dir($this->fixturesDir . '/mod-perm')) {
            mkdir($this->fixturesDir . '/mod-perm', 0777, true);
        }
        file_put_contents(
            $this->fixturesDir . '/mod-perm/module.php',
            '<?php return ' . var_export([
                'id' => 'mod-perm',
                'permissions' => $permissions,
            ], true) . ';'
        );

        return ModuleRegistry::boot($this->fixturesDir, ['mod-perm']);
    }

    private function removeDir(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }
        $items = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($items as $item) {
            if ($item->isDir()) {
                rmdir($item->getPathname());
            } else {
                unlink($item->getPathname());
            }
        }
        rmdir($dir);
    }
}

final class InMemoryPermissionRepository implements PermissionRepositoryInterface
{
    /** @var array<int, array<string, mixed>> */
    private array $rows = [];
    private int $nextId = 1;

    public function list(): array
    {
        return array_values($this->rows);
    }

    public function listActive(): array
    {
        return array_values(array_filter($this->rows, static fn (array $row): bool => (int) ($row['active'] ?? 0) === 1));
    }

    public function listPaged(array $options): array
    {
        return ['rows' => $this->list(), 'total' => count($this->rows)];
    }

    public function find(int $id): ?array
    {
        return $this->rows[$id] ?? null;
    }

    public function findByKey(string $key): ?array
    {
        foreach ($this->rows as $row) {
            if ((string) ($row['key'] ?? '') === $key) {
                return $row;
            }
        }
        return null;
    }

    public function create(array $data): int
    {
        $id = $this->nextId++;
        $this->rows[$id] = [
            'id' => $id,
            'key' => (string) ($data['key'] ?? ''),
            'description' => (string) ($data['description'] ?? ''),
            'active' => (int) ($data['active'] ?? 1),
            'is_system' => (int) ($data['is_system'] ?? 1),
        ];
        return $id;
    }

    public function update(int $id, array $data): bool
    {
        if (!isset($this->rows[$id])) {
            return false;
        }
        $this->rows[$id]['key'] = (string) ($data['key'] ?? $this->rows[$id]['key']);
        $this->rows[$id]['description'] = (string) ($data['description'] ?? $this->rows[$id]['description']);
        $this->rows[$id]['active'] = (int) ($data['active'] ?? $this->rows[$id]['active']);
        $this->rows[$id]['is_system'] = (int) ($data['is_system'] ?? $this->rows[$id]['is_system']);
        return true;
    }

    public function delete(int $id): bool
    {
        unset($this->rows[$id]);
        return true;
    }

    public function listSystem(): array
    {
        return array_values(array_filter($this->rows, static fn (array $row): bool => (int) ($row['is_system'] ?? 0) === 1));
    }
}
