<?php

namespace MintyPHP\Tests\App\Module;

use MintyPHP\App\Module\ModuleManifest;
use MintyPHP\App\Module\ModuleRuntimePageBuilder;
use PHPUnit\Framework\TestCase;
use RuntimeException;

final class ModuleRuntimePageBuilderTest extends TestCase
{
    private string $tmpDir;

    protected function setUp(): void
    {
        $this->tmpDir = sys_get_temp_dir() . '/corecore-page-builder-test-' . uniqid();
        mkdir($this->tmpDir, 0777, true);
    }

    protected function tearDown(): void
    {
        $this->removePath($this->tmpDir);
    }

    // ── Atomic build / swap ─────────────────────────────────────────

    public function testBuildCreatesRuntimeDirWithCoreAndModuleSymlinks(): void
    {
        $coreDir = $this->createCorePages(['admin', 'login']);
        $moduleA = $this->createModuleWithPages('mod-a', ['mod-a-page']);
        $runtimeDir = $this->tmpDir . '/runtime/pages';

        $builder = new ModuleRuntimePageBuilder();
        $result = $builder->build($runtimeDir, $coreDir, ['mod-a' => $moduleA]);

        self::assertSame(2, $result['core_entries']);
        self::assertSame(1, $result['module_entries']);
        self::assertDirectoryExists($runtimeDir);
        self::assertTrue(is_link($runtimeDir . '/admin'));
        self::assertTrue(is_link($runtimeDir . '/login'));
        self::assertTrue(is_link($runtimeDir . '/mod-a-page'));
    }

    public function testBuildWithNoModulesCreatesSymlinkToCore(): void
    {
        $coreDir = $this->createCorePages(['admin']);
        $runtimeDir = $this->tmpDir . '/runtime/pages';
        mkdir(dirname($runtimeDir), 0777, true);

        $builder = new ModuleRuntimePageBuilder();
        $result = $builder->build($runtimeDir, $coreDir, []);

        self::assertSame(0, $result['core_entries']);
        self::assertSame(0, $result['module_entries']);
        self::assertTrue(is_link($runtimeDir));
    }

    public function testBuildFailureDoesNotLeavePartialRuntimeState(): void
    {
        $coreDir = $this->createCorePages(['shared-page']);
        // Module has a page that collides with core — will cause RuntimeException
        $moduleA = $this->createModuleWithPages('mod-a', ['shared-page']);
        $runtimeDir = $this->tmpDir . '/runtime/pages';

        // Pre-populate a valid runtime dir to ensure it survives the failed build
        mkdir($runtimeDir, 0777, true);
        file_put_contents($runtimeDir . '/sentinel.txt', 'existing');

        $builder = new ModuleRuntimePageBuilder();

        try {
            $builder->build($runtimeDir, $coreDir, ['mod-a' => $moduleA]);
            self::fail('Expected RuntimeException for page path collision');
        } catch (RuntimeException $e) {
            self::assertStringContainsString('collision', $e->getMessage());
        }

        // The existing runtime dir must still be intact — atomic build
        // should not have corrupted it
        self::assertDirectoryExists($runtimeDir);
        self::assertFileExists($runtimeDir . '/sentinel.txt');
        self::assertSame('existing', file_get_contents($runtimeDir . '/sentinel.txt'));
    }

    public function testBuildReplacesExistingRuntimeDir(): void
    {
        $coreDir = $this->createCorePages(['admin']);
        $moduleA = $this->createModuleWithPages('mod-a', ['mod-page']);
        $runtimeDir = $this->tmpDir . '/runtime/pages';

        // First build
        $builder = new ModuleRuntimePageBuilder();
        $builder->build($runtimeDir, $coreDir, ['mod-a' => $moduleA]);
        self::assertTrue(is_link($runtimeDir . '/mod-page'));

        // Second build with different module
        $moduleB = $this->createModuleWithPages('mod-b', ['mod-b-page']);
        $result = $builder->build($runtimeDir, $coreDir, ['mod-b' => $moduleB]);

        self::assertSame(1, $result['core_entries']);
        self::assertSame(1, $result['module_entries']);
        self::assertTrue(is_link($runtimeDir . '/mod-b-page'));
        // Old module page should be gone
        self::assertFalse(is_link($runtimeDir . '/mod-page'));
    }

    public function testNoStagingLeftoverOnSuccess(): void
    {
        $coreDir = $this->createCorePages(['admin']);
        $moduleA = $this->createModuleWithPages('mod-a', ['mod-page']);
        $runtimeDir = $this->tmpDir . '/runtime/pages';

        $builder = new ModuleRuntimePageBuilder();
        $builder->build($runtimeDir, $coreDir, ['mod-a' => $moduleA]);

        // Staging dir should be cleaned up
        $parentDir = dirname($runtimeDir);
        $entries = scandir($parentDir);
        self::assertNotFalse($entries);
        $stagingEntries = array_filter(
            $entries,
            static fn (string $e): bool => str_starts_with($e, '.pages-staging-'),
        );
        self::assertEmpty($stagingEntries, 'Staging directory should be removed after successful build');
    }

    // ── Fingerprint ─────────────────────────────────────────────────

    public function testFingerprintChangesWhenManifestContentChanges(): void
    {
        $moduleDir = $this->tmpDir . '/modules/mod-a';
        mkdir($moduleDir, 0777, true);
        file_put_contents($moduleDir . '/module.php', '<?php return ["id" => "mod-a"];');

        $manifest = ModuleManifest::fromArray(['id' => 'mod-a'], $moduleDir);
        $fp1 = ModuleRuntimePageBuilder::expectedFingerprint(['mod-a' => $manifest]);

        // Change manifest content
        file_put_contents($moduleDir . '/module.php', '<?php return ["id" => "mod-a", "version" => "2.0.0"];');

        $fp2 = ModuleRuntimePageBuilder::expectedFingerprint(['mod-a' => $manifest]);

        self::assertNotSame($fp1, $fp2, 'Fingerprint must change when manifest file content changes');
    }

    public function testFingerprintChangesWhenPageEntriesChange(): void
    {
        $moduleDir = $this->tmpDir . '/modules/mod-a';
        mkdir($moduleDir . '/pages', 0777, true);
        file_put_contents($moduleDir . '/module.php', '<?php return ["id" => "mod-a"];');
        file_put_contents($moduleDir . '/pages/page-a.php', '<?php');

        $manifest = ModuleManifest::fromArray(['id' => 'mod-a'], $moduleDir);
        $fp1 = ModuleRuntimePageBuilder::expectedFingerprint(['mod-a' => $manifest]);

        // Add a new page entry
        file_put_contents($moduleDir . '/pages/page-b.php', '<?php');

        $fp2 = ModuleRuntimePageBuilder::expectedFingerprint(['mod-a' => $manifest]);

        self::assertNotSame($fp1, $fp2, 'Fingerprint must change when page entries change');
    }

    public function testFingerprintStableForSameInputs(): void
    {
        $moduleDir = $this->tmpDir . '/modules/mod-a';
        mkdir($moduleDir . '/pages', 0777, true);
        file_put_contents($moduleDir . '/module.php', '<?php return ["id" => "mod-a"];');
        file_put_contents($moduleDir . '/pages/page-a.php', '<?php');

        $manifest = ModuleManifest::fromArray(['id' => 'mod-a'], $moduleDir);
        $fp1 = ModuleRuntimePageBuilder::expectedFingerprint(['mod-a' => $manifest]);
        $fp2 = ModuleRuntimePageBuilder::expectedFingerprint(['mod-a' => $manifest]);

        self::assertSame($fp1, $fp2, 'Fingerprint must be deterministic for same inputs');
    }

    public function testFingerprintEmptyForNoModules(): void
    {
        $fp = ModuleRuntimePageBuilder::expectedFingerprint([]);
        self::assertSame('', $fp);
    }

    public function testWriteAndReadFingerprintRoundtrip(): void
    {
        $runtimeDir = $this->tmpDir . '/runtime/pages';
        mkdir(dirname($runtimeDir), 0777, true);

        $moduleDir = $this->tmpDir . '/modules/mod-a';
        mkdir($moduleDir, 0777, true);
        file_put_contents($moduleDir . '/module.php', '<?php return ["id" => "mod-a"];');

        $manifest = ModuleManifest::fromArray(['id' => 'mod-a'], $moduleDir);
        $modules = ['mod-a' => $manifest];

        ModuleRuntimePageBuilder::writeFingerprint($runtimeDir, $modules);

        $expected = ModuleRuntimePageBuilder::expectedFingerprint($modules);
        $actual = ModuleRuntimePageBuilder::readFingerprint($runtimeDir);

        self::assertSame($expected, $actual);
    }

    public function testReadFingerprintReturnsEmptyWhenFileDoesNotExist(): void
    {
        $runtimeDir = $this->tmpDir . '/runtime/pages';
        self::assertSame('', ModuleRuntimePageBuilder::readFingerprint($runtimeDir));
    }

    // ── Helpers ──────────────────────────────────────────────────────

    private function createCorePages(array $entries): string
    {
        $coreDir = $this->tmpDir . '/pages';
        mkdir($coreDir, 0777, true);
        foreach ($entries as $entry) {
            mkdir($coreDir . '/' . $entry, 0777, true);
        }
        return $coreDir;
    }

    private function createModuleWithPages(string $id, array $pageEntries): ModuleManifest
    {
        $moduleDir = $this->tmpDir . '/modules/' . $id;
        mkdir($moduleDir . '/pages', 0777, true);
        file_put_contents($moduleDir . '/module.php', '<?php return ' . var_export(['id' => $id], true) . ';');
        foreach ($pageEntries as $entry) {
            mkdir($moduleDir . '/pages/' . $entry, 0777, true);
        }
        return ModuleManifest::fromArray(['id' => $id], $moduleDir);
    }

    private function removePath(string $path): void
    {
        if (!file_exists($path) && !is_link($path)) {
            return;
        }
        if (is_link($path) || is_file($path)) {
            @unlink($path);
            return;
        }
        if (!is_dir($path)) {
            return;
        }
        $entries = scandir($path);
        if ($entries !== false) {
            foreach ($entries as $entry) {
                if ($entry === '.' || $entry === '..') {
                    continue;
                }
                $this->removePath($path . '/' . $entry);
            }
        }
        @rmdir($path);
    }
}
