<?php

namespace Tests\Feature\Creatives;

use App\Contracts\CompetitorAdProvider;
use App\Contracts\ScriptGenerator;
use App\Contracts\VoiceSynthesizer;
use App\DTOs\CompetitorAd;
use App\Exceptions\VideoDialogueException;
use App\Jobs\GenerateImageCreativeJob;
use App\Jobs\PrefetchCreativeBriefJob;
use App\Jobs\RenderVideoCreativeJob;
use App\Jobs\ScrapeMetaAdLibraryJob;
use App\Livewire\Creatives\ChatWizard;
use App\Models\Avatar;
use App\Models\Creative;
use App\Models\User;
use App\Models\Voice;
use App\Services\AI\ApifyMetaAdLibraryProvider;
use App\Services\AI\PuppeteerMetaAdLibraryProvider;
use Database\Seeders\AvatarSeeder;
use Database\Seeders\VoiceSeeder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Image;
use Laravel\Ai\StructuredAnonymousAgent;
use Livewire\Livewire;
use Tests\TestCase;

class ChatWizardTest extends TestCase
{
    use RefreshDatabase;

    /**
     * The brief prefetch goes to a worker, so it does not run inside these tests.
     *
     * It is dispatched afterResponse(), which in production hands it to the queue
     * and returns. In a test the same call runs it in-process as the request
     * finishes, so it consumed one of the faked AI responses and every assertion
     * after it read the wrong one - a test-only effect of code that is correct.
     *
     * Faking the queue is also the more honest arrangement: the step that needs the
     * brief cannot assume the worker has finished, and these walk the wizard faster
     * than any worker could.
     *
     * The test that checks the prefetch is dispatched fakes the queue itself and
     * asserts on the push, which is the right level for it.
     */
    protected function setUp(): void
    {
        parent::setUp();

        Queue::fake([PrefetchCreativeBriefJob::class]);
    }

    public function test_the_route_renders_the_wizard(): void
    {
        $this->actingAs(User::factory()->create())
            ->get(route('creatives.create'))
            ->assertOk()
            ->assertSeeLivewire(ChatWizard::class);
    }

    public function test_image_ratio_picker_shows_dimensions_below_the_aspect_ratio(): void
    {
        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->set('step', 'ratio')
            ->assertSeeHtml('<span class="mb-ratio-name">16:9</span>')
            ->assertSeeHtml('<span class="mb-ratio-dimensions">1600×900</span>')
            ->assertSeeHtml('<span class="mb-ratio-dimensions">1200×1200</span>')
            ->assertSeeHtml('<span class="mb-ratio-dimensions">900×1600</span>')
            ->assertSeeHtml('<span class="mb-ratio-dimensions">960×1200</span>')
            ->assertSeeHtml('<span class="mb-ratio-dimensions">1200×628</span>');
    }

    public function test_review_step_shows_product_handling_from_the_brief(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->set('step', 'summary')
            ->set('brief', ['handling' => 'display', 'product_name' => 'HP Smart Tank printer'])
            ->assertSee('Handling')
            ->assertSee('Display — product stays in place');
    }

    public function test_it_walks_through_type_source_and_input_steps(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->assertSet('step', 'type')
            ->call('selectType', 'image')
            ->assertSet('type', 'image')
            ->assertSet('step', 'source')
            ->call('selectSource', 'prompt')
            ->assertSet('source', 'prompt')
            ->assertSet('step', 'input')
            ->set('rawInput', 'A clean, premium ad for our ad-free browser.')
            ->call('submitInput')
            ->assertSet('step', 'ratio')
            ->assertHasNoErrors();
    }

    public function test_prompt_longer_than_600_characters_advances(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->assertDontSee('maxlength="600"', false)
            ->set('rawInput', str_repeat('A long prompt. ', 50))
            ->call('submitInput')
            ->assertHasNoErrors()
            ->assertSet('step', 'ratio');
    }

    public function test_product_url_submission_advances_immediately_and_queues_brief_prefetch(): void
    {
        Queue::fake();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'product_url')
            ->set('rawInput', 'https://example.com/product')
            ->call('submitInput')
            ->assertSet('step', 'ratio')
            ->assertHasNoErrors();

        Queue::assertPushed(
            PrefetchCreativeBriefJob::class,
            fn (PrefetchCreativeBriefJob $job) => $job->payload === 'https://example.com/product'
                && $job->source === 'product_url',
        );
    }

    public function test_it_walks_through_ratio_style_and_quantity_to_summary(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A clean, premium ad for our ad-free browser.')
            ->call('submitInput')
            ->call('selectRatio', '9:16')
            ->assertSet('ratio', '9:16')
            ->call('selectQuantity', 3)
            ->assertSet('quantity', 3)
            ->call('submitFormat')
            ->assertSet('step', 'style')
            ->set('style', 'minimalist')
            ->call('submitStyle')
            ->assertSet('step', 'summary');
    }

    public function test_skip_style_moves_to_quantity_without_a_style(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->call('skipStyle')
            ->assertSet('style', '')
            ->assertSet('step', 'summary');
    }

    public function test_suggest_style_asks_ai_and_fills_in_a_style(): void
    {
        $this->actingAs(User::factory()->create());

        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing', 'No annoying ads'],
                'features' => ['Built-in ad blocker'],
                'images' => [],
                'messaging' => 'Clean and premium',
                'cta' => 'Get Chromium Browser Free',
            ],
            ['style' => 'minimalist, high-contrast, bold typography'],
        ]);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->call('suggestStyle')
            ->assertSet('style', 'minimalist, high-contrast, bold typography')
            ->assertSet('step', 'summary');
    }

    public function test_custom_quantity_must_be_a_valid_integer(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->set('customQuantity', 'abc')
            ->call('submitCustomQuantity')
            ->assertHasErrors(['customQuantity'])
            ->assertSet('step', 'ratio');
    }

    private function fakeAiBrief(): void
    {
        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing', 'No annoying ads'],
                'features' => ['Built-in ad blocker'],
                'images' => [],
                'messaging' => 'Clean and premium',
                'cta' => 'Get Chromium Browser Free',
            ],
        ]);
    }

    public function test_confirm_generate_with_quantity_one_queues_image_and_polls_for_result(): void
    {
        Storage::fake('public');
        Queue::fake();
        $this->fakeAiBrief();
        Image::fake();

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

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done')
            ->assertSee('Waiting for image worker');

        $creative = Creative::sole();
        $this->assertSame($user->id, $creative->user_id);
        $this->assertSame('1:1', $creative->ratio);
        $this->assertSame('prompt', $creative->source);
        $this->assertSame('queued', $creative->status);
        $this->assertNull($creative->asset_path);
        $this->assertNull($creative->variation_group_id);
        Image::assertNothingGenerated();
        Queue::assertPushed(GenerateImageCreativeJob::class, fn (GenerateImageCreativeJob $job) => $job->creativeId === $creative->id);

        app()->call([new GenerateImageCreativeJob($creative->id), 'handle']);
        $component->call('refreshResults')
            ->assertSet('results.0.status', 'completed');

        $creative->refresh();
        $this->assertNotNull($creative->asset_path);
        Storage::disk('public')->assertExists($creative->asset_path);
    }

    public function test_confirm_generate_adds_a_preset_image_job_to_the_database_queue(): void
    {
        config(['queue.default' => 'database']);
        $this->fakeAiBrief();
        Image::fake();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '16:9')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $this->assertDatabaseCount('jobs', 1);
        $this->assertSame('queued', Creative::sole()->status);
        Image::assertNothingGenerated();
    }

    public function test_google_landscape_preset_can_be_selected_with_the_other_image_sizes(): void
    {
        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectRatio', '1.91:1')
            ->assertSet('ratio', '1.91:1')
            ->call('selectAllSizes')
            ->assertSet('quantity', 5)
            ->assertSet('selectedRatios', ['1:1', '9:16', '16:9', '4:5', '1.91:1'])
            ->set('step', 'ratio')
            ->assertSeeHtml('<button type="button" class="is-selected" wire:click="selectQuantity(5)">5</button>')
            ->call('selectQuantity', 5)
            ->assertSet('quantity', 5);
    }

    public function test_google_landscape_records_openai_when_gemini_is_the_default(): void
    {
        Storage::fake('public');
        config(['ai.default_for_images' => 'gemini']);
        $this->fakeAiBrief();
        $source = imagecreatetruecolor(64, 64);
        ob_start();
        imagepng($source);
        $sourceBytes = ob_get_clean();
        imagedestroy($source);
        Image::fake(fn () => base64_encode($sourceBytes));
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1.91:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('completed', $creative->status);
        $this->assertSame('openai', $creative->provider['provider']);
    }

    public function test_custom_image_generation_is_queued_for_openai(): void
    {
        Queue::fake();
        $this->fakeAiBrief();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->set('customRatioWidth', '500')
            ->set('customRatioHeight', '600')
            ->call('submitCustomRatio')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('500x600', $creative->ratio);
        $this->assertSame('queued', $creative->status);
        Queue::assertPushed(GenerateImageCreativeJob::class, fn (GenerateImageCreativeJob $job) => $job->creativeId === $creative->id);
    }

    public function test_custom_ratio_aspect_ratio_numbers_are_normalized_to_high_resolution(): void
    {
        Storage::fake('public');
        $this->fakeAiBrief();
        Image::fake();

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->set('customRatioWidth', '16')
            ->set('customRatioHeight', '9')
            ->call('submitCustomRatio')
            ->assertSet('customRatioWidth', 1920)
            ->assertSet('customRatioHeight', 1080)
            ->assertSet('ratio', '1920x1080')
            ->set('customRatioWidth', '16')
            ->set('customRatioHeight', '16')
            ->call('submitCustomRatio')
            ->assertSet('customRatioWidth', 1080)
            ->assertSet('customRatioHeight', 1080)
            ->assertSet('ratio', '1080x1080');
    }

    public function test_confirm_generate_with_multiple_quantity_queues_each_variation_in_one_group(): void
    {
        Queue::fake();
        $this->fakeAiBrief();
        Image::fake();

        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 3)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $this->assertDatabaseCount('creatives', 3);
        $this->assertCount(3, $component->get('results'));

        $creatives = Creative::all();
        $this->assertCount(1, $creatives->pluck('variation_group_id')->unique());
        $this->assertNotNull($creatives->first()->variation_group_id);
        $this->assertTrue($creatives->every(fn (Creative $creative) => $creative->status === 'queued'));
        Image::assertNothingGenerated();
        Queue::assertPushed(GenerateImageCreativeJob::class, 3);
        $this->assertEqualsCanonicalizing(
            $creatives->pluck('id')->all(),
            Queue::pushed(GenerateImageCreativeJob::class)->map(fn (GenerateImageCreativeJob $job) => $job->creativeId)->all(),
        );
    }

    public function test_queued_image_failure_is_shown_without_provider_details(): void
    {
        Queue::fake();
        $this->fakeAiBrief();
        Image::fake(fn () => throw new \RuntimeException('Provider unavailable'));

        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('queued', $creative->status);

        try {
            app()->call([new GenerateImageCreativeJob($creative->id), 'handle']);
            $this->fail('Expected the queued generation to fail.');
        } catch (\RuntimeException $exception) {
            $this->assertSame('Provider unavailable', $exception->getMessage());
        }

        $component->call('refreshResults')
            ->assertSet('results.0.status', 'failed')
            ->assertSee('Generation failed. Please try again.')
            ->assertDontSee('Provider unavailable');
    }

    public function test_video_is_selectable_and_walks_through_the_extra_video_steps(): void
    {
        Config::set('creative.video_engine', 'veo');
        Storage::fake('public');
        Queue::fake();
        $this->fakeAiBrief();
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);

        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing', 'No annoying ads'],
                'features' => ['Built-in ad blocker'],
                'images' => [],
                'messaging' => 'Clean and premium',
                'cta' => 'Get Chromium Browser Free',
            ],
            ['hook' => 'Hook A', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download free today.', 'problem' => 'Problem A', 'product' => 'Product A', 'benefits' => 'Benefits A', 'cta' => 'CTA A'],
            ['hook' => 'Hook B', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download free today.', 'problem' => 'Problem B', 'product' => 'Product B', 'benefits' => 'Benefits B', 'cta' => 'CTA B'],
            ['hook' => 'Hook C', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download free today.', 'problem' => 'Problem C', 'product' => 'Product C', 'benefits' => 'Benefits C', 'cta' => 'CTA C'],
            ['hook' => 'Hook D', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download free today.', 'problem' => 'Problem D', 'product' => 'Product D', 'benefits' => 'Benefits D', 'cta' => 'CTA D'],
        ]);

        $avatar = Avatar::first();
        $voice = Voice::first();

        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->assertSet('type', 'video')
            ->assertSet('step', 'source')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A video ad for our browser.')
            ->call('submitInput')
            ->call('selectRatio', '9:16')
            ->call('selectDuration', 8)
            ->assertSet('duration', 8)
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->assertSet('step', 'style')
            ->call('skipStyle')
            ->assertSet('step', 'script')
            ->call('generateScripts')
            ->assertCount('scriptVariations', 4)
            ->call('toggleScript', 0)
            ->call('confirmScripts')
            ->assertSet('step', 'avatar')
            ->call('selectAvatar', $avatar->id)
            // Avatar and voice share one screen, so picking an avatar records it
            // and stays put. Only the voice, which depends on the avatar's
            // gender, moves the wizard on.
            ->assertSet('step', 'avatar')
            ->assertSet('avatarId', $avatar->id)
            ->call('selectVoice', $voice->id)
            ->assertSet('step', 'storyboard')
            ->assertCount('storyboards', 1)
            ->assertCount('storyboards.0', 1)
            ->call('confirmStoryboard')
            ->assertSet('step', 'summary')
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('video', $creative->type);
        $this->assertStringStartsWith('9:16 - Chromium Browser - ', $creative->title);
        $this->assertSame(8, $creative->duration);
        $this->assertSame('veo', $creative->provider['engine']);
        $this->assertSame($avatar->id, $creative->avatar_id);
        $this->assertSame($voice->id, $creative->voice_id);
        // Rendering happens out-of-request via a queued job (see RenderVideoCreativeJob) so it stays 'generating' here.
        $this->assertSame('generating', $creative->status);
        $this->assertNull($creative->asset_path);

        Queue::assertPushed(RenderVideoCreativeJob::class, fn (RenderVideoCreativeJob $job) => $job->creativeId === $creative->id
            && $job->avatarId === $avatar->id
            && $job->voiceId === $voice->id
            && $job->duration === 8
            && $job->ratio === '9:16');
    }

    public function test_a_new_video_uses_the_configured_higgsfield_engine(): void
    {
        Config::set('creative.video_engine', 'higgsfield-seedance');
        Storage::fake('public');
        Queue::fake();
        $this->fakeAiBrief();
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);

        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing', 'No annoying ads'],
                'features' => ['Built-in ad blocker'],
                'images' => [],
                'messaging' => 'Clean and premium',
                'cta' => 'Get Chromium Browser Free',
            ],
            ['hook' => 'Hook A', 'spoken_line' => implode(' ', array_fill(0, 77, 'browsing')), 'problem' => 'Problem A', 'product' => 'Product A', 'benefits' => 'Benefits A', 'cta' => 'CTA A'],
            ['hook' => 'Hook B', 'spoken_line' => implode(' ', array_fill(0, 77, 'browsing')), 'problem' => 'Problem B', 'product' => 'Product B', 'benefits' => 'Benefits B', 'cta' => 'CTA B'],
            ['hook' => 'Hook C', 'spoken_line' => implode(' ', array_fill(0, 77, 'browsing')), 'problem' => 'Problem C', 'product' => 'Product C', 'benefits' => 'Benefits C', 'cta' => 'CTA C'],
            ['hook' => 'Hook D', 'spoken_line' => implode(' ', array_fill(0, 77, 'browsing')), 'problem' => 'Problem D', 'product' => 'Product D', 'benefits' => 'Benefits D', 'cta' => 'CTA D'],
        ]);

        $avatar = Avatar::first();
        $voice = Voice::first();

        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A video ad for our browser.')
            ->call('submitInput')
            ->call('selectRatio', '9:16')
            ->call('selectDuration', 30)
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->call('skipStyle')
            ->call('generateScripts')
            ->call('toggleScript', 0)
            ->call('confirmScripts')
            ->call('selectAvatar', $avatar->id)
            ->call('selectVoice', $voice->id)
            ->call('confirmStoryboard')
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('higgsfield-seedance', $creative->provider['engine']);
        $this->assertSame(30, $creative->duration);
        Queue::assertPushed(RenderVideoCreativeJob::class, fn (RenderVideoCreativeJob $job) => $job->creativeId === $creative->id
            && $job->duration === 30);
    }

    public function test_the_format_step_uses_the_configured_engine_and_hides_the_model_picker(): void
    {
        Config::set('creative.video_engine', 'higgsfield');
        Config::set('creative.higgsfield_model', 'wan-3.0');

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'ratio')
            ->assertSet('videoEngine', 'higgsfield-seedance')
            ->assertDontSee('selectVideoEngine', false)
            ->assertDontSee('Veo generates this video.');

        Config::set('creative.video_engine', 'vertex');

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'ratio')
            ->assertSet('videoEngine', 'veo')
            ->assertDontSee('selectVideoEngine', false);

        Config::set('creative.video_engine', 'seedance-2.5');

        Livewire::test(ChatWizard::class)
            ->assertSet('videoEngine', 'higgsfield-seedance');
    }

    public function test_retired_sora_duration_is_not_offered_or_accepted(): void
    {
        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'ratio')
            ->assertDontSee('12s')
            ->assertSee('8s')
            ->assertSee('15s')
            ->assertSee('30s');

        $this->expectException(\InvalidArgumentException::class);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectDuration', 12);
    }

    public function test_avatar_picker_explains_that_preview_and_rendered_voice_may_differ(): void
    {
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);

        Config::set('creative.video_engine', 'higgsfield-seedance');
        Config::set('creative.higgsfield_model', 'wan-3.0');

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'avatar')
            ->assertSee('Choose a presenter and voice style for the script. Wan generates the video from the script and does not use the presenter photo.')
            ->call('selectAvatar', Avatar::where('gender', 'female')->firstOrFail()->id)
            ->assertSee('Gemini-generated sample of the selected voice. Wan generates the final video speech separately, so its sound may differ.')
            ->call('selectVoice', Voice::where('gender', 'female')->firstOrFail()->id)
            ->set('step', 'summary')
            ->assertDontSeeHtml('goToStep(\'voice\')');
    }

    public function test_new_video_selections_only_offer_gemini_voices(): void
    {
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);
        $legacyVoice = Voice::create([
            'name' => 'Legacy Alloy',
            'gender' => 'female',
            'accent' => 'American',
            'tone' => 'warm',
            'provider' => 'openai',
            'provider_voice_id' => 'alloy',
        ]);
        $this->seed(VoiceSeeder::class);
        $this->assertModelExists($legacyVoice);
        $this->assertSame(10, Voice::where('provider', 'gemini')->count());
        $avatar = Avatar::where('gender', 'female')->firstOrFail();

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'avatar')
            ->call('selectAvatar', $avatar->id)
            ->assertSee('Achernar')
            ->assertSee('Winchester Friendly')
            ->assertSee('British')
            ->assertDontSee('Legacy Alloy');

        $this->expectException(\InvalidArgumentException::class);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'avatar')
            ->call('selectAvatar', $avatar->id)
            ->call('selectVoice', $legacyVoice->id);
    }

    public function test_mismatched_voice_cannot_be_selected_for_an_avatar(): void
    {
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);

        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Choose a voice that matches');

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectAvatar', Avatar::where('gender', 'female')->firstOrFail()->id)
            ->call('selectVoice', Voice::where('gender', 'male')->firstOrFail()->id);
    }

    public function test_video_generation_requires_a_valid_avatar_and_voice_before_spending(): void
    {
        Queue::fake();

        try {
            Livewire::test(ChatWizard::class)
                ->call('selectType', 'video')
                ->call('selectRatio', '9:16')
                ->call('selectQuantity', 1)
                ->set('selectedScriptIndices', [0])
                ->call('confirmGenerate');
            $this->fail('Expected a missing avatar and voice to be rejected.');
        } catch (\InvalidArgumentException $exception) {
            $this->assertStringContainsString('Choose a voice that matches', $exception->getMessage());
        }

        $this->assertDatabaseCount('creatives', 0);
        Queue::assertNotPushed(RenderVideoCreativeJob::class);
    }

    public function test_mismatched_voice_preview_never_calls_the_speech_provider(): void
    {
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);

        $synthesizer = $this->createMock(VoiceSynthesizer::class);
        $synthesizer->expects($this->never())->method('preview');
        app()->instance(VoiceSynthesizer::class, $synthesizer);

        $this->expectException(\InvalidArgumentException::class);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectAvatar', Avatar::where('gender', 'female')->firstOrFail()->id)
            ->call('previewVoice', Voice::where('gender', 'male')->firstOrFail()->id);
    }

    public function test_storyboard_shot_count_scales_with_video_duration(): void
    {
        $this->actingAs(User::factory()->create());

        $script = [
            'spoken_line' => 'Discover faster browsing. Download today.',
            'visual_action' => 'The presenter addresses the camera.',
            'product_action' => 'Show the verified product.',
        ];

        Config::set('creative.video_engine', 'veo');

        foreach ([8 => 1, 15 => 2, 30 => 4] as $duration => $expectedScenes) {
            Livewire::test(ChatWizard::class)
                ->call('selectDuration', $duration)
                ->set('scriptVariations', [$script])
                ->set('selectedScriptIndices', [0])
                ->call('buildStoryboards')
                ->assertCount('storyboards.0', $expectedScenes);
        }
    }

    public function test_clicking_another_script_selects_it_without_unselecting_first(): void
    {
        $this->actingAs(User::factory()->create());

        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing'],
                'features' => ['Built-in ad blocker'],
                'images' => [],
                'messaging' => 'Clean and premium',
                'cta' => 'Get Chromium Browser Free',
            ],
            ['hook' => 'Hook A', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download it free today now.', 'problem' => 'Problem A', 'product' => 'Product A', 'benefits' => 'Benefits A', 'cta' => 'CTA A'],
            ['hook' => 'Hook B', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download it free today now.', 'problem' => 'Problem B', 'product' => 'Product B', 'benefits' => 'Benefits B', 'cta' => 'CTA B'],
            ['hook' => 'Hook C', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download it free today now.', 'problem' => 'Problem C', 'product' => 'Product C', 'benefits' => 'Benefits C', 'cta' => 'CTA C'],
            ['hook' => 'Hook D', 'spoken_line' => 'Discover faster browsing with a cleaner page and no annoying ads, then download it free today now.', 'problem' => 'Problem D', 'product' => 'Product D', 'benefits' => 'Benefits D', 'cta' => 'CTA D'],
        ]);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A video ad.')
            ->call('submitInput')
            ->call('selectRatio', '9:16')
            ->call('selectDuration', 8)
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->call('skipStyle')
            ->call('generateScripts')
            ->call('toggleScript', 0)
            ->assertSet('selectedScriptIndices', [0])
            ->call('toggleScript', 1)
            ->assertSet('selectedScriptIndices', [1]);
    }

    public function test_uploaded_images_source_resolves_a_brief_from_the_images(): void
    {
        Storage::fake('public');
        $this->fakeAiBrief();
        Image::fake();

        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'upload')
            ->assertSet('step', 'input')
            ->set('uploadedImages', [UploadedFile::fake()->image('product.jpg')])
            ->call('submitInput')
            ->assertSet('step', 'ratio')
            ->call('selectRatio', '1:1')
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->call('skipStyle')
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('upload', $creative->source);
        $this->assertSame('completed', $creative->status);
    }

    public function test_uploaded_images_source_rejects_svg_files(): void
    {
        Storage::fake('public');

        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'upload')
            ->set('uploadedImages', [UploadedFile::fake()->create('malicious.svg', 10, 'image/svg+xml')])
            ->call('submitInput')
            ->assertHasErrors(['uploadedImages.0']);
    }

    public function test_uploaded_images_source_accepts_avif_files(): void
    {
        Storage::fake('public');

        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'upload')
            ->set('uploadedImages', [UploadedFile::fake()->create('product.avif', 50, 'image/avif')])
            ->call('submitInput')
            ->assertHasNoErrors(['uploadedImages', 'uploadedImages.0'])
            ->assertSet('step', 'ratio');
    }

    public function test_video_ratio_is_restricted_to_veo_supported_values(): void
    {
        $this->actingAs(User::factory()->create());

        $this->expectException(\InvalidArgumentException::class);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A video ad.')
            ->call('submitInput')
            ->call('selectRatio', '4:5');
    }

    public function test_video_quantity_is_capped_lower_than_image_quantity(): void
    {
        $this->actingAs(User::factory()->create());

        $this->expectException(\InvalidArgumentException::class);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A video ad.')
            ->call('submitInput')
            ->call('selectRatio', '9:16')
            ->call('selectDuration', 8)
            ->call('skipStyle')
            ->call('selectQuantity', 5);
    }

    public function test_existing_creative_can_be_cloned_into_a_variation(): void
    {
        Storage::fake('public');
        $this->fakeAiBrief();
        Image::fake();

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

        $parent = Creative::create([
            'user_id' => $user->id,
            'type' => 'image',
            'status' => 'completed',
            'source' => 'prompt',
            'ratio' => '1:1',
            'style' => 'minimalist',
            'prompt' => 'Original prompt',
            'asset_path' => 'creatives/images/original.png',
            'brief' => [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing'],
                'features' => [],
                'images' => [],
                'messaging' => 'Clean',
                'cta' => 'Get it',
            ],
        ]);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'existing')
            ->assertSet('step', 'clone_select')
            ->call('selectCloneFrom', $parent->id)
            ->assertSet('cloneFromId', $parent->id)
            ->assertSet('ratio', '1:1')
            ->assertSet('step', 'ratio')
            ->call('selectRatio', '4:5')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $this->assertDatabaseCount('creatives', 2);
        $clone = Creative::where('id', '!=', $parent->id)->sole();
        $this->assertSame($parent->id, $clone->parent_creative_id);
        $this->assertSame('clone', $clone->source);
        $this->assertSame('completed', $clone->status);
    }

    public function test_clone_from_another_users_creative_is_rejected(): void
    {
        $owner = User::factory()->create();
        $attacker = User::factory()->create();

        $parent = Creative::create([
            'user_id' => $owner->id,
            'type' => 'image',
            'status' => 'completed',
            'source' => 'prompt',
            'ratio' => '1:1',
            'style' => 'minimalist',
            'prompt' => 'Original prompt',
            'asset_path' => 'creatives/images/original.png',
            'brief' => [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing'],
                'features' => [],
                'images' => [],
                'messaging' => 'Clean',
                'cta' => 'Get it',
            ],
        ]);

        $this->actingAs($attacker);

        $this->expectException(ModelNotFoundException::class);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'existing')
            ->call('selectCloneFrom', $parent->id);
    }

    public function test_quantity_type_ratio_and_clone_id_cannot_be_set_directly_from_the_client(): void
    {
        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'A prompt.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1);

        // #[Locked] properties reject any direct client-side ->set(), which is how a forged
        // Livewire update payload would otherwise be able to rewrite quantity/ratio/type/cloneFromId
        // ahead of confirmGenerate() and dispatch far more billable renders than any preset allows.
        foreach (['quantity' => 500, 'ratio' => '16:9', 'type' => 'video', 'cloneFromId' => 999] as $property => $value) {
            try {
                $component->set($property, $value);
                $this->fail("Expected setting locked property [{$property}] from the client to be rejected.");
            } catch (\Throwable $e) {
                $this->assertStringContainsStringIgnoringCase('locked', $e->getMessage());
            }
        }
    }

    public function test_product_url_input_must_be_a_valid_url(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'product_url')
            ->set('rawInput', 'not-a-url')
            ->call('submitInput')
            ->assertHasErrors(['rawInput' => 'url'])
            ->assertSet('step', 'input');
    }

    public function test_start_over_resets_state(): void
    {
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Something')
            ->call('startOver')
            ->assertSet('step', 'type')
            ->assertSet('type', null)
            ->assertSet('source', null)
            ->assertSet('rawInput', '');
    }

    public function test_project_name_sets_custom_creative_title_format_and_shows_view_detail_button(): void
    {
        Storage::fake('public');
        $this->fakeAiBrief();
        Image::fake();

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

        Livewire::test(ChatWizard::class)
            ->assertSee('Project Name')
            ->assertSee('Mention brand or website')
            ->set('projectName', 'Nike')
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Fast shoes.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done')
            ->assertSee('View Detail')
            ->assertSee(route('creatives.show', Creative::sole()->id));

        $creative = Creative::sole();
        $this->assertSame('Nike', $creative->project_name);
        $this->assertMatchesRegularExpression('/^1200x1200 - Nike - .+$/', $creative->title);

        $this->get(route('creatives.show', $creative))
            ->assertOk()
            ->assertSee('Nike');
    }

    public function test_inferred_app_name_is_saved_in_the_three_part_image_title(): void
    {
        Queue::fake();
        StructuredAnonymousAgent::fake([[
            'product_name' => 'Acme Notes',
            'project_name' => 'Acme',
            'benefits' => ['Organize notes'],
            'features' => ['Search'],
            'images' => [],
            'messaging' => 'Take notes quickly',
            'cta' => 'Download now',
        ]]);
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Make an ad for the Acme Notes app.')
            ->call('submitInput')
            ->call('selectRatio', '16:9')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate');

        $this->assertSame('Acme', Creative::sole()->project_name);
        $this->assertStringStartsWith('1600x900 - Acme - ', Creative::sole()->title);
    }

    public function test_website_name_is_used_when_the_brief_has_no_project_name(): void
    {
        Queue::fake();
        StructuredAnonymousAgent::fake();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'product_url')
            ->set('rawInput', 'https://www.example.com/products/widget')
            ->call('submitInput')
            ->set('brief', [
                'product_name' => 'Blue Widget',
                'benefits' => [],
                'features' => [],
                'images' => [],
                'messaging' => 'Reliable',
                'cta' => 'Shop now',
            ])
            ->call('selectRatio', '1.91:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate');

        $this->assertSame('example.com', Creative::sole()->project_name);
        $this->assertStringStartsWith('1200x628 - example.com - ', Creative::sole()->title);
    }

    public function test_each_selected_image_size_uses_its_own_dimension_in_the_title(): void
    {
        Queue::fake();
        $this->fakeAiBrief();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('projectName', 'Browser App')
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Advertise the browser app.')
            ->call('submitInput')
            ->call('selectAllSizes')
            ->call('skipStyle')
            ->call('confirmGenerate');

        $this->assertSame([
            '1200x1200',
            '900x1600',
            '1600x900',
            '960x1200',
            '1200x628',
        ], Creative::orderBy('id')->get()->map(
            fn (Creative $creative) => explode(' - ', $creative->title)[0]
        )->all());
        $this->assertTrue(Creative::all()->every(
            fn (Creative $creative) => str_contains($creative->title, ' - Browser App - ')
        ));
    }

    public function test_custom_image_title_uses_exact_requested_pixels(): void
    {
        Queue::fake();
        $this->fakeAiBrief();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Advertise Chromium Browser.')
            ->call('submitInput')
            ->set('customRatioWidth', '1200')
            ->set('customRatioHeight', '628')
            ->call('submitCustomRatio')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate');

        $this->assertStringStartsWith('1200x628 - Chromium Browser - ', Creative::sole()->title);
    }

    public function test_long_project_name_keeps_the_title_and_database_value_within_limits(): void
    {
        Queue::fake();
        $this->fakeAiBrief();
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('projectName', str_repeat('Long Project ', 20))
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Advertise the product.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate');

        $this->assertLessThanOrEqual(120, mb_strlen(Creative::sole()->project_name));
        $this->assertLessThanOrEqual(80, mb_strlen(Creative::sole()->title));
        $this->assertMatchesRegularExpression('/^1200x1200 - .+ - .+$/', Creative::sole()->title);
    }

    public function test_creative_list_shows_the_full_generated_title(): void
    {
        $user = User::factory()->create();
        $title = '1200x1200 - An Extended Project Name - A Distinct Creative Title';
        Creative::create([
            'user_id' => $user->id,
            'type' => 'image',
            'status' => 'completed',
            'source' => 'prompt',
            'ratio' => '1:1',
            'title' => $title,
            'project_name' => 'An Extended Project Name',
        ]);

        $this->actingAs($user)
            ->get(route('creatives.index'))
            ->assertOk()
            ->assertSee($title);
    }

    public function test_a_one_letter_ai_title_keeps_dimension_and_project_name(): void
    {
        Queue::fake();
        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Chromium Browser',
                'benefits' => ['Faster browsing', 'No annoying ads'],
                'features' => ['Built-in ad blocker'],
                'images' => [],
                'messaging' => 'Clean and premium',
                'cta' => 'Get Chromium Browser Free',
            ],
            ['title' => 'i'],
        ]);
        $this->actingAs(User::factory()->create());

        Livewire::test(ChatWizard::class)
            ->set('projectName', 'Nike')
            ->call('selectType', 'image')
            ->call('selectSource', 'prompt')
            ->set('rawInput', 'Fast shoes.')
            ->call('submitInput')
            ->call('selectRatio', '1:1')
            ->call('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate');

        $this->assertSame('1200x1200 - Nike - i', Creative::sole()->title);
    }

    public function test_clone_selection_renders_playable_video_and_image_previews(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $videoCreative = Creative::create([
            'user_id' => $user->id,
            'type' => 'video',
            'status' => 'completed',
            'source' => 'prompt',
            'ratio' => '9:16',
            'duration' => 15,
            'title' => 'Sample Video Creative',
            'asset_path' => 'creatives/videos/test.mp4',
        ]);

        $imageCreative = Creative::create([
            'user_id' => $user->id,
            'type' => 'image',
            'status' => 'completed',
            'source' => 'prompt',
            'ratio' => '1:1',
            'title' => 'Sample Image Creative',
            'asset_path' => 'creatives/images/test.png',
        ]);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'existing')
            ->assertSet('step', 'clone_select')
            ->assertSeeHtml('<video src="'.asset('storage/creatives/videos/test.mp4').'"')
            ->assertSeeHtml('15s')
            ->assertSee('Sample Video Creative');

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'existing')
            ->assertSet('step', 'clone_select')
            ->assertSeeHtml('<img src="'.asset('storage/creatives/images/test.png').'"')
            ->assertSee('Sample Image Creative');
    }

    public function test_competitor_ad_search_renders_thumbnail_and_selecting_an_ad_advances(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        Storage::fake('public');
        Http::fake([
            'graph.facebook.com/*' => Http::response(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='), 200, ['Content-Type' => 'image/png']),
        ]);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public ?string $lastCountry = null;

            public ?string $lastMediaType = null;

            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->lastCountry = $country;
                $this->lastMediaType = $mediaType;

                return collect([
                    new CompetitorAd(
                        externalAdId: 'ext-ad-12345',
                        platform: 'meta',
                        advertiserName: 'Acme Shoes',
                        thumbnailUrl: 'https://graph.facebook.com/v26.0/123456/picture?type=large',
                        snapshot: [
                            'id' => 'ext-ad-12345',
                            'ad_snapshot_url' => 'https://facebook.com/ads/archive/render_ad/?id=ext-ad-12345',
                            'ad_creative_link_titles' => ['Super Lightweight Running Shoes'],
                            'ad_creative_link_captions' => ['www.acme.com'],
                            'ad_creative_bodies' => ['Run faster and lighter with Acme.'],
                            'ad_delivery_start_time' => '2026-09-09T00:00:00+0000',
                        ],
                    ),
                ]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->assertSet('step', 'competitor_search')
            ->assertSet('competitorCountry', 'US')
            ->assertSeeInOrder(['Brand or keyword', 'Country', 'Search'])
            ->assertSee('United States')
            ->assertDontSee('No ads found')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->assertSeeHtml('<img src="https://graph.facebook.com/v26.0/123456/picture?type=large"')
            ->assertSeeHtml('href="https://facebook.com/ads/archive/render_ad/?id=ext-ad-12345"')
            ->assertSee('Acme Shoes')
            ->assertDontSee('Recreate This')
            ->assertDontSee('No ads found')
            ->call('selectCompetitorAd', 'ext-ad-12345')
            ->assertSet('selectedCompetitorAdId', 'ext-ad-12345')
            ->assertSet('step', 'ratio');

        $this->assertSame('US', $mockProvider->lastCountry);
        $this->assertSame('VIDEO', $mockProvider->lastMediaType);

        $this->assertDatabaseHas('competitor_ad_references', [
            'user_id' => $user->id,
            'external_ad_id' => 'ext-ad-12345',
            'search_query' => 'Acme',
        ]);

        $storedThumbnails = $component->get('competitorImagePaths');
        $this->assertCount(1, $storedThumbnails);
        Storage::disk('public')->assertExists($storedThumbnails[0]);
    }

    public function test_selecting_a_competitor_ad_still_advances_when_the_thumbnail_cannot_be_downloaded(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        Storage::fake('public');
        Http::fake([
            'graph.facebook.com/*' => Http::response('not found', 404),
        ]);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect([
                    new CompetitorAd(
                        externalAdId: 'ext-ad-404',
                        platform: 'meta',
                        advertiserName: 'Acme Shoes',
                        thumbnailUrl: 'https://graph.facebook.com/v26.0/404/picture?type=large',
                        snapshot: [
                            'ad_creative_link_titles' => ['Super Lightweight Running Shoes'],
                            'ad_creative_bodies' => ['Run faster and lighter with Acme.'],
                        ],
                    ),
                ]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->call('selectCompetitorAd', 'ext-ad-404')
            ->assertSet('selectedCompetitorAdId', 'ext-ad-404')
            ->assertSet('step', 'ratio')
            ->assertSet('competitorImagePaths', []);
    }

    public function test_competitor_video_scripts_ground_the_brief_in_the_selected_ad_thumbnail(): void
    {
        Storage::fake('public');
        Http::fake([
            'graph.facebook.com/*' => Http::response(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='), 200, ['Content-Type' => 'image/png']),
        ]);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect([
                    new CompetitorAd(
                        externalAdId: 'ext-ad-12345',
                        platform: 'meta',
                        advertiserName: 'Acme Shoes',
                        thumbnailUrl: 'https://graph.facebook.com/v26.0/123456/picture?type=large',
                        snapshot: [
                            'ad_creative_link_titles' => ['Super Lightweight Running Shoes'],
                            'ad_creative_bodies' => ['Run faster and lighter with Acme.'],
                        ],
                    ),
                ]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        StructuredAnonymousAgent::fake([
            [
                'product_name' => 'Acme Running Shoes',
                'benefits' => ['Lighter on every stride'],
                'features' => ['Mesh upper', 'Chunky sole'],
                'visual_description' => 'A white mesh running shoe with a chunky black sole and red laces.',
                'images' => [],
                'messaging' => 'Inspired by Acme',
                'cta' => 'Shop now',
            ],
            ['hook' => 'Hook A', 'spoken_line' => 'Shop these lighter running shoes and feel every stride stay easy from the very first step today.', 'problem' => 'Problem A', 'product' => 'Product A', 'benefits' => 'Benefits A', 'cta' => 'CTA A'],
            ['hook' => 'Hook B', 'spoken_line' => 'Shop these lighter running shoes and feel every stride stay easy from the very first step today.', 'problem' => 'Problem B', 'product' => 'Product B', 'benefits' => 'Benefits B', 'cta' => 'CTA B'],
            ['hook' => 'Hook C', 'spoken_line' => 'Shop these lighter running shoes and feel every stride stay easy from the very first step today.', 'problem' => 'Problem C', 'product' => 'Product C', 'benefits' => 'Benefits C', 'cta' => 'CTA C'],
            ['hook' => 'Hook D', 'spoken_line' => 'Shop these lighter running shoes and feel every stride stay easy from the very first step today.', 'problem' => 'Problem D', 'product' => 'Product D', 'benefits' => 'Benefits D', 'cta' => 'CTA D'],
        ]);

        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->call('selectCompetitorAd', 'ext-ad-12345');

        $storedThumbnails = $component->get('competitorImagePaths');
        $this->assertCount(1, $storedThumbnails);
        Storage::disk('public')->assertExists($storedThumbnails[0]);

        $component
            ->call('selectRatio', '16:9')
            ->call('selectDuration', 8)
            ->call('selectQuantity', 1)
            ->call('submitFormat')
            ->call('skipStyle')
            ->call('generateScripts')
            ->assertSet('brief.visual_description', 'A white mesh running shoe with a chunky black sole and red laces.')
            ->assertCount('scriptVariations', 4)
            ->assertSet('competitorImagePaths', []);

        Storage::disk('public')->assertMissing($storedThumbnails[0]);
    }

    public function test_starting_over_deletes_the_selected_competitor_thumbnail(): void
    {
        Storage::fake('public');
        Http::fake([
            'graph.facebook.com/*' => Http::response(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='), 200, ['Content-Type' => 'image/png']),
        ]);

        $this->app->instance(CompetitorAdProvider::class, new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect([
                    new CompetitorAd(
                        externalAdId: 'ext-ad-12345',
                        platform: 'meta',
                        advertiserName: 'Acme Shoes',
                        thumbnailUrl: 'https://graph.facebook.com/v26.0/123456/picture?type=large',
                        snapshot: [
                            'ad_creative_link_titles' => ['Super Lightweight Running Shoes'],
                            'ad_creative_bodies' => ['Run faster and lighter with Acme.'],
                        ],
                    ),
                ]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        });

        $this->actingAs(User::factory()->create());

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->call('selectCompetitorAd', 'ext-ad-12345');

        $storedThumbnails = $component->get('competitorImagePaths');
        $this->assertCount(1, $storedThumbnails);
        Storage::disk('public')->assertExists($storedThumbnails[0]);

        $component->call('startOver');

        Storage::disk('public')->assertMissing($storedThumbnails[0]);
        $component->assertSet('competitorImagePaths', []);
    }

    public function test_untrusted_competitor_thumbnail_url_is_not_downloaded(): void
    {
        $this->actingAs(User::factory()->create());
        Storage::fake('public');
        Http::fake();

        $this->app->instance(CompetitorAdProvider::class, new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect([new CompetitorAd(
                    externalAdId: 'untrusted-ad',
                    platform: 'meta',
                    advertiserName: 'Acme Shoes',
                    thumbnailUrl: 'http://127.0.0.1/private',
                    snapshot: ['ad_creative_bodies' => ['Run with Acme.']],
                )]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        });

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->call('selectCompetitorAd', 'untrusted-ad')
            ->assertSet('step', 'ratio')
            ->assertSet('competitorImagePaths', []);

        Http::assertNothingSent();
    }

    public function test_competitor_thumbnail_with_disallowed_content_type_is_not_stored(): void
    {
        $this->actingAs(User::factory()->create());
        Storage::fake('public');
        Http::fake([
            'graph.facebook.com/*' => Http::response('<?php echo "unsafe";', 200, ['Content-Type' => 'application/x-php']),
        ]);

        $this->app->instance(CompetitorAdProvider::class, new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect([new CompetitorAd(
                    externalAdId: 'unsafe-ad',
                    platform: 'meta',
                    advertiserName: 'Acme Shoes',
                    thumbnailUrl: 'https://graph.facebook.com/v26.0/123/picture',
                    snapshot: ['ad_creative_bodies' => ['Run with Acme.']],
                )]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        });

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->call('selectCompetitorAd', 'unsafe-ad')
            ->assertSet('step', 'ratio')
            ->assertSet('competitorImagePaths', []);

        $this->assertSame([], Storage::disk('public')->allFiles('creatives/brief-images'));
    }

    public function test_oversized_competitor_thumbnail_is_not_stored(): void
    {
        $this->actingAs(User::factory()->create());
        Storage::fake('public');
        Http::fake([
            'graph.facebook.com/*' => Http::response(
                str_repeat('x', 10 * 1024 * 1024 + 1),
                200,
                ['Content-Type' => 'image/png'],
            ),
        ]);

        $this->app->instance(CompetitorAdProvider::class, new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect([new CompetitorAd(
                    externalAdId: 'oversized-ad',
                    platform: 'meta',
                    advertiserName: 'Acme Shoes',
                    thumbnailUrl: 'https://graph.facebook.com/v26.0/123/picture',
                    snapshot: ['ad_creative_bodies' => ['Run with Acme.']],
                )]);
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        });

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Acme')
            ->call('searchCompetitorAds')
            ->call('selectCompetitorAd', 'oversized-ad')
            ->assertSet('step', 'ratio')
            ->assertSet('competitorImagePaths', []);

        $this->assertSame([], Storage::disk('public')->allFiles('creatives/brief-images'));
    }

    public function test_script_generation_failure_shows_a_retry_action(): void
    {
        $this->actingAs(User::factory()->create());
        $this->mock(ScriptGenerator::class)
            ->shouldReceive('generateVariations')
            ->once()
            ->andThrow(new VideoDialogueException('Dialogue is too long.'));

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'script')
            ->set('brief', [
                'product_name' => 'Acme Shoes',
                'benefits' => ['Lightweight'],
                'features' => ['Mesh upper'],
                'images' => [],
                'messaging' => 'Run lighter',
                'cta' => 'Shop now',
            ])
            ->call('generateScripts')
            ->assertSet('scriptVariations', [])
            ->assertHasErrors(['scriptVariations'])
            ->assertSee('Try generating scripts again')
            ->assertDontSee('Regenerate scripts');
    }

    public function test_failed_script_regeneration_preserves_previous_selection(): void
    {
        $this->actingAs(User::factory()->create());
        $this->mock(ScriptGenerator::class)
            ->shouldReceive('generateVariations')
            ->once()
            ->andThrow(new VideoDialogueException('Dialogue is too long.'));

        $script = [
            'hook' => 'Run lighter',
            'spoken_line' => 'Run lighter. Shop now.',
            'visual_action' => 'Presenter faces camera.',
            'problem' => 'Heavy shoes',
            'product' => 'Acme Shoes',
            'benefits' => 'Lightweight',
            'cta' => 'Shop now',
        ];

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->set('step', 'script')
            ->set('brief', [
                'product_name' => 'Acme Shoes',
                'benefits' => ['Lightweight'],
                'features' => ['Mesh upper'],
                'images' => [],
                'messaging' => 'Run lighter',
                'cta' => 'Shop now',
            ])
            ->set('scriptVariations', [$script])
            ->set('selectedScriptIndices', [0])
            ->call('generateScripts')
            ->assertSet('scriptVariations', [$script])
            ->assertSet('selectedScriptIndices', [0])
            ->assertHasErrors(['scriptVariations']);
    }

    public function test_competitor_ad_search_sends_the_selected_country_to_the_provider(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public ?string $lastCountry = null;

            public ?string $lastMediaType = null;

            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->lastCountry = $country;
                $this->lastMediaType = $mediaType;

                return collect();
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->set('competitorCountry', 'IN')
            ->call('searchCompetitorAds')
            ->assertSee('No ads found');

        $this->assertSame('IN', $mockProvider->lastCountry);
        $this->assertSame('VIDEO', $mockProvider->lastMediaType);
    }

    public function test_competitor_ad_search_rejects_an_unknown_country_code(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public bool $searched = false;

            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->searched = true;

                return collect();
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->searched = true;

                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->set('competitorCountry', 'XX')
            ->call('searchCompetitorAds')
            ->assertHasErrors(['competitorCountry']);

        $this->assertFalse($mockProvider->searched);
    }

    public function test_competitor_ad_search_shows_an_error_when_the_provider_fails(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                throw new \RuntimeException('HTTP request returned status code 400');
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                throw new \RuntimeException('HTTP request returned status code 400');
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->call('searchCompetitorAds')
            ->assertHasErrors(['competitorQuery'])
            ->assertSee('Could not search Meta\'s Ad Library. Try again in a moment.')
            ->assertDontSee('No ads found')
            ->assertDontSee('HTTP request returned status code 400');
    }

    public function test_competitor_ad_search_sends_image_media_type_for_an_image_creative(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public ?string $lastMediaType = null;

            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->lastMediaType = $mediaType;

                return collect();
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->lastMediaType = $mediaType;

                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->call('searchCompetitorAds');

        $this->assertSame('IMAGE', $mockProvider->lastMediaType);
    }

    public function test_competitor_ad_search_uses_page_ids_when_the_query_is_a_facebook_page_id(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $mockProvider = new class implements CompetitorAdProvider
        {
            public bool $searchedByKeyword = false;

            /** @var list<string>|null */
            public ?array $lastPageIds = null;

            public ?string $lastMediaType = null;

            public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->searchedByKeyword = true;

                return collect();
            }

            public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
            {
                $this->lastPageIds = $pageIds;
                $this->lastMediaType = $mediaType;

                return collect();
            }
        };

        $this->app->instance(CompetitorAdProvider::class, $mockProvider);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'video')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'https://www.facebook.com/profile.php?id=15087023444')
            ->call('searchCompetitorAds')
            ->assertSee('No ads found');

        $this->assertFalse($mockProvider->searchedByKeyword);
        $this->assertSame(['15087023444'], $mockProvider->lastPageIds);
        $this->assertSame('VIDEO', $mockProvider->lastMediaType);
    }

    public function test_competitor_ad_search_polls_apify_until_ads_arrive(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        config([
            'services.apify.token' => 'test-apify-token',
            'creative.competitor_ad_provider' => ApifyMetaAdLibraryProvider::class,
        ]);
        Http::preventStrayRequests();
        Http::fake(function ($request) {
            $url = $request->url();

            if ($request->method() === 'POST' && str_contains($url, '/acts/') && str_contains($url, '/runs')) {
                return Http::response([
                    'data' => [
                        'id' => 'run-1',
                        'status' => 'RUNNING',
                        'defaultDatasetId' => 'ds-1',
                    ],
                ], 201);
            }

            if (str_contains($url, '/actor-runs/run-1')) {
                return Http::response([
                    'data' => [
                        'id' => 'run-1',
                        'status' => 'SUCCEEDED',
                        'defaultDatasetId' => 'ds-1',
                    ],
                ]);
            }

            if (str_contains($url, '/datasets/ds-1/items')) {
                return Http::response([
                    [
                        'id' => '1249043200627555',
                        'pageName' => 'Nike',
                        'title' => 'Just Do It',
                        'body' => 'Celebrate your birthday with Nike.',
                        'linkDomain' => 'nike.com',
                        'startDate' => '2026-03-17',
                        'totalActiveDays' => 10,
                        'images' => [['resizedUrl' => 'https://cdn.example/thumb.jpg']],
                        'videos' => [],
                        'platforms' => ['FACEBOOK'],
                        'adLibraryUrl' => 'https://www.facebook.com/ads/library/?id=1249043200627555',
                    ],
                ]);
            }

            return Http::response(['error' => 'unexpected '.$url], 500);
        });

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->call('searchCompetitorAds')
            ->assertSet('competitorSearchPending', true)
            ->assertSet('competitorApifyRunId', 'run-1')
            ->assertSee('this can take a minute.')
            ->assertDontSee('Celebrate your birthday with Nike.')
            ->call('pollCompetitorAds')
            ->assertSet('competitorSearchPending', false)
            ->assertSee('Nike')
            ->assertSee('Celebrate your birthday with Nike.')
            ->assertSeeHtml('<img src="https://cdn.example/thumb.jpg"');
    }

    public function test_competitor_ad_search_shows_an_error_when_the_apify_run_fails(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        config([
            'services.apify.token' => 'test-apify-token',
            'creative.competitor_ad_provider' => ApifyMetaAdLibraryProvider::class,
        ]);
        Http::preventStrayRequests();
        Http::fake(function ($request) {
            $url = $request->url();

            if ($request->method() === 'POST' && str_contains($url, '/acts/') && str_contains($url, '/runs')) {
                return Http::response([
                    'data' => [
                        'id' => 'run-1',
                        'status' => 'RUNNING',
                        'defaultDatasetId' => 'ds-1',
                    ],
                ], 201);
            }

            if (str_contains($url, '/actor-runs/run-1')) {
                return Http::response([
                    'data' => [
                        'id' => 'run-1',
                        'status' => 'FAILED',
                    ],
                ]);
            }

            return Http::response(['error' => 'unexpected '.$url], 500);
        });

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->call('searchCompetitorAds')
            ->assertSet('competitorSearchPending', true)
            ->call('pollCompetitorAds')
            ->assertSet('competitorSearchPending', false)
            ->assertHasErrors(['competitorQuery'])
            ->assertSee('Could not search Meta\'s Ad Library. Try again in a moment.')
            ->assertDontSee('No ads found');
    }

    public function test_competitor_ad_search_polls_puppeteer_until_ads_arrive(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        config([
            'creative.competitor_ad_provider' => PuppeteerMetaAdLibraryProvider::class,
            'creative.puppeteer_ad_library.enabled' => true,
            'creative.puppeteer_ad_library.script' => 'scripts/ad-library/scrape-ad-library.js',
        ]);

        Queue::fake([ScrapeMetaAdLibraryJob::class]);

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'adidas')
            ->set('competitorCountry', 'IN')
            ->call('searchCompetitorAds')
            ->assertSet('competitorSearchPending', true)
            ->assertSee('this can take a minute.');

        Queue::assertPushed(ScrapeMetaAdLibraryJob::class);

        $runId = $component->get('competitorApifyRunId');

        $this->assertNotEmpty($runId);

        File::put(storage_path('app/private/ad-library/'.$runId.'.json'), json_encode([
            'status' => 'SUCCEEDED',
            'ads' => [[
                'id' => '1249043200627555',
                'pageName' => 'adidas',
                'title' => 'Shop adidas',
                'body' => 'Impossible is nothing.',
                'linkDomain' => 'adidas.com',
                'images' => [['resizedUrl' => 'https://cdn.example/thumb.jpg']],
                'adLibraryUrl' => 'https://www.facebook.com/ads/library/?id=1249043200627555',
            ]],
        ], JSON_THROW_ON_ERROR));

        $component
            ->call('pollCompetitorAds')
            ->assertSet('competitorSearchPending', false)
            ->assertSee('adidas')
            ->assertSee('Impossible is nothing.')
            ->assertSeeHtml('<img src="https://cdn.example/thumb.jpg"');
    }

    public function test_puppeteer_competitor_search_uses_the_keyword_even_when_the_query_looks_like_a_page_url(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        config([
            'creative.competitor_ad_provider' => PuppeteerMetaAdLibraryProvider::class,
            'creative.puppeteer_ad_library.enabled' => true,
            'creative.puppeteer_ad_library.script' => 'scripts/ad-library/scrape-ad-library.js',
        ]);

        Queue::fake([ScrapeMetaAdLibraryJob::class]);

        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'https://www.facebook.com/profile.php?id=15087023444')
            ->call('searchCompetitorAds')
            ->assertSet('competitorSearchPending', true);

        Queue::assertPushed(ScrapeMetaAdLibraryJob::class, function (ScrapeMetaAdLibraryJob $job): bool {
            return str_contains($job->url, 'q=')
                && str_contains($job->url, 'search_type=keyword_unordered')
                && ! str_contains($job->url, 'view_all_page_id');
        });
    }

    public function test_competitor_ad_search_falls_back_to_apify_when_the_puppeteer_scrape_fails(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        config([
            'creative.competitor_ad_provider' => PuppeteerMetaAdLibraryProvider::class,
            'creative.puppeteer_ad_library.enabled' => true,
            'creative.puppeteer_ad_library.script' => 'scripts/ad-library/scrape-ad-library.js',
            'services.apify.token' => 'test-apify-token',
        ]);

        Queue::fake([ScrapeMetaAdLibraryJob::class]);
        Http::preventStrayRequests();
        Http::fake(function ($request) {
            $url = $request->url();

            if ($request->method() === 'POST' && str_contains($url, '/acts/') && str_contains($url, '/runs')) {
                return Http::response([
                    'data' => [
                        'id' => 'run-1',
                        'status' => 'RUNNING',
                        'defaultDatasetId' => 'ds-1',
                    ],
                ], 201);
            }

            if (str_contains($url, '/actor-runs/run-1')) {
                return Http::response([
                    'data' => [
                        'id' => 'run-1',
                        'status' => 'SUCCEEDED',
                        'defaultDatasetId' => 'ds-1',
                    ],
                ]);
            }

            if (str_contains($url, '/datasets/ds-1/items')) {
                return Http::response([
                    [
                        'id' => '1249043200627555',
                        'pageName' => 'Nike',
                        'title' => 'Just Do It',
                        'body' => 'Celebrate your birthday with Nike.',
                        'linkDomain' => 'nike.com',
                        'startDate' => '2026-03-17',
                        'totalActiveDays' => 10,
                        'images' => [['resizedUrl' => 'https://cdn.example/thumb.jpg']],
                        'videos' => [],
                        'platforms' => ['FACEBOOK'],
                        'adLibraryUrl' => 'https://www.facebook.com/ads/library/?id=1249043200627555',
                    ],
                ]);
            }

            return Http::response(['error' => 'unexpected '.$url], 500);
        });

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->call('searchCompetitorAds')
            ->assertSet('competitorSearchPending', true)
            ->assertSet('competitorSearchBackend', 'puppeteer');

        Queue::assertPushed(ScrapeMetaAdLibraryJob::class);

        $runId = $component->get('competitorApifyRunId');

        File::put(storage_path('app/private/ad-library/'.$runId.'.json'), json_encode([
            'status' => 'FAILED',
            'ads' => [],
            'error' => 'Facebook Ad Library blocked automated access.',
        ], JSON_THROW_ON_ERROR));

        $component
            ->call('pollCompetitorAds')
            ->assertSet('competitorSearchPending', true)
            ->assertSet('competitorSearchBackend', 'apify')
            ->assertSet('competitorApifyRunId', 'run-1')
            ->assertSee('this can take a minute.')
            ->call('pollCompetitorAds')
            ->assertSet('competitorSearchPending', false)
            ->assertSee('Nike')
            ->assertSee('Celebrate your birthday with Nike.');

        Http::assertSent(fn ($request): bool => $request->method() === 'POST'
            && str_contains($request->url(), 'acts/sukhdipp~meta-ad-library-scraper/runs'));
    }

    public function test_competitor_ad_search_shows_an_error_when_puppeteer_fails_and_apify_is_not_configured(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        config([
            'creative.competitor_ad_provider' => PuppeteerMetaAdLibraryProvider::class,
            'creative.puppeteer_ad_library.enabled' => true,
            'creative.puppeteer_ad_library.script' => 'scripts/ad-library/scrape-ad-library.js',
            'services.apify.token' => null,
        ]);

        Queue::fake([ScrapeMetaAdLibraryJob::class]);

        $component = Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->call('selectSource', 'competitor')
            ->set('competitorQuery', 'Nike')
            ->call('searchCompetitorAds')
            ->assertSet('competitorSearchPending', true);

        File::put(storage_path('app/private/ad-library/'.$component->get('competitorApifyRunId').'.json'), json_encode([
            'status' => 'FAILED',
            'ads' => [],
            'error' => 'Puppeteer Meta Ad Library search failed.',
        ], JSON_THROW_ON_ERROR));

        $component
            ->call('pollCompetitorAds')
            ->assertSet('competitorSearchPending', false)
            ->assertHasErrors(['competitorQuery'])
            ->assertSee('Could not search Meta\'s Ad Library. Try again in a moment.');
    }

    public function test_small_custom_pixel_dimensions_remain_exact(): void
    {
        Livewire::test(ChatWizard::class)
            ->set('customRatioWidth', '150')
            ->set('customRatioHeight', '150')
            ->call('submitCustomRatio')
            ->assertSet('ratio', '150x150')
            ->assertSet('customRatioWidth', 150)
            ->assertSet('customRatioHeight', 150);
    }

    public function test_custom_image_exceeding_pixel_limit_is_rejected(): void
    {
        Livewire::test(ChatWizard::class)
            ->call('selectType', 'image')
            ->set('step', 'ratio')
            ->set('customRatioWidth', '8000')
            ->set('customRatioHeight', '8000')
            ->call('submitCustomRatio')
            ->assertHasErrors(['customRatioWidth'])
            ->assertSee('Custom image size cannot exceed 8,294,400 pixels.');
    }

    /** Small aspect-ratio inputs must stay within the output limit after scaling. */
    public function test_an_extreme_ratio_stays_inside_the_allowed_dimensions(): void
    {
        foreach ([[1, 100], [100, 1], [3, 100], [100, 3]] as [$w, $h]) {
            $component = Livewire::test(ChatWizard::class)
                ->set('customRatioWidth', (string) $w)
                ->set('customRatioHeight', (string) $h)
                ->call('submitCustomRatio');

            $width = (int) $component->get('customRatioWidth');
            $height = (int) $component->get('customRatioHeight');

            $this->assertLessThanOrEqual(8000, max($width, $height), "{$w}x{$h} produced {$width}x{$height}.");
            $this->assertGreaterThanOrEqual(1, min($width, $height), "{$w}x{$h} produced {$width}x{$height}.");
        }
    }

    /** And the shape asked for survives being brought back into range. */
    public function test_the_aspect_is_kept_when_the_result_is_scaled_down(): void
    {
        $component = Livewire::test(ChatWizard::class)
            ->set('customRatioWidth', '100')
            ->set('customRatioHeight', '1')
            ->call('submitCustomRatio');

        $width = (int) $component->get('customRatioWidth');
        $height = (int) $component->get('customRatioHeight');

        $this->assertSame(8000, $width, 'The long side should sit on the ceiling.');
        $this->assertEqualsWithDelta(100.0, $width / $height, 1.0, 'The shape changed while being clamped.');
    }

    /** Ordinary ratios are untouched by the clamp. */
    public function test_a_normal_ratio_is_not_scaled_down(): void
    {
        Livewire::test(ChatWizard::class)
            ->set('customRatioWidth', '16')
            ->set('customRatioHeight', '9')
            ->call('submitCustomRatio')
            ->assertSet('customRatioWidth', 1920)
            ->assertSet('customRatioHeight', 1080);
    }
}
