<?php

declare(strict_types=1);

namespace App\Tests\Shared\Infrastructure\Upload;

use App\Shared\Domain\Exception\FileTooLargeException;
use App\Shared\Domain\Exception\UnsupportedMimeTypeException;
use App\Shared\Infrastructure\Upload\FileUploader;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * Tests unitaires du service generique de televersement (ERP-154).
 *
 * Couvre : rejet d'un MIME hors whitelist, rejet d'un fichier trop volumineux,
 * et le chemin nominal (checksum sha256 calcule, taille/MIME captures, fichier
 * ecrit sous var/uploads/{yyyy}/{mm}/ avec horodatage de l'horloge injectee).
 *
 * @internal
 */
final class FileUploaderTest extends TestCase
{
    private string $uploadBaseDir;

    /** @var list<string> */
    private array $tempFiles = [];

    protected function setUp(): void
    {
        $this->uploadBaseDir = sys_get_temp_dir().'/erp154-uploads-'.bin2hex(random_bytes(4));
    }

    protected function tearDown(): void
    {
        // Nettoyage des fichiers sources et de l'arborescence de destination.
        foreach ($this->tempFiles as $path) {
            if (is_file($path)) {
                @unlink($path);
            }
        }
        $this->removeDirectory($this->uploadBaseDir);
    }

    public function testRejectsMimeTypeOutsideWhitelist(): void
    {
        $uploader = $this->createUploader();
        $file     = $this->makeUploadedFile('hello world plain text', 'note.txt');

        $this->expectException(UnsupportedMimeTypeException::class);

        $uploader->upload($file);
    }

    public function testRejectsFileLargerThanMaxSize(): void
    {
        $uploader = $this->createUploader();

        // Contenu PDF valide mais artificiellement gonfle au-dela de la borne.
        $content = "%PDF-1.4\n".str_repeat('A', FileUploader::MAX_SIZE_BYTES + 1);
        $file    = $this->makeUploadedFile($content, 'huge.pdf');

        $this->expectException(FileTooLargeException::class);

        $uploader->upload($file);
    }

    public function testStoresPdfAndComputesSha256Checksum(): void
    {
        $content  = $this->minimalPdf();
        $clock    = new MockClock(new DateTimeImmutable('2026-06-15 10:00:00'));
        $uploader = $this->createUploader($clock);
        $file     = $this->makeUploadedFile($content, 'facture.pdf');

        $document = $uploader->upload($file);

        self::assertSame('facture.pdf', $document->getOriginalFilename());
        self::assertSame('application/pdf', $document->getMimeType());
        self::assertSame(\strlen($content), $document->getSizeBytes());
        self::assertSame(hash('sha256', $content), $document->getChecksum());
        self::assertSame(64, \strlen($document->getChecksum()));

        // Chemin relatif date selon l'horloge injectee (2026/06).
        self::assertStringStartsWith('2026/06/', $document->getStoredPath());
        self::assertFileExists($this->uploadBaseDir.'/'.$document->getStoredPath());

        // Le fichier ecrit a bien le contenu d'origine (checksum coherent).
        self::assertSame(
            $document->getChecksum(),
            hash_file('sha256', $this->uploadBaseDir.'/'.$document->getStoredPath()),
        );
    }

    public function testRemoveDeletesStoredFile(): void
    {
        $uploader   = $this->createUploader();
        $file       = $this->makeUploadedFile($this->minimalPdf(), 'facture.pdf');
        $document   = $uploader->upload($file);
        $storedPath = $this->uploadBaseDir.'/'.$document->getStoredPath();
        self::assertFileExists($storedPath);

        // Compensation : remove() efface le fichier physique...
        $uploader->remove($document);
        self::assertFileDoesNotExist($storedPath);

        // ...et reste silencieux si on le rappelle alors que le fichier a disparu.
        $uploader->remove($document);
        self::assertFileDoesNotExist($storedPath);
    }

    private function createUploader(?MockClock $clock = null): FileUploader
    {
        return new FileUploader($this->uploadBaseDir, $clock ?? new MockClock());
    }

    /**
     * Cree un UploadedFile en mode test (move() autorise hors contexte HTTP).
     */
    private function makeUploadedFile(string $content, string $clientName): UploadedFile
    {
        $path = sys_get_temp_dir().'/erp154-src-'.bin2hex(random_bytes(4));
        file_put_contents($path, $content);
        $this->tempFiles[] = $path;

        // Le 5e argument `test: true` court-circuite move_uploaded_file().
        return new UploadedFile($path, $clientName, null, null, true);
    }

    /**
     * Contenu PDF minimal valide — l'entete `%PDF-1.4` suffit a faire detecter
     * `application/pdf` par finfo (getMimeType server-side).
     */
    private function minimalPdf(): string
    {
        return "%PDF-1.4\n"
            ."1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
            ."2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n"
            ."3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj\n"
            ."trailer<</Root 1 0 R/Size 4>>\n"
            ."%%EOF\n";
    }

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

        $items = scandir($dir) ?: [];
        foreach ($items as $item) {
            if ('.' === $item || '..' === $item) {
                continue;
            }
            $path = $dir.'/'.$item;
            is_dir($path) ? $this->removeDirectory($path) : @unlink($path);
        }
        @rmdir($dir);
    }
}
