<?php

declare(strict_types=1);

use App\Data\Images\CropBoxData;
use App\Services\Images\Detectors\HeuristicSubjectDetector;
use App\Services\Images\Detectors\NullSubjectDetector;
use App\Services\Images\SquareThumbnailService;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Tests\TestCase;

uses(TestCase::class);

beforeEach(function () {
    $this->imageTestRoot = storage_path('framework/testing/square-thumbnails-' . Str::lower(Str::random(10)));
    File::deleteDirectory($this->imageTestRoot);
    File::ensureDirectoryExists($this->imageTestRoot);
});

afterEach(function () {
    File::deleteDirectory($this->imageTestRoot);
});

function squareThumbCreateImage(string $root, int $width, int $height, ?callable $drawer = null): string
{
    $path = $root . DIRECTORY_SEPARATOR . Str::uuid()->toString() . '.png';
    $image = imagecreatetruecolor($width, $height);
    $background = imagecolorallocate($image, 22, 28, 36);
    imagefilledrectangle($image, 0, 0, $width, $height, $background);

    if ($drawer !== null) {
        $drawer($image, $width, $height);
    }

    imagepng($image, $path);
    imagedestroy($image);

    return $path;
}

it('calculates a padded square crop and clamps it to image bounds', function () {
    $service = new SquareThumbnailService(new NullSubjectDetector());

    $crop = $service->calculateCropBox(1200, 800, new CropBoxData(10, 40, 260, 180), [
        'padding_ratio' => 0.2,
    ]);

    expect($crop->width)->toBe($crop->height)
        ->and($crop->x)->toBe(0)
        ->and($crop->y)->toBeGreaterThanOrEqual(0)
        ->and($crop->x + $crop->width)->toBeLessThanOrEqual(1200)
        ->and($crop->y + $crop->height)->toBeLessThanOrEqual(800);
});

it('falls back to a centered square crop when no focus box exists', function () {
    $service = new SquareThumbnailService(new NullSubjectDetector());

    $crop = $service->calculateCropBox(900, 500, null);

    expect($crop->width)->toBe(500)
        ->and($crop->height)->toBe(500)
        ->and($crop->x)->toBe(200)
        ->and($crop->y)->toBe(0);
});

it('generates a valid square thumbnail when smart detection is unavailable', function () {
    $source = squareThumbCreateImage($this->imageTestRoot, 640, 320, function ($image): void {
        $accent = imagecolorallocate($image, 240, 200, 80);
        imagefilledellipse($image, 320, 160, 180, 180, $accent);
    });
    $destination = $this->imageTestRoot . DIRECTORY_SEPARATOR . 'output.webp';

    $service = new SquareThumbnailService(new NullSubjectDetector());
    $result = $service->generateFromPath($source, $destination, ['target_size' => 256]);
    $size = getimagesize($destination);

    expect(File::exists($destination))->toBeTrue()
        ->and($result->cropMode)->toBe('center')
        ->and($size[0] ?? null)->toBe(256)
        ->and($size[1] ?? null)->toBe(256);
});

it('does not upscale tiny images when allow_upscale is disabled', function () {
    $source = squareThumbCreateImage($this->imageTestRoot, 60, 40, function ($image): void {
        $accent = imagecolorallocate($image, 220, 120, 120);
        imagefilledrectangle($image, 4, 4, 36, 32, $accent);
    });
    $destination = $this->imageTestRoot . DIRECTORY_SEPARATOR . 'tiny.webp';

    $service = new SquareThumbnailService(new NullSubjectDetector());
    $result = $service->generateFromPath($source, $destination, [
        'target_size' => 256,
        'allow_upscale' => false,
    ]);
    $size = getimagesize($destination);

    expect($result->outputWidth)->toBe(40)
        ->and($result->outputHeight)->toBe(40)
        ->and($size[0] ?? null)->toBe(40)
        ->and($size[1] ?? null)->toBe(40);
});

it('can derive a saliency crop near the border', function () {
    $source = squareThumbCreateImage($this->imageTestRoot, 900, 600, function ($image): void {
        $subject = imagecolorallocate($image, 250, 240, 240);
        imagefilledellipse($image, 120, 240, 180, 220, $subject);
    });
    $detector = new HeuristicSubjectDetector();
    $size = getimagesize($source);

    $result = $detector->detect($source, (int) $size[0], (int) $size[1]);

    expect($result)->not->toBeNull()
        ->and($result?->strategy)->toBe('saliency')
        ->and($result?->cropBox->x)->toBeLessThan(220);
});

it('prefers a distinct subject over textured foliage', function () {
    $source = squareThumbCreateImage($this->imageTestRoot, 1200, 700, function ($image): void {
        $sky = imagecolorallocate($image, 165, 205, 255);
        $leaf = imagecolorallocate($image, 42, 118, 34);
        $leafDark = imagecolorallocate($image, 28, 88, 24);
        $branch = imagecolorallocate($image, 96, 72, 52);
        $fur = imagecolorallocate($image, 242, 236, 228);
        $ginger = imagecolorallocate($image, 214, 152, 102);
        $mouth = imagecolorallocate($image, 36, 20, 18);

        imagefilledrectangle($image, 0, 0, 1200, 700, $sky);

        for ($i = 0; $i < 28; $i++) {
            imageline($image, rand(0, 520), rand(0, 340), rand(80, 620), rand(80, 420), $branch);
            imagefilledellipse($image, rand(40, 560), rand(80, 620), rand(60, 120), rand(20, 60), $leaf);
            imagefilledellipse($image, rand(40, 560), rand(80, 620), rand(40, 90), rand(16, 44), $leafDark);
        }

        imagefilledellipse($image, 890, 300, 420, 500, $fur);
        imagefilledpolygon($image, [760, 130, 840, 10, 865, 165], 3, $ginger);
        imagefilledpolygon($image, [930, 165, 1000, 10, 1070, 130], 3, $ginger);
        imagefilledellipse($image, 885, 355, 150, 220, $mouth);
        imagefilledellipse($image, 890, 520, 180, 130, $fur);
    });

    $detector = new HeuristicSubjectDetector();
    $size = getimagesize($source);

    $result = $detector->detect($source, (int) $size[0], (int) $size[1]);

    expect($result)->not->toBeNull()
        ->and($result?->strategy)->toBe('saliency')
        ->and($result?->cropBox->x)->toBeGreaterThan(180);
});