<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service\Cron\Job;

use App\Service\Cron\Job\CleanDb;
use App\Service\Cron\UI\CronUIInterface;
use App\Service\Utils\DatesUtils;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Statement;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class TestableCleanDb extends CleanDb
{
    public function purgeDb(bool $commit = true): void
    {
        parent::purgeDb($commit);
    }

    public function purgeAuditTables(\DateTime $maxDate): int
    {
        return parent::purgeAuditTables($maxDate);
    }

    public function purgeMessages(\DateTime $maxDate): int
    {
        return parent::purgeMessages($maxDate);
    }

    public function purgeNotifications(\DateTime $maxDate): int
    {
        return parent::purgeNotifications($maxDate);
    }
}

class CleanDbTest extends TestCase
{
    private CronUIInterface|MockObject $ui;
    private MockObject|LoggerInterface $logger;
    private Connection|MockObject $connection;

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

        $this->connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
    }

    private function getMockFor(string $method): MockObject|TestableCleanDb
    {
        $cleanDb = $this->getMockBuilder(TestableCleanDb::class)
            ->setConstructorArgs([$this->connection])
            ->setMethodsExcept([$method, 'setUI', 'setLoggerInterface'])
            ->getMock();
        $cleanDb->setUI($this->ui);
        $cleanDb->setLoggerInterface($this->logger);

        return $cleanDb;
    }

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

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

        $cleanDb->preview();
    }

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

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

        $cleanDb->execute();
    }

    public function testPurgeDb(): void
    {
        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');

        $maxDate = DatesUtils::new();
        $maxDate->sub(new \DateInterval('P60D'));

        $maxDateAudit = DatesUtils::new();
        $maxDateAudit->sub(new \DateInterval('P180D'));

        $cleanDb = $this->getMockFor('purgeDb');

        $this->connection->expects($this->once())->method('beginTransaction');
        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
        $this->connection->expects($this->once())->method('commit');
        $this->connection->expects($this->never())->method('rollback');

        $cleanDb->expects(self::once())->method('purgeAuditTables')->with($maxDateAudit)->willReturn(100);
        $cleanDb->expects(self::once())->method('purgeMessages')->with($maxDate)->willReturn(101);
        $cleanDb->expects(self::once())->method('purgeNotifications')->with($maxDate)->willReturn(102);

        $this->logger->expects(self::once())->method('info')->with('DB purged - 303 records permanently deleted');

        $cleanDb->purgeDb();
    }

    public function testPurgeDbNoCommit(): void
    {
        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');

        $maxDate = DatesUtils::new();
        $maxDate->sub(new \DateInterval('P60D'));

        $maxDateAudit = DatesUtils::new();
        $maxDateAudit->sub(new \DateInterval('P180D'));

        $cleanDb = $this->getMockFor('purgeDb');

        $this->connection->expects($this->once())->method('beginTransaction');
        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
        $this->connection->expects($this->never())->method('commit');
        $this->connection->expects($this->once())->method('rollback');

        $cleanDb->expects(self::once())->method('purgeAuditTables')->with($maxDateAudit)->willReturn(100);
        $cleanDb->expects(self::once())->method('purgeMessages')->with($maxDate)->willReturn(101);
        $cleanDb->expects(self::once())->method('purgeNotifications')->with($maxDate)->willReturn(102);

        $this->ui->expects(self::once())->method('print')->with('DB purged - 303 records would be permanently deleted');

        $cleanDb->purgeDb(false);
    }

    public function testPurgeDbWithError(): void
    {
        DatesUtils::setFakeDatetime('2022-01-08 00:00:00');

        $maxDate = DatesUtils::new();
        $maxDate->sub(new \DateInterval('P60D'));

        $maxDateAudit = DatesUtils::new();
        $maxDateAudit->sub(new \DateInterval('P180D'));

        $cleanDb = $this->getMockFor('purgeDb');

        $this->connection->expects($this->once())->method('beginTransaction');
        $this->connection->expects($this->once())->method('setAutoCommit')->with(false);
        $this->connection->expects($this->never())->method('commit');
        $this->connection->expects($this->once())->method('rollback');

        $cleanDb->expects(self::once())->method('purgeAuditTables')->with($maxDateAudit)->willThrowException(new \Exception('Error'));

        $this->expectException(\Exception::class);

        $cleanDb->purgeDb(false);
    }

    public function testPurgeAuditTables(): void
    {
        $maxDateAudit = DatesUtils::new('2022-06-30 00:00:00');
        $maxDateAudit->sub(new \DateInterval('P180D'));

        $cleanDb = $this->getMockFor('purgeAuditTables');

        $schemaManager = $this->getMockBuilder(\Doctrine\DBAL\Schema\AbstractSchemaManager::class)->disableOriginalConstructor()->getMock();
        $this->connection->method('createSchemaManager')->willReturn($schemaManager);

        $schemaManager->method('listTableNames')->willReturn([
            'table1',
            'Audit_table1',
            'table2',
            'Audit_table2',
        ]);

        $stmt1 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
        $stmt2 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();

        $this->connection
            ->expects(self::exactly(2))
            ->method('prepare')
            ->willReturnMap([
                [
                    'DELETE a, r 
                     FROM opentalent.Audit_table1 a
                     INNER JOIN opentalent.revisions r ON r.id = a.rev
                     WHERE r.timestamp < :maxDate;',
                    $stmt1,
                ],
                [
                    'DELETE a, r 
                     FROM opentalent.Audit_table2 a
                     INNER JOIN opentalent.revisions r ON r.id = a.rev
                     WHERE r.timestamp < :maxDate;',
                    $stmt2,
                ],
            ]);

        $stmt1
            ->expects(self::once())
            ->method('executeStatement')
            ->with(['maxDate' => '2022-01-01'])
            ->willReturn(100);

        $stmt2
            ->expects(self::once())
            ->method('executeStatement')
            ->with(['maxDate' => '2022-01-01'])
            ->willReturn(100);

        $this->assertEquals(200, $cleanDb->purgeAuditTables($maxDateAudit));
    }

    public function testPurgeMessages(): void
    {
        $maxDate = DatesUtils::new('2022-03-02');
        $maxDate->sub(new \DateInterval('P60D'));

        $cleanDb = $this->getMockFor('purgeMessages');

        $stmt1 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
        $stmt2 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();

        $this->connection
            ->expects(self::exactly(2))
            ->method('prepare')
            ->willReturnMap([
                [
                    'DELETE r
                FROM opentalent.Message m
                inner join opentalent.ReportMessage r on r.message_id = m.id
                where (m.dateSent < :maxDate or (m.dateSent is null and m.createDate < :maxDate)) and m.isSystem = true and m.id > 0;',
                    $stmt1,
                ],
                [
                    'DELETE
                FROM opentalent.Message
                where (dateSent < :maxDate or (dateSent is null and createDate < :maxDate)) and isSystem = true and id > 0;',
                    $stmt2,
                ],
            ]);

        $stmt1
            ->expects(self::once())
            ->method('executeStatement')
            ->with(['maxDate' => '2022-01-01'])
            ->willReturn(100);

        $stmt2
            ->expects(self::once())
            ->method('executeStatement')
            ->with(['maxDate' => '2022-01-01'])
            ->willReturn(100);

        $this->assertEquals(200, $cleanDb->purgeMessages($maxDate));
    }

    public function testPurgeNotifications(): void
    {
        $maxDate = DatesUtils::new('2022-03-02');
        $maxDate->sub(new \DateInterval('P60D'));

        $cleanDb = $this->getMockFor('purgeNotifications');

        $stmt1 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();
        $stmt2 = $this->getMockBuilder(Statement::class)->disableOriginalConstructor()->getMock();

        $this->connection
            ->expects(self::exactly(2))
            ->method('prepare')
            ->willReturnMap([
                [
                    "DELETE u
                FROM opentalent.Information i
                inner join opentalent.NotificationUser u on u.notification_id = i.id
                where i.createDate < :maxDate and i.discr = 'notification';",
                    $stmt1,
                ],
                [
                    "DELETE
                FROM opentalent.Information
                where createDate < :maxDate and discr = 'notification';",
                    $stmt2,
                ],
            ]);

        $stmt1
            ->expects(self::once())
            ->method('executeStatement')
            ->with(['maxDate' => '2022-01-01'])
            ->willReturn(100);

        $stmt2
            ->expects(self::once())
            ->method('executeStatement')
            ->with(['maxDate' => '2022-01-01'])
            ->willReturn(100);

        $this->assertEquals(200, $cleanDb->purgeNotifications($maxDate));
    }
}
