<?php

declare(strict_types=1);

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

use NoahVet\Reef\Command\OpenAPI\QueryParamDumpCommand;
use NoahVet\Reef\Command\OpenAPI\QueryParamDumpCommandInterface;
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 QueryParamDumpCommandTest extends TestCase
{
    private string $tempDir;

    private string $openApiPath;

    private QueryParamDumpCommandInterface $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().'/qpdc_'.\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 QueryParamDumpCommand(
            $this->openApiPath,
            $this->tempDir,
            $this->openApiToolMock,
            $this->yamlDumperMock,
        );
    }

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

    public function testExecuteSkipsEndpointsWithoutQueryParametersAndCleansDestination(): void
    {
        @\mkdir($this->tempDir.'/old', 0o777, true);
        \file_put_contents($this->tempDir.'/old/leftover.yaml', 'stale');

        $paths = [
            '/no-query' => [
                'get' => [
                    'parameters' => [
                        ['in' => 'header', 'name' => 'X-Only-Header'],
                        ['in' => 'path', 'name' => 'uuid'],
                    ],
                ],
            ],
        ];
        \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);
        self::assertDirectoryDoesNotExist($this->tempDir);
    }

    public function testExecuteGeneratesFileWhenNonEmptyQueryParametersPresent(): void
    {
        // Ensure destination exists so any cleanup logic in preExecute() can run safely
        @\mkdir($this->tempDir, 0o777, true);

        $methodContent = [
            'parameters' => [
                ['in' => 'query', 'name' => 'page'],
                ['in' => 'header', 'name' => 'X-Trace'],
                ['in' => 'query', 'name' => 'tags[]'],
            ],
        ];
        $openApi = [
            'openapi' => '3.0.0',
            'paths' => [
                '/pets/{id}' => [
                    'get' => $methodContent,
                ],
            ],
        ];
        \file_put_contents($this->openApiPath, Yaml::dump($openApi, 32, 2));

        // Expect interactions: cleanParameters is called with the method content
        $returnedParameters = ['parameters' => ['page', 'tags']];
        $this->openApiToolMock
            ->expects($this->once())
            ->method('cleanParameters')
            ->with($this->callback(function (array $arg) use ($methodContent): bool {
                // Be strict: we pass the exact method-level node to the tool
                \PHPUnit\Framework\Assert::assertSame($methodContent, $arg);

                return true;
            }))
            ->willReturn($returnedParameters)
        ;

        // cleanUri used to resolve target path
        $this->openApiToolMock
            ->expects($this->once())
            ->method('cleanUri')
            ->with('/pets/{id}')
            ->willReturn('/pets/id')
        ;

        // Dumper receives the cleaned parameters and returns YAML content
        $expectedYaml = "parameters:\n  - page\n  - tags\n";
        $this->yamlDumperMock
            ->expects($this->once())
            ->method('dumpContent')
            ->with($returnedParameters)
            ->willReturn($expectedYaml)
        ;

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

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

        // One file is generated with the dumper result
        self::assertDirectoryExists($this->tempDir);
        $files = $this->gatherFiles($this->tempDir);
        self::assertCount(1, $files);
        self::assertSame($expectedYaml, (string) \file_get_contents($files[0]));
    }

    public function testWriteFileCreatesDirectoriesAndWritesExpectedContent(): void
    {
        $target = $this->tempDir.'/nested/path/parameter.yaml';
        $payload = Yaml::dump(['parameters' => ['a', 'b']], 32, 2);

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

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

    /**
     * Ensures YAML file exists, is non-empty, and decodes to an array.
     * Produces explicit failures instead of passing false to array operations.
     *
     * @return array<string,mixed>
     */
    private function testReadYamlStrict(string $path, string $label): array
    {
        self::assertFileExists($path, \sprintf('%s YAML file must exist', $label));
        $raw = \file_get_contents($path);
        self::assertNotFalse($raw, \sprintf('%s YAML file must be readable', $label));
        self::assertNotSame('', $raw, \sprintf('%s YAML file must not be empty', $label));

        $parsed = Yaml::parse($raw);
        self::assertIsArray($parsed, \sprintf('%s YAML must decode to array, got %s', $label, \gettype($parsed)));

        /* @var array<string,mixed> $parsed */
        return $parsed;
    }

    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;
    }
}
