<?php

namespace App\Tests\Command;

use App\Command\CleanAttestationsCommand;
use App\Entity\Attestation;
use App\Repository\AttestationRepository;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Docuseal\Api;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Filesystem\Filesystem;

class CleanAttestationsCommandTest extends TestCase
{
    private EntityManagerInterface $em;
    private AttestationRepository $repository;
    private Filesystem $filesystem;
    private Api $api;
    private string $projectDir;

    protected function setUp(): void
    {
        $this->em = $this->createStub(EntityManagerInterface::class);
        $this->repository = $this->createStub(AttestationRepository::class);
        $this->filesystem = $this->createStub(Filesystem::class);
        $this->api = $this->createStub(Api::class);
        $this->projectDir = sys_get_temp_dir().'/clean-test-'.bin2hex(random_bytes(4));
        mkdir($this->projectDir, 0775, true);
    }

    protected function tearDown(): void
    {
        $this->removeDir($this->projectDir);
    }

    private function removeDir(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }
        foreach (scandir($dir) as $item) {
            if ('.' === $item || '..' === $item) {
                continue;
            }
            $path = $dir.'/'.$item;
            is_dir($path) ? $this->removeDir($path) : unlink($path);
        }
        rmdir($dir);
    }

    private function execute(): CommandTester
    {
        // Ensure dirs exist so DirectoryIterator doesn't fail in cleanOrphanFiles
        if (!is_dir($this->projectDir.'/var/rgpd/signed')) {
            mkdir($this->projectDir.'/var/rgpd/signed', 0775, true);
        }

        $command = new CleanAttestationsCommand(
            $this->em,
            $this->repository,
            $this->filesystem,
            $this->projectDir,
            'https://fake.docuseal.test',
            'fake-key',
        );

        // Replace Api with stub
        $ref = new \ReflectionProperty(CleanAttestationsCommand::class, 'docuSealApi');
        $ref->setValue($command, $this->api);

        $tester = new CommandTester($command);
        $tester->execute([]);

        return $tester;
    }

    private function mockQueryReturning(array $results): void
    {
        $query = $this->createStub(Query::class);
        $query->method('getResult')->willReturn($results);

        $qb = $this->createStub(QueryBuilder::class);
        $qb->method('where')->willReturnSelf();
        $qb->method('setParameter')->willReturnSelf();
        $qb->method('getQuery')->willReturn($query);

        $this->repository->method('createQueryBuilder')->willReturn($qb);
    }

    public function testNoAttestationsToClean(): void
    {
        $this->mockQueryReturning([]);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('Aucune attestation', $tester->getDisplay());
    }

    public function testDeleteAttestationsWithFiles(): void
    {
        $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', 'secret');
        $attestation->setPdfFileUnsigned('/tmp/unsigned.pdf');
        $attestation->setPdfFileSigned('/tmp/signed.pdf');
        $attestation->setPdfFileCertificate('/tmp/cert.pdf');

        $this->mockQueryReturning([$attestation]);
        $this->filesystem->method('exists')->willReturn(false);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('1 attestation(s) supprimee(s)', $tester->getDisplay());
    }

    public function testDeleteAttestationWithSubmitterId(): void
    {
        $attestation = new Attestation('deletion', '127.0.0.1', 'test@test.com', 'secret');
        $attestation->setSubmitterId(42);

        $this->mockQueryReturning([$attestation]);
        $this->api->method('getSubmitter')->willReturn(['submission_id' => 99]);
        $this->filesystem->method('exists')->willReturn(false);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('1 attestation(s) supprimee(s)', $tester->getDisplay());
    }

    public function testDeleteAttestationSubmitterNoSubmissionId(): void
    {
        $attestation = new Attestation('no_data', '127.0.0.1', 'test@test.com', 'secret');
        $attestation->setSubmitterId(42);

        $this->mockQueryReturning([$attestation]);
        $this->api->method('getSubmitter')->willReturn([]);
        $this->filesystem->method('exists')->willReturn(false);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function testDeleteFromDocuSealThrows(): void
    {
        $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', 'secret');
        $attestation->setSubmitterId(42);

        $this->mockQueryReturning([$attestation]);
        $this->api->method('getSubmitter')->willThrowException(new \RuntimeException('API error'));
        $this->filesystem->method('exists')->willReturn(false);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('DocuSeal: impossible de supprimer', $tester->getDisplay());
    }

    public function testCleanOrphanFilesInExistingDir(): void
    {
        $repository = $this->createStub(AttestationRepository::class);

        // Create orphan file with old mtime
        $rgpdDir = $this->projectDir.'/var/rgpd';
        mkdir($rgpdDir, 0775, true);
        $orphanFile = $rgpdDir.'/orphan.pdf';
        file_put_contents($orphanFile, 'orphan');
        touch($orphanFile, time() - 86400 * 30);

        // Return one attestation so we get past the "aucune" check
        $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', 'secret');
        $query = $this->createStub(Query::class);
        $query->method('getResult')->willReturn([$attestation]);
        $qb = $this->createStub(QueryBuilder::class);
        $qb->method('where')->willReturnSelf();
        $qb->method('setParameter')->willReturnSelf();
        $qb->method('getQuery')->willReturn($query);
        $repository->method('createQueryBuilder')->willReturn($qb);

        $command = new CleanAttestationsCommand(
            $this->em,
            $repository,
            new Filesystem(),
            $this->projectDir,
            'https://fake.docuseal.test',
            'fake-key',
        );
        $ref = new \ReflectionProperty(CleanAttestationsCommand::class, 'docuSealApi');
        $ref->setValue($command, $this->api);

        $tester = new CommandTester($command);
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('fichier(s) orphelin(s) supprime(s)', $tester->getDisplay());
    }

    public function testDeleteFileWithNullPath(): void
    {
        $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', 'secret');
        // No PDF paths set (all null)

        $this->mockQueryReturning([$attestation]);
        $this->filesystem->method('exists')->willReturn(false);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function testDeleteFileExistsOnDisk(): void
    {
        $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', 'secret');
        $attestation->setPdfFileUnsigned('/tmp/exists.pdf');

        $this->mockQueryReturning([$attestation]);
        $this->filesystem->method('exists')->willReturn(true);

        $tester = $this->execute();

        $this->assertSame(0, $tester->getStatusCode());
    }
}
