<?php

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

use NoahVet\Reef\Command\OpenAPI\FormatCommand;
use NoahVet\Reef\Command\OpenAPI\FormatOpenApiCommandInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;

final class FormatCommandTest extends TestCase
{
    private string $tempDir;

    private string $yamlFile;

    private FormatOpenApiCommandInterface $subject;

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

        $this->tempDir = \sys_get_temp_dir().'/fmt_'.\bin2hex(\random_bytes(6));
        $this->yamlFile = \sys_get_temp_dir().'/fmt_'.\bin2hex(\random_bytes(6)).'.yaml';

        $this->subject = new FormatCommand();
    }

    protected function tearDown(): void
    {
        $this->removeDir($this->tempDir);
        @\unlink($this->yamlFile);

        parent::tearDown();
    }

    public function testExecuteReturnsFailureWhenPathDoesNotExist(): void
    {
        $input = new ArrayInput([
            'path' => $this->tempDir.'/does_not_exist',
        ]);
        $output = new BufferedOutput();

        $code = $this->subject->run($input, $output);

        self::assertSame(Command::FAILURE, $code);
        self::assertStringContainsString('does not exist', $output->fetch());
    }

    public function testExecuteFormatsComponentsSectionsAndComments(): void
    {
        $content = <<<YAML
            openapi: 3.0.0
            components:
              parameters:
                LocaleRequestHeader:
                  in: header
                  name: Accept-Language
                  required: false
                  schema:
                    type: string
                  description: The current locale.
                LocaleResponseHeader:
                  type: string
                  description: The current locale.
              schemas:
                Foo:
                  type: string
                Bar:
                  type: integer
            paths:
              /v1/foo:
                get: {}
              # First comment
              /v1/bar:
                get: {}
              # Second comment

            YAML;

        \file_put_contents($this->yamlFile, $content);

        $input = new ArrayInput([
            'path' => $this->yamlFile,
        ]);
        $output = new BufferedOutput();

        $code = $this->subject->run($input, $output);

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

        $expected = <<<YAML
            openapi: 3.0.0
            components:
              parameters:
                LocaleRequestHeader:
                  in: header
                  name: Accept-Language
                  required: false
                  schema:
                    type: string
                  description: The current locale.

                LocaleResponseHeader:
                  type: string
                  description: The current locale.
              schemas:
                Foo:
                  type: string

                Bar:
                  type: integer
            paths:
              /v1/foo:
                get: {}

              # First comment
              /v1/bar:
                get: {}

              # Second comment

            YAML;

        $actual = (string) \file_get_contents($this->yamlFile);
        self::assertSame($expected, $actual);
        self::assertStringContainsString('Formatted:', $output->fetch());
    }

    public function testExecuteWithDryRunDoesNotModifyFile(): void
    {
        $original = <<<YAML
            components:
              parameters:
                A:
                  type: string
                B:
                  type: string
            # Comment
            paths: {}

            YAML;

        \file_put_contents($this->yamlFile, $original);

        $input = new ArrayInput([
            'path' => $this->yamlFile,
            '--dry-run' => true,
        ]);
        $output = new BufferedOutput();

        $code = $this->subject->run($input, $output);

        self::assertSame(Command::SUCCESS, $code);
        self::assertSame($original, (string) \file_get_contents($this->yamlFile));
        self::assertStringContainsString('[DRY-RUN]', $output->fetch());
    }

    public function testExecuteOnDirectoryProcessesOnlyYamlFiles(): void
    {
        @\mkdir($this->tempDir, 0o777, true);

        $yaml1 = $this->tempDir.'/file1.yaml';
        $yaml2 = $this->tempDir.'/file2.yml';
        $txt = $this->tempDir.'/ignore.txt';

        $file1Content = <<<YAML
            components:
              schemas:
                Foo:
                  type: string
                Bar:
                  type: integer
            # Comment on schemas

            YAML;

        $file2Content = <<<YAML
            paths:
              /v1/foo:
                get: {}
              # Endpoint comment
              /v1/bar:
                get: {}

            YAML;

        \file_put_contents($yaml1, $file1Content);
        \file_put_contents($yaml2, $file2Content);
        \file_put_contents($txt, "should\n\nstay\n\nas\n\nis\n");

        $input = new ArrayInput([
            'path' => $this->tempDir,
        ]);
        $output = new BufferedOutput();

        $code = $this->subject->run($input, $output);

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

        $expectedYaml1 = <<<YAML
            components:
              schemas:
                Foo:
                  type: string

                Bar:
                  type: integer

            # Comment on schemas

            YAML;

        $expectedYaml2 = <<<YAML
            paths:
              /v1/foo:
                get: {}

              # Endpoint comment
              /v1/bar:
                get: {}

            YAML;

        self::assertSame($expectedYaml1, (string) \file_get_contents($yaml1));
        self::assertSame($expectedYaml2, (string) \file_get_contents($yaml2));

        // Non-YAML file must not be changed
        self::assertSame("should\n\nstay\n\nas\n\nis\n", (string) \file_get_contents($txt));

        $outputStr = $output->fetch();
        self::assertStringContainsString('Formatted:', $outputStr);
    }

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

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

        /** @var \SplFileInfo $f */
        foreach ($files as $f) {
            $path = $f->getPathname();
            $f->isDir() ? @\rmdir($path) : @\unlink($path);
        }

        @\rmdir($dir);
    }
}
