<?php

namespace NoahVet\Reef\Test\A_Unit\Command\DataFixtures;

use NoahVet\Reef\Command\DataFixtures\SortDataFixtureCommand;
use NoahVet\Reef\Command\DataFixtures\SortDataFixtureCommandInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Yaml\Yaml;

class SortDataFixtureCommandTest extends TestCase
{
    private SortDataFixtureCommandInterface $subject;

    private string $tempDir;

    private string $fixturesDir;

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

        $this->subject = new SortDataFixtureCommand();

        $this->tempDir = \sys_get_temp_dir()
            .'/sort_data_fixture_'
            .\uniqid('', true);

        \mkdir($this->tempDir, 0o777, true);

        $this->fixturesDir = __DIR__.'/fixtures';
    }

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

        $this->removeDir($this->tempDir);
    }

    public function testExecuteReturnsFailureWhenPathDoesNotExist(): void
    {
        $commandTester = new CommandTester($this->subject);

        $statusCode = $commandTester->execute([
            'path' => $this->tempDir.'/does_not_exist',
        ]);

        self::assertSame(
            Command::FAILURE,
            $statusCode,
        );

        $display = $commandTester->getDisplay();

        self::assertStringContainsString(
            'does not exist',
            $display,
        );
    }

    public function testExecuteSortsYamlFileKeysAndFormatsLists(): void
    {
        $filePath = $this->copyFixtureFile('input/sort_keys.yaml');
        $expectedPath = $this->getFixturePath('expected/sort_keys.yaml');

        $commandTester = new CommandTester($this->subject);

        $statusCode = $commandTester->execute([
            'path' => $filePath,
        ]);

        self::assertSame(
            Command::SUCCESS,
            $statusCode,
        );

        self::assertFileEquals(
            $expectedPath,
            $filePath,
        );

        $parsed = Yaml::parseFile($filePath);

        self::assertIsArray($parsed);
        self::assertArrayHasKey('customer', $parsed);
        self::assertIsArray($parsed['customer']);

        foreach ($parsed['customer'] as $index => $customer) {
            self::assertIsArray($customer);

            $keys = \array_keys($customer);

            self::assertSame(
                'id',
                $keys[0],
                \sprintf('First key of customer[%d] should be "id"', $index),
            );

            $lastKey = $keys[\array_key_last($keys)];

            self::assertSame(
                '_ref',
                $lastKey,
                \sprintf('Last key of customer[%d] should be "_ref"', $index),
            );
        }
    }

    public function testExecuteDoesNotModifyFileInDryRunMode(): void
    {
        $filePath = $this->copyFixtureFile('input/dry_run.yaml');

        $originalContent = \file_get_contents($filePath);

        $commandTester = new CommandTester($this->subject);

        $statusCode = $commandTester->execute([
            'path' => $filePath,
            '--dry-run' => true,
        ]);

        self::assertSame(
            Command::SUCCESS,
            $statusCode,
        );

        $result = \file_get_contents($filePath);

        self::assertSame(
            $originalContent,
            $result,
            'File should not be modified in dry-run mode',
        );

        $display = $commandTester->getDisplay();

        self::assertStringContainsString(
            '[DRY-RUN]',
            $display,
        );
    }

    public function testExecuteProcessesDirectoryRecursively(): void
    {
        $directory = $this->copyFixtureDirectory('directory/input');
        $file1 = $directory.'/dir1/file1.yaml';
        $file2 = $directory.'/dir1/subdir/file2.yml';

        $commandTester = new CommandTester($this->subject);

        $statusCode = $commandTester->execute([
            'path' => $directory,
        ]);

        self::assertSame(
            Command::SUCCESS,
            $statusCode,
        );

        self::assertFileEquals(
            $this->getFixturePath('directory/expected/dir1/file1.yaml'),
            $file1,
        );

        self::assertFileEquals(
            $this->getFixturePath('directory/expected/dir1/subdir/file2.yml'),
            $file2,
        );
    }

    public function testRefIsAlwaysSortedLastEvenWithOtherUnderscoreKeys(): void
    {
        $filePath = $this->copyFixtureFile('input/underscore_keys.yaml');
        $expectedPath = $this->getFixturePath('expected/underscore_keys.yaml');

        $commandTester = new CommandTester($this->subject);

        $statusCode = $commandTester->execute([
            'path' => $filePath,
        ]);

        self::assertSame(
            Command::SUCCESS,
            $statusCode,
        );

        self::assertFileEquals(
            $expectedPath,
            $filePath,
        );

        $parsed = Yaml::parseFile($filePath);

        self::assertSame(
            ['id', 'name', '_aa', '_zz', '_ref'],
            \array_keys($parsed['fixtures'][0]),
        );
    }

    public function testCommentsStartingWithHashArePreserved(): void
    {
        $filePath = $this->copyFixtureFile('input/comments.yaml');
        $expectedPath = $this->getFixturePath('expected/comments.yaml');

        $commandTester = new CommandTester($this->subject);

        $statusCode = $commandTester->execute([
            'path' => $filePath,
        ]);

        self::assertSame(
            Command::SUCCESS,
            $statusCode,
        );

        self::assertFileEquals(
            $expectedPath,
            $filePath,
        );
    }

    private function copyFixtureFile(string $relativePath): string
    {
        $source = $this->getFixturePath($relativePath);
        $target = $this->tempDir.'/'.$relativePath;
        $dir = \dirname($target);

        if (!\is_dir($dir)) {
            \mkdir($dir, 0o777, true);
        }

        \copy($source, $target);

        return $target;
    }

    private function copyFixtureDirectory(string $relativePath): string
    {
        $source = $this->getFixturePath($relativePath);
        $target = $this->tempDir.'/'.$relativePath;

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator(
                $source,
                \FilesystemIterator::SKIP_DOTS,
            ),
            \RecursiveIteratorIterator::SELF_FIRST,
        );

        /** @var \SplFileInfo $item */
        foreach ($iterator as $item) {
            $targetPath = $target.'/'.$iterator->getSubIterator()->getSubPathname();

            if ($item->isDir()) {
                if (!\is_dir($targetPath)) {
                    \mkdir($targetPath, 0o777, true);
                }

                continue;
            }

            $dir = \dirname($targetPath);

            if (!\is_dir($dir)) {
                \mkdir($dir, 0o777, true);
            }

            \copy($item->getPathname(), $targetPath);
        }

        return $target;
    }

    private function getFixturePath(string $relativePath): string
    {
        return $this->fixturesDir.'/'.$relativePath;
    }

    private function removeDir(string $dir): void
    {
        if (!\is_dir($dir)) {
            return;
        }

        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator(
                $dir,
                \FilesystemIterator::SKIP_DOTS,
            ),
            \RecursiveIteratorIterator::CHILD_FIRST,
        );

        /** @var \SplFileInfo $file */
        foreach ($files as $file) {
            if ($file->isDir()) {
                \rmdir($file->getRealPath());

                continue;
            }

            \unlink($file->getRealPath());
        }

        \rmdir($dir);
    }
}
