<?php
namespace App\Tests\Unit\Service\Cron\Job;

use App\Entity\Access\Access;
use App\Entity\Core\File;
use App\Repository\Access\AccessRepository;
use App\Repository\Core\FileRepository;
use App\Service\Cron\Job\CleanTempFiles;
use App\Service\Cron\UI\CronUIInterface;
use App\Service\File\Storage\LocalStorage;
use App\Service\Utils\DatesUtils;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class TestableCleanTempFile extends CleanTempFiles {
    public function listFilesToDelete(): Collection { return parent::listFilesToDelete(); }
    public function deleteFiles(Collection $files): void { parent::deleteFiles($files); }
    public function purgeDb(bool $commit = true): void { parent::purgeDb($commit); }
}

class CleanTempFilesTest extends TestCase
{
    private CronUIInterface|MockObject $ui;
    private MockObject|LoggerInterface $logger;
    private FileRepository|MockObject $fileRepository;
    private EntityManagerInterface|MockObject $em;
    private AccessRepository|MockObject $accessRepository;
    private LocalStorage|MockObject $storage;

    public function setUp(): void
    {
        $this->ui = $this->getMockBuilder(CronUIInterface::class)->disableOriginalConstructor()->getMock();
        $this->logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();

        $this->fileRepository = $this->getMockBuilder(FileRepository::class)->disableOriginalConstructor()->getMock();
        $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
        $this->accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
        $this->storage = $this->getMockBuilder(LocalStorage::class)->disableOriginalConstructor()->getMock();
    }

    private function getMockFor(string $method) {
        $cleanTempFiles = $this->getMockBuilder(TestableCleanTempFile::class)
            ->setConstructorArgs([$this->fileRepository, $this->em, $this->accessRepository, $this->storage])
            ->setMethodsExcept([$method, 'setUI', 'setLoggerInterface'])
            ->getMock();
        $cleanTempFiles->setUI($this->ui);
        $cleanTempFiles->setLoggerInterface($this->logger);

        return $cleanTempFiles;
    }

    public function testPreview(): void
    {
        $cleanTempFiles = $this->getMockFor('preview');

        $file1 = $this->getMockBuilder(File::class)->getMock();
        $file1->method('getPath')->willReturn('/foo');

        $file2 = $this->getMockBuilder(File::class)->getMock();
        $file2->method('getPath')->willReturn('/bar');

        $file3 = $this->getMockBuilder(File::class)->getMock();
        $file3->method('getPath')->willReturn('/foo/bar');

        $cleanTempFiles->method('listFilesToDelete')->willReturn(new ArrayCollection([$file1, $file2, $file3]));

        $this->ui->expects(self::atLeastOnce())->method('print')->withConsecutive(
            ["3 temporary files to be removed"],
            ["> Printing the first and last 50 :"],
            ["  * /foo"],
            ["  * /bar"],
            ["  * /foo/bar"]
        );

        $cleanTempFiles->expects(self::once())->method('purgeDb')->with(false);

        $cleanTempFiles->preview();
    }

    public function testExecute(): void {
        $cleanTempFiles = $this->getMockFor('execute');

        $files = new ArrayCollection([
            $this->getMockBuilder(File::class)->getMock(),
            $this->getMockBuilder(File::class)->getMock(),
            $this->getMockBuilder(File::class)->getMock()
        ]);

        $cleanTempFiles->method('listFilesToDelete')->willReturn($files);

        $cleanTempFiles->expects(self::once())->method('deleteFiles')->with($files);

        $cleanTempFiles->expects(self::once())->method('purgeDb')->with();

        $cleanTempFiles->execute();
    }

    public function testListFilesToDelete(): void {
        $cleanTempFiles = $this->getMockFor('listFilesToDelete');

        DatesUtils::setFakeDatetime('2022-01-08 12:00:00');

        $this->ui->expects(self::once())->method('print')->with(
            'List temporary files created before 2022-01-01T12:00:00+00:00'
        );

        $file1 = $this->getMockBuilder(File::class)->getMock(); // Temporary, but recent : do not remove
        $file1->method('getId')->willReturn(1);
        $file1->method('getCreateDate')->willReturn(new \DateTime('2022-01-02'));
        $file1->method('getIsTemporaryFile')->willReturn(true);

        $file2 = $this->getMockBuilder(File::class)->getMock(); // Temporary and 1-year-old : remove
        $file1->method('getId')->willReturn(2);
        $file2->method('getCreateDate')->willReturn(new \DateTime('2021-01-01'));
        $file2->method('getIsTemporaryFile')->willReturn(true);

        $file3 = $this->getMockBuilder(File::class)->getMock(); // 2 years old but not temporary : do not remove
        $file1->method('getId')->willReturn(3);
        $file3->method('getCreateDate')->willReturn(new \DateTime('2020-01-01'));
        $file3->method('getIsTemporaryFile')->willReturn(false);

        $files = new ArrayCollection([$file1, $file2, $file3]);

        $this->fileRepository
            ->expects(self::once())
            ->method('matching')
            ->with(
                self::callback(static function (Criteria $c) use ($files) {
                    $matching = $files->matching($c)->toArray();
                    return count($matching) === 1 && array_values($matching)[0] === $files[1];
                })
            )->willReturn($files);

        $this->assertEquals(
            $files,
            $cleanTempFiles->listFilesToDelete()
        );
    }

    public function testDeleteFiles(): void
    {
        $cleanTempFiles = $this->getMockFor('deleteFiles');

        $file1 = $this->getMockBuilder(File::class)->getMock();
        $file2 = $this->getMockBuilder(File::class)->getMock();
        $file3 = $this->getMockBuilder(File::class)->getMock();
        $files = new ArrayCollection([$file1, $file2, $file3]);

        $this->ui->expects(self::atLeastOnce())->method('print')->withConsecutive(
            ['3 temporary files to be removed'],
            ['Deleting files...'],
            ['3 files deleted'],
        );

        $author = $this->getMockBuilder(Access::class)->getMock();
        $this->accessRepository->method('find')->with(10984)->willReturn($author);

        $this->ui->expects(self::exactly(4))->method('progress')->withConsecutive([0, 3], [1, 3], [2, 3], [3, 3]);

        $this->storage
            ->expects(self::exactly(3))
            ->method('delete')
            ->withConsecutive(
                [$file1, $author],
                [$file2, $author],
                [$file3, $author]
            );

        $cleanTempFiles->deleteFiles($files);
    }

    public function testDeleteFilesUnknownAuthor(): void
    {
        $cleanTempFiles = $this->getMockFor('deleteFiles');

        $file1 = $this->getMockBuilder(File::class)->getMock();
        $file2 = $this->getMockBuilder(File::class)->getMock();
        $file3 = $this->getMockBuilder(File::class)->getMock();
        $files = new ArrayCollection([$file1, $file2, $file3]);

        $this->accessRepository->method('find')->with(10984)->willReturn(null);

        $this->ui->expects(self::never())->method('progress');
        $this->storage->expects(self::never())->method('delete');

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Access 10984 could not be found');

        $cleanTempFiles->deleteFiles($files);
    }

    public function testDeleteFilesDeletionError(): void
    {
        $cleanTempFiles = $this->getMockFor('deleteFiles');

        $file1 = $this->getMockBuilder(File::class)->getMock();
        $file1->method('getId')->willReturn(1);

        $file2 = $this->getMockBuilder(File::class)->getMock();
        $file2->method('getId')->willReturn(2);

        $files = new ArrayCollection([$file1, $file2]);

        $this->ui->expects(self::atLeastOnce())->method('print')->withConsecutive(
            ['2 temporary files to be removed'],
            ['Deleting files...'],
            ['ERROR : foo'],
            ['1 files deleted'],
        );

        $author = $this->getMockBuilder(Access::class)->getMock();
        $this->accessRepository->method('find')->with(10984)->willReturn($author);

        $this->ui->expects(self::exactly(3))->method('progress')->withConsecutive([0, 2], [1, 2], [2, 2]);

        $this->storage
            ->expects(self::exactly(2))
            ->method('delete')
            ->willReturnCallback(static function ($file, $author): File {
                if ($file->getId() === 1) {
                    throw new \RuntimeException('foo');
                }
                return $file;
            });

        $cleanTempFiles->deleteFiles($files);
    }

    public function testPurgeDb(): void {
        $cleanTempFiles = $this->getMockFor('purgeDb');

        DatesUtils::setFakeDatetime('2022-01-01 12:00:00');
        $this->ui->expects(self::exactly(2))->method('print')->withConsecutive(
            ['Purge DB from records of files deleted before 2021-01-01T12:00:00+00:00'],
            ['DB purged - 3 records permanently deleted'],
        );

        $qb = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
        $this->em->expects(self::once())->method('createQueryBuilder')->willReturn($qb);

        $exprBuilder = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
        $qb->method('expr')->willReturn($exprBuilder);

        $cmp1 = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
        $exprBuilder->expects(self::once())->method('eq')->with('f.isTemporaryFile', 1)->willReturn($cmp1);

        $cmp2 = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
        $exprBuilder->expects(self::once())->method('lt')->with('f.updateDate', ':maxDate')->willReturn($cmp2);

        $qb->expects(self::once())->method('delete')->with('File', 'f')->willReturnSelf();
        $qb->method('where')->with($cmp1)->willReturnSelf();
        $qb->method('andWhere')->with($cmp2)->willReturnSelf();
        $qb->method('setParameter')->with('maxDate', DatesUtils::new('2021-01-01T12:00:00+00:00'))->willReturnSelf();

        $q = $this->getMockBuilder(AbstractQuery::class)->disableOriginalConstructor()->getMock();
        $qb->method('getQuery')->willReturn($q);

        $q->method('getResult')->willReturn(3);

        $this->em->expects(self::once())->method('commit');

        $cleanTempFiles->purgeDb();
    }

    public function testPurgeDbNoCommit(): void {
        $cleanTempFiles = $this->getMockFor('purgeDb');

        DatesUtils::setFakeDatetime('2022-01-01 12:00:00');
        $this->ui->expects(self::exactly(2))->method('print')->withConsecutive(
            ['Purge DB from records of files deleted before 2021-01-01T12:00:00+00:00'],
            ['DB purged - 3 records would be permanently deleted'],
        );

        $qb = $this->getMockBuilder(QueryBuilder::class)->disableOriginalConstructor()->getMock();
        $this->em->expects(self::once())->method('createQueryBuilder')->willReturn($qb);

        $exprBuilder = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
        $qb->method('expr')->willReturn($exprBuilder);

        $cmp1 = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
        $exprBuilder->expects(self::once())->method('eq')->with('f.isTemporaryFile', 1)->willReturn($cmp1);

        $cmp2 = $this->getMockBuilder(Expr::class)->disableOriginalConstructor()->getMock();
        $exprBuilder->expects(self::once())->method('lt')->with('f.updateDate', ':maxDate')->willReturn($cmp2);

        $qb->expects(self::once())->method('delete')->with('File', 'f')->willReturnSelf();
        $qb->method('where')->willReturnSelf();
        $qb->method('andWhere')->willReturnSelf();
        $qb->method('setParameter')->willReturnSelf();

        $q = $this->getMockBuilder(AbstractQuery::class)->disableOriginalConstructor()->getMock();
        $qb->method('getQuery')->willReturn($q);

        $q->method('getResult')->willReturn(3);

        $this->em->expects(self::never())->method('commit');
        $this->em->expects(self::once())->method('rollback');

        $cleanTempFiles->purgeDb(false);
    }
}
