<?php

declare(strict_types=1);

namespace App\Tests\Functional\Controller;

use App\Entity\Device;
use App\Entity\Image;
use App\Enum\DeviceModel;
use App\Enum\Orientation;
use App\Tests\Functional\AppWebTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport;

class ImageApiControllerTest extends AppWebTestCase
{
    private string $fixturePath;

    protected function setUp(): void
    {
        parent::setUp();
        $this->fixturePath = dirname(__DIR__, 2) . '/fixtures/test.jpg';
    }

    private function makeUploadedFile(): UploadedFile
    {
        // Copy to a temp file so the upload doesn't consume the original
        $tmp = tempnam(sys_get_temp_dir(), 'img') . '.jpg';
        copy($this->fixturePath, $tmp);
        return new UploadedFile($tmp, 'test.jpg', 'image/jpeg', null, true);
    }

    public function test_upload_creates_image_and_returns_201(): void
    {
        $user   = $this->createUser('upload@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], [
            'file' => $this->makeUploadedFile(),
        ]);

        $this->assertResponseStatusCodeSame(201);
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertArrayHasKey('id', $data);

        $image = $this->em()->find(Image::class, $data['id']);
        $this->assertNotNull($image);
        $this->assertSame($user->getId(), $image->getUser()->getId());
    }

    public function test_upload_dispatches_render_message(): void
    {
        $user   = $this->createUser('upload_msg@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], [
            'file' => $this->makeUploadedFile(),
        ]);

        $this->assertResponseStatusCodeSame(201);

        /** @var InMemoryTransport $transport */
        $transport = static::getContainer()->get('messenger.transport.image_processing');
        $this->assertGreaterThan(0, count($transport->get()), 'Expected RenderImageMessage(s) to be dispatched');
    }

    public function test_upload_without_file_returns_422(): void
    {
        $user   = $this->createUser('noupload@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], [], ['CONTENT_TYPE' => 'application/json'], '{}');

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_list_returns_users_images(): void
    {
        $user  = $this->createUser('listimg@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('GET', '/api/images');

        $this->assertResponseIsSuccessful();
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertCount(1, $data);
        $this->assertSame($image->getId(), $data[0]['id']);
    }

    public function test_delete_soft_deletes_image(): void
    {
        $user  = $this->createUser('delimg@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $imageId = $image->getId();
        $client  = $this->loginAs($user);

        $client->request('DELETE', '/api/images/' . $imageId);

        $this->assertResponseStatusCodeSame(204);

        $this->em()->clear();
        $reloaded = $this->em()->find(Image::class, $imageId);
        $this->assertNotNull($reloaded);
        $this->assertNotNull($reloaded->getDeletedAt());
    }

    public function test_delete_wrong_users_image_returns_404(): void
    {
        $owner   = $this->createUser('delown@example.com');
        $other   = $this->createUser('deloth@example.com');
        $image   = (new Image())->setUser($owner)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($other);

        $client->request('DELETE', '/api/images/' . $image->getId());

        $this->assertResponseStatusCodeSame(404);
    }

    /**
     * IMG-06: reprocess endpoint returns 200 and re-queues render messages.
     *
     * We first upload an image (which creates the RenderedAsset records via the controller),
     * then call reprocess on the same image to verify it resets assets and dispatches new renders.
     */
    public function test_reprocess_returns_200(): void
    {
        $user   = $this->createUser('reprocess@example.com');
        $client = $this->loginAs($user);

        // Step 1: Upload so the controller creates RenderedAssets and storage directory
        $client->request('POST', '/api/images', [], [
            'file' => $this->makeUploadedFile(),
        ]);
        $this->assertResponseStatusCodeSame(201);
        $uploadData = json_decode($client->getResponse()->getContent(), true);
        $imageId    = $uploadData['id'];

        // Step 2: Reprocess
        $client->request('POST', '/api/images/' . $imageId . '/reprocess', [], [
            'file' => $this->makeUploadedFile(),
        ]);

        $this->assertResponseStatusCodeSame(200);
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertSame($imageId, $data['id']);
    }

    public function test_upload_unsupported_mime_returns_422(): void
    {
        $user   = $this->createUser('badmime@example.com');
        $client = $this->loginAs($user);

        $tmp = tempnam(sys_get_temp_dir(), 'txt') . '.txt';
        file_put_contents($tmp, 'This is plain text content');
        $file = new UploadedFile($tmp, 'test.txt', 'text/plain', null, true);

        $client->request('POST', '/api/images', [], ['file' => $file]);

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_thumbnail_returns_200_after_upload(): void
    {
        $user   = $this->createUser('thumb200@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], ['file' => $this->makeUploadedFile()]);
        $this->assertResponseStatusCodeSame(201);
        $data = json_decode($client->getResponse()->getContent(), true);

        $client->request('GET', '/api/images/' . $data['id'] . '/thumbnail');
        $this->assertResponseIsSuccessful();
    }

    public function test_thumbnail_returns_404_for_unknown_image(): void
    {
        $user   = $this->createUser('thumb404@example.com');
        $client = $this->loginAs($user);

        $client->request('GET', '/api/images/999999/thumbnail');
        $this->assertResponseStatusCodeSame(404);
    }

    public function test_thumbnail_returns_404_when_file_not_present(): void
    {
        $user  = $this->createUser('thumbnofile@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('GET', '/api/images/' . $image->getId() . '/thumbnail');
        $this->assertResponseStatusCodeSame(404);
    }

    public function test_original_returns_200_after_upload(): void
    {
        $user   = $this->createUser('orig200@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], ['file' => $this->makeUploadedFile()]);
        $this->assertResponseStatusCodeSame(201);
        $data = json_decode($client->getResponse()->getContent(), true);

        $client->request('GET', '/api/images/' . $data['id'] . '/original');
        $this->assertResponseIsSuccessful();
    }

    public function test_original_returns_404_for_unknown_image(): void
    {
        $user   = $this->createUser('orig404@example.com');
        $client = $this->loginAs($user);

        $client->request('GET', '/api/images/999999/original');
        $this->assertResponseStatusCodeSame(404);
    }

    public function test_share_success_returns_204(): void
    {
        $sender    = $this->createUser('sharesend@example.com');
        $recipient = $this->createUser('sharerecip@example.com');

        $image = (new Image())->setUser($sender)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();

        $client = $this->loginAs($sender);
        $client->request('POST', '/api/images/' . $image->getId() . '/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'sharerecip@example.com']));

        $this->assertResponseStatusCodeSame(204);
    }

    public function test_share_invalid_email_returns_422(): void
    {
        $user  = $this->createUser('shareinvalid@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/' . $image->getId() . '/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'not-an-email']));

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_share_nonexistent_recipient_returns_422(): void
    {
        $user  = $this->createUser('sharenorecip@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/' . $image->getId() . '/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'nobody@example.com']));

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_share_nonexistent_image_returns_404(): void
    {
        $user   = $this->createUser('sharenoimg@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/999999/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'someone@example.com']));

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_hard_delete_request_returns_204(): void
    {
        $user  = $this->createUser('hdrequser@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/' . $image->getId() . '/hard-delete-request');

        $this->assertResponseStatusCodeSame(204);
    }

    public function test_hard_delete_request_unknown_image_returns_404(): void
    {
        $user   = $this->createUser('hdrnotfound@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/999999/hard-delete-request');

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_approve_and_revoke_device(): void
    {
        $user   = $this->createUser('approvrevoke@example.com');
        $device = new Device();
        $device->setMac('AA:BB:CC:FF:01:01');
        $device->setName('Frame');
        $device->setUser($user);
        $device->setModel(DeviceModel::V1);
        $device->setOrientation(Orientation::Landscape);
        $this->em()->persist($device);

        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();

        $client = $this->loginAs($user);

        // Approve
        $client->request('POST', '/api/images/' . $image->getId() . '/approve/' . $device->getId());
        $this->assertResponseIsSuccessful();
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertContains($device->getId(), $data['approvedDeviceIds']);

        // Revoke
        $client->request('DELETE', '/api/images/' . $image->getId() . '/approve/' . $device->getId());
        $this->assertResponseIsSuccessful();
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertNotContains($device->getId(), $data['approvedDeviceIds']);
    }

    public function test_approve_unknown_device_returns_404(): void
    {
        $user  = $this->createUser('apprunkndev@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/' . $image->getId() . '/approve/999999');

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_approve_unknown_image_returns_404(): void
    {
        $user   = $this->createUser('apprunknimg@example.com');
        $device = new Device();
        $device->setMac('AA:BB:CC:FF:02:01');
        $device->setName('Frame2');
        $device->setUser($user);
        $device->setModel(DeviceModel::V1);
        $device->setOrientation(Orientation::Landscape);
        $this->em()->persist($device);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/999999/approve/' . $device->getId());

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_revoke_unknown_image_returns_404(): void
    {
        $user   = $this->createUser('revokeunknimg@example.com');
        $device = new Device();
        $device->setMac('AA:BB:CC:FF:03:01');
        $device->setName('Frame3');
        $device->setUser($user);
        $device->setModel(DeviceModel::V1);
        $device->setOrientation(Orientation::Landscape);
        $this->em()->persist($device);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('DELETE', '/api/images/999999/approve/' . $device->getId());

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_upload_too_large_returns_422(): void
    {
        $user   = $this->createUser('toolarge@example.com');
        $client = $this->loginAs($user);

        // Create a file > MAX_BYTES (30 MB).
        // Use UPLOAD_ERR_CANT_WRITE so HttpKernelBrowser::filterFiles() does not replace
        // it with an empty-path stub (filterFiles only replaces *valid* oversized files).
        // The controller's $file->getSize() check still sees the real file size.
        $tmp = tempnam(sys_get_temp_dir(), 'large') . '.jpg';
        $fp  = fopen($tmp, 'wb');
        $chunk = str_repeat("\0", 1024 * 256); // 256 KB chunks
        for ($i = 0; $i < 121; $i++) {         // 121 × 256 KB ≈ 31 MB > MAX_BYTES
            fwrite($fp, $chunk);
        }
        fclose($fp);
        unset($chunk);

        $file = new UploadedFile($tmp, 'large.jpg', 'image/jpeg', \UPLOAD_ERR_CANT_WRITE, true);
        $client->request('POST', '/api/images', [], ['file' => $file]);
        @unlink($tmp);

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_upload_with_composited_and_original(): void
    {
        $user   = $this->createUser('uploadcomp@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], [
            'file'     => $this->makeUploadedFile(),
            'original' => $this->makeUploadedFile(),
        ]);

        $this->assertResponseStatusCodeSame(201);
    }

    public function test_upload_with_crop_and_sticker_params(): void
    {
        $user   = $this->createUser('uploadmeta@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [
            'cropParams'   => '{"x":0,"y":0,"w":100,"h":100}',
            'stickerState' => '{"stickers":[]}',
            'cropOrientation' => 'portrait',
        ], [
            'file' => $this->makeUploadedFile(),
        ]);

        $this->assertResponseStatusCodeSame(201);
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertNotNull($data['cropParams']);
        $this->assertNotNull($data['stickerState']);
        $this->assertSame('portrait', $data['cropOrientation']);
    }

    public function test_original_returns_404_when_no_file_on_disk(): void
    {
        $user  = $this->createUser('orignofile@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('GET', '/api/images/' . $image->getId() . '/original');

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_reprocess_unknown_image_returns_404(): void
    {
        $user   = $this->createUser('repronotfound@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/999999/reprocess', [], ['file' => $this->makeUploadedFile()]);

        $this->assertResponseStatusCodeSame(404);
    }

    public function test_reprocess_without_file_returns_422(): void
    {
        $user   = $this->createUser('reprocessnofile@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], ['file' => $this->makeUploadedFile()]);
        $this->assertResponseStatusCodeSame(201);
        $imageId = json_decode($client->getResponse()->getContent(), true)['id'];

        $client->request('POST', '/api/images/' . $imageId . '/reprocess');

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_reprocess_with_crop_and_sticker_metadata(): void
    {
        $user   = $this->createUser('reprocessmeta@example.com');
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images', [], ['file' => $this->makeUploadedFile()]);
        $this->assertResponseStatusCodeSame(201);
        $imageId = json_decode($client->getResponse()->getContent(), true)['id'];

        $client->request('POST', '/api/images/' . $imageId . '/reprocess', [
            'cropParams'   => '{"x":10,"y":10}',
            'stickerState' => '{"stickers":[]}',
            'cropOrientation' => 'landscape',
        ], [
            'file' => $this->makeUploadedFile(),
        ]);

        $this->assertResponseStatusCodeSame(200);
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertNotNull($data['cropParams']);
        $this->assertNotNull($data['stickerState']);
        $this->assertSame('landscape', $data['cropOrientation']);
    }

    public function test_share_with_self_returns_422(): void
    {
        $user  = $this->createUser('shareself@example.com');
        $image = (new Image())->setUser($user)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($user);

        $client->request('POST', '/api/images/' . $image->getId() . '/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'shareself@example.com']));

        $this->assertResponseStatusCodeSame(422);
    }

    public function test_share_idempotent_returns_204(): void
    {
        $sender    = $this->createUser('shareidem_send@example.com');
        $recipient = $this->createUser('shareidem_recip@example.com');
        $image     = (new Image())->setUser($sender)->setOriginalFilename('x.jpg')->setStoragePath('x');
        $this->em()->persist($image);
        $this->em()->flush();
        $client = $this->loginAs($sender);

        $client->request('POST', '/api/images/' . $image->getId() . '/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'shareidem_recip@example.com']));
        $this->assertResponseStatusCodeSame(204);

        // Second call hits the idempotent ($existing) return path
        $client->request('POST', '/api/images/' . $image->getId() . '/share', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode(['recipientEmail' => 'shareidem_recip@example.com']));
        $this->assertResponseStatusCodeSame(204);
    }
}
