<?php

namespace Tests\Feature;

use App\Models\Asset;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

/**
 * Serving a creative back to the person who owns it.
 *
 * Uploads sit on the local disk and generated creatives on the public one, so
 * previewing meant either publishing every upload or showing half of them as a
 * placeholder. This route is the third option, and being a route it is now an
 * authorisation boundary, which is the part worth covering.
 */
class AssetPreviewTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

    protected function setUp(): void
    {
        parent::setUp();

        Storage::fake('local');
        Storage::fake('public');

        $this->user = User::factory()->create();
    }

    private function asset(?User $owner, string $disk, string $path = 'assets/one.png'): Asset
    {
        Storage::disk($disk)->put($path, 'the-bytes');

        return Asset::create([
            'owner_type' => $owner?->getMorphClass(),
            'owner_id' => $owner?->getKey(),
            'kind' => 'image',
            'source' => $disk === 'public' ? 'generated' : 'upload',
            'disk' => $disk,
            'path' => $path,
            'original_name' => 'one.png',
            'mime' => 'image/png',
            'bytes' => 9,
        ]);
    }

    public function test_the_owner_sees_an_upload_from_the_local_disk(): void
    {
        $asset = $this->asset($this->user, 'local');

        $response = $this->actingAs($this->user)
            ->get(route('assets.preview', $asset))
            ->assertOk()
            ->assertHeader('content-type', 'image/png');

        $this->assertSame('the-bytes', $response->streamedContent());
    }

    public function test_the_owner_sees_a_generated_creative_from_the_public_disk(): void
    {
        $asset = $this->asset($this->user, 'public', 'creatives/images/two.png');

        $this->actingAs($this->user)
            ->get(route('assets.preview', $asset))
            ->assertOk();
    }

    /** Colleagues share creatives; the route keeps them off the public internet. */
    public function test_a_colleague_can_see_another_users_creative(): void
    {
        $asset = $this->asset($this->user, 'local');

        $this->actingAs(User::factory()->create())
            ->get(route('assets.preview', $asset))
            ->assertOk();
    }

    public function test_a_guest_cannot_see_a_creative(): void
    {
        $asset = $this->asset($this->user, 'local');

        $this->get(route('assets.preview', $asset))->assertRedirect(route('login'));
    }

    /** An asset with no owner is still served to a signed-in colleague. */
    public function test_an_unowned_asset_is_served_to_a_signed_in_user(): void
    {
        $asset = $this->asset(null, 'local');

        $this->actingAs($this->user)
            ->get(route('assets.preview', $asset))
            ->assertOk();
    }

    /** A row whose file has gone must 404, not error. */
    public function test_a_missing_file_is_not_found(): void
    {
        $asset = $this->asset($this->user, 'local');
        Storage::disk('local')->delete($asset->path);

        $this->actingAs($this->user)
            ->get(route('assets.preview', $asset))
            ->assertNotFound();
    }

    /**
     * A big creative is served small.
     *
     * The generator writes 8MB JPEGs and the panel renders them at 56 pixels,
     * so two ads pulled 16MB before this. The thumbnail is written once and
     * reused.
     */
    public function test_a_large_image_is_served_as_a_thumbnail(): void
    {
        $asset = $this->wideJpeg();

        $response = $this->actingAs($this->user)->get(route('assets.preview', $asset))->assertOk();

        $served = strlen($response->streamedContent());
        $original = strlen(Storage::disk('local')->get($asset->path));

        $this->assertLessThan($original, $served, 'The thumbnail should be smaller than the original.');
        $this->assertTrue(Storage::disk('local')->exists('thumbnails/'.$asset->id.'-320.jpg'));
    }

    /**
     * A thumbnail must never land on the web served disk.
     *
     * Asset ids are sequential, so a thumbnail under the public disk would sit
     * at a guessable URL and undo the reason this route exists.
     */
    public function test_a_thumbnail_of_a_public_disk_creative_is_kept_private(): void
    {
        $image = imagecreatetruecolor(1600, 900);
        ob_start();
        imagejpeg($image, null, 92);
        $bytes = (string) ob_get_clean();

        Storage::disk('public')->put('creatives/images/big.jpg', $bytes);

        $asset = Asset::create([
            'owner_type' => $this->user->getMorphClass(),
            'owner_id' => $this->user->getKey(),
            'kind' => 'image',
            'source' => 'generated',
            'disk' => 'public',
            'path' => 'creatives/images/big.jpg',
            'original_name' => 'big.jpg',
            'mime' => 'image/jpeg',
            'bytes' => strlen($bytes),
        ]);

        $this->actingAs($this->user)->get(route('assets.preview', $asset))->assertOk();

        $this->assertTrue(Storage::disk('local')->exists('thumbnails/'.$asset->id.'-320.jpg'));
        $this->assertFalse(
            Storage::disk('public')->exists('thumbnails/'.$asset->id.'-320.jpg'),
            'A thumbnail must not be written to the web served disk.',
        );
    }

    /** The original stays reachable, for anyone who needs the real file. */
    public function test_the_full_image_can_still_be_asked_for(): void
    {
        $asset = $this->wideJpeg();

        $response = $this->actingAs($this->user)
            ->get(route('assets.preview', $asset).'?full=1')
            ->assertOk();

        $this->assertSame(
            strlen(Storage::disk('local')->get($asset->path)),
            strlen($response->streamedContent()),
        );
    }

    private function wideJpeg(): Asset
    {
        $image = imagecreatetruecolor(1600, 900);
        imagefilledrectangle($image, 0, 0, 1600, 900, imagecolorallocate($image, 40, 90, 200));
        ob_start();
        imagejpeg($image, null, 92);
        $bytes = (string) ob_get_clean();
        imagedestroy($image);

        Storage::disk('local')->put('assets/wide.jpg', $bytes);

        return Asset::create([
            'owner_type' => $this->user->getMorphClass(),
            'owner_id' => $this->user->getKey(),
            'kind' => 'image',
            'source' => 'upload',
            'disk' => 'local',
            'path' => 'assets/wide.jpg',
            'original_name' => 'wide.jpg',
            'mime' => 'image/jpeg',
            'bytes' => strlen($bytes),
        ]);
    }

    /** Shared caches must not hold one user's creative. */
    public function test_the_response_is_not_publicly_cacheable(): void
    {
        $asset = $this->asset($this->user, 'local');

        $this->actingAs($this->user)
            ->get(route('assets.preview', $asset))
            ->assertHeader('cache-control', 'max-age=300, private');
    }
}
