<?php

declare(strict_types=1);

namespace App\Tests\Integration\MessageHandler;

use App\Entity\Device;
use App\Entity\DeviceImageHistory;
use App\Entity\Image;
use App\Entity\RenderedAsset;
use App\Enum\DeviceModel;
use App\Enum\Orientation;
use App\Enum\RenderStatus;
use App\Message\AdvanceRotationMessage;
use App\MessageHandler\AdvanceRotationMessageHandler;
use App\Repository\DeviceImageHistoryRepository;
use App\Tests\AppKernelTestCase;

class AdvanceRotationMessageHandlerTest extends AppKernelTestCase
{
    private int $deviceSeq = 0;

    private function invokeHandler(): void
    {
        $handler = static::getContainer()->get(AdvanceRotationMessageHandler::class);
        $handler(new AdvanceRotationMessage());
    }

    private function makeDevice(int $intervalMinutes = 60): Device
    {
        $seq   = ++$this->deviceSeq;
        $mac   = sprintf('AA:BB:CC:%02X:%02X:%02X', 10, 0, $seq);
        $user  = $this->createUser('ar' . $seq . '@example.com');
        $device = new Device();
        $device->setMac($mac)->setName('Frame ' . $seq)->setUser($user)
               ->setRotationIntervalMinutes($intervalMinutes);
        $this->em()->persist($device);
        return $device;
    }

    private function makeReadyImage(Device $device): Image
    {
        $image = (new Image())->setUser($device->getUser())->setOriginalFilename('x.jpg')->setStoragePath('x');
        $image->approveForDevice($device);
        $this->em()->persist($image);

        $asset = (new RenderedAsset())
            ->setImage($image)
            ->setDeviceModel($device->getModel() ?? DeviceModel::V1)
            ->setOrientation($device->getOrientation() ?? Orientation::Landscape)
            ->setStatus(RenderStatus::Ready)
            ->setFilePath('var/storage/dummy.bin');
        $this->em()->persist($asset);

        return $image;
    }

    // AR-01: due device (no history) with no ready images → advance returns null
    public function test_ar01_due_with_no_images_does_not_rotate(): void
    {
        $device = $this->makeDevice();
        $this->em()->flush();

        // Ensure repository constructor is covered
        $this->em()->getRepository(DeviceImageHistory::class);

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $device->getId());
        $this->assertNull($reloaded->getCurrentImage());
    }

    // AR-02: interval-based device with recent history → not due → rotation skipped
    public function test_ar02_recent_history_is_not_due(): void
    {
        $device = $this->makeDevice(60);
        $image  = $this->makeReadyImage($device);
        $this->em()->flush();

        $history = new DeviceImageHistory($device, $image);
        $this->em()->persist($history);
        $device->setCurrentImage($image);
        $this->em()->flush();

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $device->getId());
        // currentImage was set before handler; rotation should not have occurred again
        $this->assertNotNull($reloaded->getCurrentImage());
        $this->assertSame($image->getId(), $reloaded->getCurrentImage()->getId());
    }

    // AR-03: interval-based device with old history → due → rotation occurs
    public function test_ar03_old_history_triggers_rotation(): void
    {
        $device = $this->makeDevice(1); // 1-minute interval
        $image  = $this->makeReadyImage($device);
        $this->em()->flush();

        $deviceId = $device->getId();

        $history = new DeviceImageHistory($device, $image);
        $this->em()->persist($history);
        $this->em()->flush();

        // Backdate history to 2 minutes ago (older than 1-minute interval).
        // Clear the identity map afterward — DQL bulk UPDATE bypasses the entity cache.
        $this->em()->createQuery(
            'UPDATE App\Entity\DeviceImageHistory h SET h.servedAt = :old WHERE h.device = :dev'
        )->setParameters(['old' => new \DateTimeImmutable('-2 minutes'), 'dev' => $device])->execute();
        $this->em()->clear();

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $deviceId);
        $this->assertNotNull($reloaded->getCurrentImage());
    }

    // AR-04: wakeTimes=[00:00] (always past) + no history today → rotation occurs
    public function test_ar04_wake_time_past_no_history_rotates(): void
    {
        $device = $this->makeDevice();
        $device->setWakeTimes([0])->setTimezone('UTC');
        $image  = $this->makeReadyImage($device);
        $this->em()->flush();

        $deviceId = $device->getId();
        $imageId  = $image->getId();

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $deviceId);
        $this->assertNotNull($reloaded->getCurrentImage());
        $this->assertSame($imageId, $reloaded->getCurrentImage()->getId());
    }

    // AR-05: wakeTimes=[00:00] + history exists since midnight → already served today → not due
    public function test_ar05_wake_time_already_served_today_is_skipped(): void
    {
        $device = $this->makeDevice();
        $device->setWakeTimes([0])->setTimezone('UTC');
        $image  = $this->makeReadyImage($device);
        $this->em()->flush();

        // History entry timestamped just now (after midnight UTC → considered "today's wake")
        $history = new DeviceImageHistory($device, $image);
        $this->em()->persist($history);
        $device->setCurrentImage($image);
        $this->em()->flush();

        $deviceId = $device->getId();
        $imageId  = $image->getId();

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $deviceId);
        // currentImage set before; rotation should not have happened again
        $this->assertSame($imageId, $reloaded->getCurrentImage()?->getId());
    }

    // AR-06: wakeTime in future → isDue returns false → no rotation
    // Uses 'Etc/GMT+11' (UTC-11) so local time is always before 23:00 local
    // except during UTC 09:00-10:59; test is skipped then.
    public function test_ar06_wake_time_in_future_is_not_due(): void
    {
        $utcHour = (int)(new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('G');
        if ($utcHour >= 9 && $utcHour <= 10) {
            $this->markTestSkipped('Time-dependent test skipped during UTC 09:xx-10:xx boundary hour');
        }

        $device = $this->makeDevice();
        // UTC-11: local time is at most 12:59 when UTC is 23:59 → 23:00 always future
        $device->setWakeTimes([23 * 60])->setTimezone('Etc/GMT+11');
        $image  = $this->makeReadyImage($device);
        $this->em()->flush();

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $device->getId());
        $this->assertNull($reloaded->getCurrentImage(), 'Rotation must not happen when wake time is still in the future');
    }

    // AR-07: multiple wakeTimes — 00:00 has passed, so device is due even
    // though later slots haven't fired yet. Validates that we use the most
    // recent past slot as the boundary, not the earliest.
    public function test_ar07_multiple_wake_times_uses_most_recent_past_slot(): void
    {
        $device = $this->makeDevice();
        // 00:00 always past, 23:00 future for most of the day
        $device->setWakeTimes([0, 23 * 60])->setTimezone('UTC');
        $image  = $this->makeReadyImage($device);
        $this->em()->flush();

        $this->invokeHandler();

        $this->em()->clear();
        $reloaded = $this->em()->find(Device::class, $device->getId());
        $this->assertNotNull(
            $reloaded->getCurrentImage(),
            'Device with multiple wake times should rotate when at least one has passed today and no history exists since',
        );
    }
}
