<?php

namespace Tests\Feature;

use App\Assets\AssetLibrary;
use App\Models\Asset;
use App\Models\Creative;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

/**
 * A generated creative has to be able to become an ad.
 *
 * The wizard wrote to creatives and publishing read from assets, with nothing
 * joining them, so everything the creative generator produced was unreachable
 * from a campaign. These cover the join and the two things that made it subtle:
 * the halves store their files on different disks, and a creative is saved
 * several times before it is finished.
 */
class CreativeToAssetTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

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

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

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

    private function creative(array $attributes = []): Creative
    {
        return Creative::create(array_merge([
            'user_id' => $this->user->id,
            'type' => 'image',
            'status' => 'generating',
            'source' => 'prompt',
            'prompt' => 'a clean product shot',
        ], $attributes));
    }

    public function test_a_finished_creative_becomes_an_asset_without_anyone_asking(): void
    {
        Storage::disk('public')->put('creatives/images/one.png', 'the-bytes');

        $creative = $this->creative();
        $creative->update(['status' => 'completed', 'asset_path' => 'creatives/images/one.png']);

        $asset = $creative->fresh()->asset;

        $this->assertNotNull($asset, 'A completed creative should have produced an asset.');
        $this->assertSame('generated', $asset->source);
        $this->assertSame('image', $asset->kind);
        $this->assertSame($this->user->id, $asset->owner_id);
    }

    /**
     * The disk is the part that broke silently.
     *
     * Uploads live on the default disk and generated creatives on the public
     * one, and the upload to Meta read the local disk for everything, so a
     * generated asset failed only at publish time.
     */
    public function test_the_asset_records_which_disk_holds_the_file(): void
    {
        Storage::disk('public')->put('creatives/videos/two.mp4', 'video-bytes');

        $creative = $this->creative(['type' => 'video']);
        $creative->update(['status' => 'completed', 'asset_path' => 'creatives/videos/two.mp4']);

        $asset = $creative->fresh()->asset;

        $this->assertSame('public', $asset->disk);
        $this->assertSame('video', $asset->kind);
        $this->assertTrue(Storage::disk($asset->disk)->exists($asset->path));
    }

    public function test_an_upload_records_the_default_disk(): void
    {
        $asset = app(AssetLibrary::class)->fromUpload(
            UploadedFile::fake()->image('shoe.jpg'),
            $this->user,
        );

        $this->assertSame(config('filesystems.default'), $asset->disk);
        $this->assertSame('upload', $asset->source);
    }

    /** A creative is saved several times on the way to finishing. */
    public function test_a_creative_still_rendering_produces_nothing(): void
    {
        $creative = $this->creative();
        $creative->update(['status' => 'processing']);

        $this->assertNull($creative->fresh()->asset_id);
        $this->assertSame(0, Asset::count());
    }

    public function test_a_failed_render_produces_nothing(): void
    {
        $creative = $this->creative();
        $creative->update(['status' => 'failed', 'failure_reason' => 'Generation failed. Please try again.']);

        $this->assertNull($creative->fresh()->asset_id);
        $this->assertSame(0, Asset::count());
    }

    /**
     * A creative claiming a file that is not there must not become an asset.
     *
     * Otherwise the failure moves to the upload to Meta, which is both later
     * and much harder to read.
     */
    public function test_a_missing_file_produces_nothing(): void
    {
        $creative = $this->creative();
        $creative->update(['status' => 'completed', 'asset_path' => 'creatives/images/gone.png']);

        $this->assertNull($creative->fresh()->asset_id);
        $this->assertSame(0, Asset::count());
    }

    public function test_a_creative_saved_again_does_not_make_a_second_asset(): void
    {
        Storage::disk('public')->put('creatives/images/three.png', 'the-bytes');

        $creative = $this->creative();
        $creative->update(['status' => 'completed', 'asset_path' => 'creatives/images/three.png']);

        $first = $creative->fresh()->asset_id;

        $creative->fresh()->update(['style' => 'bold']);
        $creative->fresh()->touch();

        $this->assertSame($first, $creative->fresh()->asset_id);
        $this->assertSame(1, Asset::count());
    }

    /**
     * Regenerating must produce a new asset, not keep the old one.
     *
     * The regenerate endpoint clears asset_path and writes a new render, but
     * leaves asset_id pointing at the previous asset. Treating "has an asset"
     * as "nothing to do" left the campaign serving the image that had just been
     * replaced, silently and on a live campaign.
     */
    public function test_regenerating_a_creative_produces_a_new_asset(): void
    {
        Storage::disk('public')->put('creatives/images/first.png', 'first-bytes');

        $creative = $this->creative();
        $creative->update(['status' => 'completed', 'asset_path' => 'creatives/images/first.png']);

        $firstAsset = $creative->fresh()->asset_id;
        $this->assertNotNull($firstAsset);

        // What regenerate does: clear the path, keep asset_id, then a new render lands.
        $creative->fresh()->update(['status' => 'generating', 'asset_path' => null]);
        Storage::disk('public')->put('creatives/images/second.png', 'second-bytes');
        $creative->fresh()->update(['status' => 'completed', 'asset_path' => 'creatives/images/second.png']);

        $after = $creative->fresh();

        $this->assertNotSame($firstAsset, $after->asset_id, 'A regenerated creative should point at a new asset.');
        $this->assertSame('creatives/images/second.png', $after->asset->path);
    }

    /** Two identical files from one user are one asset, as with uploads. */
    public function test_identical_creatives_share_one_asset(): void
    {
        Storage::disk('public')->put('creatives/images/a.png', 'same-bytes');
        Storage::disk('public')->put('creatives/images/b.png', 'same-bytes');

        $first = $this->creative();
        $first->update(['status' => 'completed', 'asset_path' => 'creatives/images/a.png']);

        $second = $this->creative();
        $second->update(['status' => 'completed', 'asset_path' => 'creatives/images/b.png']);

        $this->assertSame(1, Asset::count());
        $this->assertSame($first->fresh()->asset_id, $second->fresh()->asset_id);
    }

    /** One user's file must never be handed to another on a checksum match. */
    public function test_an_identical_file_from_another_user_is_a_separate_asset(): void
    {
        Storage::disk('public')->put('creatives/images/mine.png', 'same-bytes');
        Storage::disk('public')->put('creatives/images/theirs.png', 'same-bytes');

        $mine = $this->creative();
        $mine->update(['status' => 'completed', 'asset_path' => 'creatives/images/mine.png']);

        $stranger = User::factory()->create();
        $theirs = Creative::create([
            'user_id' => $stranger->id,
            'type' => 'image',
            'status' => 'generating',
            'source' => 'prompt',
        ]);
        $theirs->update(['status' => 'completed', 'asset_path' => 'creatives/images/theirs.png']);

        $this->assertSame(2, Asset::count());
        $this->assertNotSame($mine->fresh()->asset_id, $theirs->fresh()->asset_id);
    }
}
