<?php

declare(strict_types=1);

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

use NoahVet\Reef\Command\OpenAPI\ValidationDumpCommand;
use NoahVet\Reef\Command\OpenAPI\ValidationParamDumpCommandInterface;
use NoahVet\Reef\Domain\Tool\OpenAPIToolInterface;
use NoahVet\Reef\File\Dumper\Yaml\DumperInterface as YamlDumperInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Yaml\Yaml;

final class ValidationDumpCommandTest extends TestCase
{
    private string $tempDir;

    private string $openApiPath;

    private ValidationParamDumpCommandInterface $subject;

    /** @var OpenAPIToolInterface&MockObject */
    private OpenAPIToolInterface $openApiToolMock;

    /** @var YamlDumperInterface&MockObject */
    private YamlDumperInterface $yamlDumperMock;

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

        $this->tempDir = \sys_get_temp_dir().'/vdc_'.\bin2hex(\random_bytes(6));
        $this->openApiPath = \sys_get_temp_dir().'/openapi_'.\bin2hex(\random_bytes(6)).'.yaml';

        \file_put_contents(
            $this->openApiPath,
            Yaml::dump(['openapi' => '3.0.0', 'paths' => []], 32, 2),
        );

        $this->openApiToolMock = $this->createMock(OpenAPIToolInterface::class);
        $this->yamlDumperMock = $this->createMock(YamlDumperInterface::class);

        $this->subject = new ValidationDumpCommand(
            $this->openApiPath,
            $this->tempDir,
            $this->openApiToolMock,
            $this->yamlDumperMock,
        );
    }

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

    public function testExecuteSkipsEndpointsWithoutRequestBodyAndCleansDestination(): void
    {
        // Pre-create garbage to ensure destination cleanup is performed
        @\mkdir($this->tempDir.'/old', 0o777, true);
        \file_put_contents($this->tempDir.'/old/leftover.yaml', 'stale');

        $paths = [
            '/no-body' => [
                'post' => [
                    // no requestBody => nothing generated
                ],
            ],
        ];
        \file_put_contents(
            $this->openApiPath,
            Yaml::dump(['openapi' => '3.0.0', 'paths' => $paths], 32, 2),
        );

        $code = $this->subject->run(new ArrayInput([]), new BufferedOutput());

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

        // As no files are written, directory may not exist
        self::assertDirectoryDoesNotExist($this->tempDir);
    }

    public function testExecuteGeneratesFileWhenRequestBodyPresent(): void
    {
        // IMPORTANT: ensure destination exists so preExecute() can scan/clean it safely
        @\mkdir($this->tempDir, 0o777, true);

        $properties = [
            'name' => ['type' => 'string'],
            'age' => ['type' => 'integer'],
        ];

        $openApi = [
            'openapi' => '3.0.0',
            'paths' => [
                '/pets/{id}' => [
                    'post' => [
                        'requestBody' => [
                            'content' => [
                                'application/json' => [
                                    'schema' => [
                                        'properties' => $properties,
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ];

        \file_put_contents(
            $this->openApiPath,
            Yaml::dump($openApi, 32, 2),
        );

        // Expect interactions
        $this->openApiToolMock
            ->expects($this->once())
            ->method('cleanUri')
            ->with('/pets/{id}')
            ->willReturn('/pets/id')
        ;

        $this->yamlDumperMock
            ->expects($this->once())
            ->method('dumpContent')
            ->with($properties)
            ->willReturn("name: string\nage: integer\n")
        ;

        $code = $this->subject->run(new ArrayInput([]), new BufferedOutput());

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

        // Verify a single file has been generated under destination with expected content
        self::assertDirectoryExists($this->tempDir);
        $files = $this->gatherFiles($this->tempDir);
        self::assertCount(1, $files);
        $generated = $files[0];
        self::assertFileExists($generated);
        self::assertSame("name: string\nage: integer\n", (string) \file_get_contents($generated));
    }

    public function testWriteFileCreatesDirectoriesAndWritesExpectedContent(): void
    {
        $target = $this->tempDir.'/nested/path/validation.yaml';
        $payload = Yaml::dump(['field' => ['type' => 'string']], 32, 2);

        $this->subject->writeFile($target, $payload);

        self::assertFileExists($target);
        self::assertSame($payload, (string) \file_get_contents($target));
    }

    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) {
            $f->isDir() ? @\rmdir($f->getPathname()) : @\unlink($f->getPathname());
        }
        @\rmdir($dir);
    }

    /**
     * @return list<string>
     */
    private function gatherFiles(string $dir): array
    {
        if (!\is_dir($dir)) {
            return [];
        }
        $it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS);
        $files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::LEAVES_ONLY);
        $paths = [];
        /** @var \SplFileInfo $f */
        foreach ($files as $f) {
            if ($f->isFile()) {
                $paths[] = $f->getPathname();
            }
        }

        return $paths;
    }
}
