<?php

declare(strict_types=1);

namespace NoahVet\Reef\Test\A_Unit\Hasher;

use NoahVet\Reef\Hasher\DirectoryHasher;
use NoahVet\Reef\Hasher\DirectoryHasherInterface;
use PHPUnit\Framework\TestCase;

final class DirectoryHasherTest extends TestCase
{
    private DirectoryHasherInterface $subject;

    private string $kernelProjectDir;

    protected function setUp(): void
    {
        parent::setUp();

        $this->kernelProjectDir = \sys_get_temp_dir().\DIRECTORY_SEPARATOR.'fixtures_hasher_'.\uniqid('', true);

        if (!\is_dir($this->kernelProjectDir) && !\mkdir($concurrentDirectory = $this->kernelProjectDir, 0o777, true) && !\is_dir($concurrentDirectory)) {
            throw new \RuntimeException(\sprintf('Directory "%s" was not created', $this->kernelProjectDir));
        }

        $this->subject = new DirectoryHasher($this->kernelProjectDir);
    }

    protected function tearDown(): void
    {
        $this->removeDirectory($this->kernelProjectDir);

        parent::tearDown();
    }

    public function testComputeHashesThrowsLogicExceptionWhenRelativeDirectoryDirsIsEmpty(): void
    {
        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage('$relativeDirs should not be empty.');

        $this->subject->computeHashes([]);
    }

    public function testComputeHashesReturnsNoPathsWhenDirectoriesDoNotExist(): void
    {
        $result = $this->subject->computeHashes(['default']);

        $this->assertSame('no-paths', $result['fixtures']);
        $this->assertSame('no-paths', $result['fixtureCommands']);
        $this->assertSame('no-paths', $result['migrations']);
        $this->assertSame('no-paths', $result['sync']);
        $this->assertSame('no-paths', $result['syncCommands']);

        $expectedGlobal = \hash(
            'xxh3',
            'fixtures=no-paths'
            .'::fixtureCommandsHash=no-paths'
            .'::migrations=no-paths'
            .'::sync=no-paths'
            .'::syncCommands=no-paths',
        );

        $this->assertSame($expectedGlobal, $result['global']);
    }

    public function testComputeHashesChangesWhenDirectoryFileContentChanges(): void
    {
        // Initial file content.
        $this->createFile('data/fixture/doctrine/default/fixture.yaml', "initial\n");
        $this->createFile('src/DataDirectory/LoadSomething.php', "<?php\n// data fixture\n");
        $this->createFile('migrations/Version20250101000000.php', "<?php\n// migration\n");
        $this->createFile('data/sync/doctrine/sync1.yaml', "sync content\n");
        $this->createFile('src/Command/Sync/SyncSomethingCommand.php', "<?php\n// sync command\n");

        $firstResult = $this->subject->computeHashes(['default']);

        // Change only the fixture file content.
        $this->createFile('data/fixture/doctrine/default/fixture.yaml', "modified\n");

        $secondResult = $this->subject->computeHashes(['default']);

        $this->assertNotSame($firstResult['fixtures'], $secondResult['fixtures']);
        $this->assertNotSame($firstResult['global'], $secondResult['global']);

        // Other sections should remain the same as their underlying files did not change.
        $this->assertSame($firstResult['fixtureCommands'], $secondResult['fixtureCommands']);
        $this->assertSame($firstResult['migrations'], $secondResult['migrations']);
        $this->assertSame($firstResult['sync'], $secondResult['sync']);
        $this->assertSame($firstResult['syncCommands'], $secondResult['syncCommands']);
    }

    /**
     * Create a directory relative to the kernel project dir.
     */
    private function createDirectory(string $relativePath): string
    {
        $relativePath = \ltrim($relativePath, '/\\');
        $path = $this->kernelProjectDir.\DIRECTORY_SEPARATOR.\str_replace(['/', '\\'], \DIRECTORY_SEPARATOR, $relativePath);

        if (!\is_dir($path) && !\mkdir($concurrentDirectory = $path, 0o777, true) && !\is_dir($concurrentDirectory)) {
            throw new \RuntimeException(\sprintf('Directory "%s" was not created', $path));
        }

        return $path;
    }

    /**
     * Create a file relative to the kernel project dir.
     */
    private function createFile(string $relativePath, string $contents): string
    {
        $relativePath = \ltrim($relativePath, '/\\');
        $normalizedRelativePath = \str_replace(['/', '\\'], \DIRECTORY_SEPARATOR, $relativePath);

        $directory = \dirname($normalizedRelativePath);
        if ('.' !== $directory) {
            $this->createDirectory($directory);
        }

        $fullPath = $this->kernelProjectDir.\DIRECTORY_SEPARATOR.$normalizedRelativePath;

        if (false === \file_put_contents($fullPath, $contents)) {
            throw new \RuntimeException(\sprintf('File "%s" could not be written', $fullPath));
        }

        return $fullPath;
    }

    /**
     * Recursively remove a directory.
     */
    private function removeDirectory(string $directory): void
    {
        if (!\is_dir($directory)) {
            return;
        }

        $items = \scandir($directory);
        if (false === $items) {
            return;
        }

        foreach ($items as $item) {
            if ('.' === $item || '..' === $item) {
                continue;
            }

            $path = $directory.\DIRECTORY_SEPARATOR.$item;

            if (\is_dir($path)) {
                $this->removeDirectory($path);
            } else {
                @\unlink($path);
            }
        }

        @\rmdir($directory);
    }
}
