<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Tests\Notification\Email;

use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Statement;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * @internal
 *
 * @covers \DailyNotificationDigestCronjob
 */
class DailyNotificationDigestCronJobTest extends TestCase
{
    private ClockInterface $clock;
    private Connection $connection;
    private MessageBusInterface $messageBus;
    private LoggerInterface $logger;
    private DailyNotificationDigestCronjob $cronjob;
    private \DateTimeImmutable $firstNow;

    protected function setUp(): void
    {
        $this->clock = $this->createMock(ClockInterface::class);
        $this->connection = $this->createMock(Connection::class);
        $this->messageBus = $this->createMock(MessageBusInterface::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->firstNow = new \DateTimeImmutable('2024-01-02T07:15:00+00:00');

        $this->cronjob = new DailyNotificationDigestCronjob(
            $this->clock,
            $this->connection,
            $this->messageBus,
            $this->logger
        );
    }

    public function testGetKey(): void
    {
        $this->assertEquals('daily-notification-digest', $this->cronjob->getKey());
    }

    /**
     * @dataProvider canRunTimeDataProvider
     */
    public function testCanRunWithNullCronJobExecution(int $hour, bool $expected): void
    {
        $now = new \DateTimeImmutable("2024-01-01 {$hour}:00:00");
        $this->clock->expects($this->once())
            ->method('now')
            ->willReturn($now);

        $result = $this->cronjob->canRun(null);

        $this->assertEquals($expected, $result);
    }

    public static function canRunTimeDataProvider(): array
    {
        return [
            'hour 5 - should not run' => [5, false],
            'hour 6 - should run' => [6, true],
            'hour 7 - should run' => [7, true],
            'hour 8 - should run' => [8, true],
            'hour 9 - should not run' => [9, false],
            'hour 10 - should not run' => [10, false],
            'hour 23 - should not run' => [23, false],
        ];
    }

    public function testRunFirstExecutionReturnsStateAndDispatches(): array
    {
        // Use MockClock for deterministic time
        $firstNow = $this->firstNow;
        $clock = new MockClock($firstNow);

        // Mock DBAL statement/result
        $statement = $this->createMock(Statement::class);
        $result = $this->createMock(Result::class);

        $this->connection->method('prepare')->willReturn($statement);
        $statement->method('bindValue')->willReturnSelf();
        $statement->method('executeQuery')->willReturn($result);

        $rows = [
            ['user_id' => 10],
            ['user_id' => 42],
        ];
        $result->method('fetchAllAssociative')->willReturn($rows);

        $dispatched = [];
        $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$dispatched) {
            $dispatched[] = $message;

            return new Envelope($message);
        });

        $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger);
        $state = $cron->run([]);

        // Assert dispatch count and message contents
        self::assertCount(2, $dispatched);
        $expectedLast = $firstNow->sub(new \DateInterval('P1D'));
        foreach ($dispatched as $i => $msg) {
            self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg);
            self::assertTrue(in_array($msg->getUserId(), [10, 42], true));
            self::assertEquals($firstNow, $msg->getCurrentDateTime(), 'compare the current date');
            self::assertEquals($expectedLast, $msg->getLastExecutionDateTime(), 'compare the last execution date');
        }

        // Assert returned state
        self::assertIsArray($state);
        self::assertArrayHasKey('last_execution', $state);
        self::assertSame($firstNow->format(\DateTimeInterface::ATOM), $state['last_execution']);

        return $state;
    }

    /**
     * @depends testRunFirstExecutionReturnsStateAndDispatches
     */
    public function testRunSecondExecutionUsesPreviousState(array $previousState): void
    {
        $firstNow = $this->firstNow;
        $secondNow = $firstNow->add(new \DateInterval('P1D'));
        $clock = new MockClock($secondNow);

        // Mock DBAL for a single user this time
        $statement = $this->createMock(Statement::class);
        $result = $this->createMock(Result::class);

        $this->connection->method('prepare')->willReturn($statement);
        $statement->method('bindValue')->willReturnSelf();
        $statement->method('executeQuery')->willReturn($result);

        $rows = [
            ['user_id' => 7],
        ];
        $result->method('fetchAllAssociative')->willReturn($rows);

        $captured = [];
        $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$captured) {
            $captured[] = $message;

            return new Envelope($message);
        });

        $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger);
        $cron->run($previousState);

        self::assertCount(1, $captured);
        $msg = $captured[0];
        self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg);
        self::assertEquals(7, $msg->getUserId());
        self::assertEquals($secondNow, $msg->getCurrentDateTime(), 'compare the current date');
        self::assertEquals($firstNow, $msg->getLastExecutionDateTime(), 'compare the last execution date');
    }

    public function testRunWithInvalidExecutionState(): void
    {
        $firstNow = new \DateTimeImmutable('2025-10-14T10:30:00 Europe/Brussels');
        $previousExpected = $firstNow->sub(new \DateInterval('P1D'));
        $clock = new MockClock($firstNow);

        // Mock DBAL for a single user this time
        $statement = $this->createMock(Statement::class);
        $result = $this->createMock(Result::class);

        $this->connection->method('prepare')->willReturn($statement);
        $statement->method('bindValue')->willReturnSelf();
        $statement->method('executeQuery')->willReturn($result);

        $rows = [
            ['user_id' => 7],
        ];
        $result->method('fetchAllAssociative')->willReturn($rows);

        $captured = [];
        $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$captured) {
            $captured[] = $message;

            return new Envelope($message);
        });

        $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger);
        $cron->run(['last_execution' => 'invalid data']);

        self::assertCount(1, $captured);
        $msg = $captured[0];
        self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg);
        self::assertEquals(7, $msg->getUserId());
        self::assertEquals($firstNow, $msg->getCurrentDateTime(), 'compare the current date');
        self::assertEquals($previousExpected, $msg->getLastExecutionDateTime(), 'compare the last execution date');
    }
}
