<?php

namespace Tests\Feature;

use App\Contracts\VideoAvatarProvider;
use App\Exceptions\VideoPromptFilteredException;
use App\Jobs\RenderVideoCreativeJob;
use App\Models\Avatar;
use App\Models\Creative;
use App\Models\User;
use App\Models\Voice;
use App\Services\AI\VertexSafetyFilteredException;
use App\Services\AI\VideoAvatarProviderFactory;
use App\Support\VideoDialogue;
use Database\Seeders\AvatarSeeder;
use Database\Seeders\VoiceSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
use Tests\TestCase;

class RenderVideoCreativeJobTest extends TestCase
{
    use RefreshDatabase;

    private function makeCreative(int $duration = 8, string $ratio = '9:16'): Creative
    {
        $this->seed(AvatarSeeder::class);
        $this->seed(VoiceSeeder::class);

        return Creative::create([
            'user_id' => User::factory()->create()->id,
            'type' => 'video',
            'status' => 'generating',
            'source' => 'prompt',
            'prompt' => 'A video ad for our browser.',
            'ratio' => $ratio,
            'duration' => $duration,
            'avatar_id' => Avatar::first()->id,
            'voice_id' => Voice::first()->id,
            'script' => ['hook' => 'H', 'problem' => 'P', 'product' => 'Pr', 'benefits' => 'B', 'cta' => 'C'],
            'storyboard' => [['scene' => 'H'], ['scene' => 'P'], ['scene' => 'Pr'], ['scene' => 'B'], ['scene' => 'C']],
        ]);
    }

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

        config([
            'ai.providers.gemini.key' => 'test-key',
            'ai.providers.gemini.url' => 'https://generativelanguage.googleapis.com/v1beta/',
            'creative.gemini_veo.poll_interval_seconds' => 0,
            'creative.gemini_veo.max_poll_attempts' => 3,
        ]);

        Http::fake([
            'generativelanguage.googleapis.com/v1beta/models/*:predictLongRunning' => Http::response([
                'name' => 'models/veo-3.0-generate-001/operations/abc123',
            ]),
            'generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-001/operations/abc123' => Http::response([
                'done' => true,
                'response' => [
                    'generateVideoResponse' => [
                        'generatedSamples' => [
                            ['video' => ['uri' => 'https://generativelanguage.googleapis.com/v1beta/files/abc123:download']],
                        ],
                    ],
                ],
            ]),
            'generativelanguage.googleapis.com/v1beta/files/abc123:download' => Http::response('FAKEVIDEOBYTES', 200),
        ]);

        $creative = $this->makeCreative(duration: 6);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('completed', $creative->status);
        $this->assertSame('veo', $creative->provider['engine']);
        $this->assertSame(6, $creative->provider['requested_duration']);
        $this->assertNotNull($creative->asset_path);
        $this->assertNull($creative->failure_reason);
        Storage::disk('public')->assertExists($creative->asset_path);
    }

    public function test_it_renders_an_eight_second_video_via_vertex_with_the_presenter_photo(): void
    {
        Storage::fake('public');
        $credentialsPath = tempnam(sys_get_temp_dir(), 'vertex-job-');
        $key = openssl_pkey_new(['private_key_bits' => 2048]);
        openssl_pkey_export($key, $privateKey);
        file_put_contents($credentialsPath, json_encode([
            'client_email' => 'test@example.iam.gserviceaccount.com',
            'private_key' => $privateKey,
        ]));

        config([
            'creative.disk' => 'public',
            'services.vertex_ai.project_id' => 'test-project',
            'services.vertex_ai.credentials_path' => $credentialsPath,
            'services.vertex_ai.location' => 'us-central1',
            'creative.gemini_veo_vertex.model' => 'veo-test',
            'creative.gemini_veo_vertex.poll_interval_seconds' => 0,
            'creative.gemini_veo_vertex.max_poll_attempts' => 2,
            'creative.vertex_extend_durations' => [8 => 0, 15 => 1, 30 => 3],
        ]);

        Http::fake(function ($request) {
            if ($request->url() === 'https://oauth2.googleapis.com/token') {
                return Http::response(['access_token' => 'test-token']);
            }

            if (str_ends_with($request->url(), ':predictLongRunning')) {
                return Http::response(['name' => 'operations/base']);
            }

            if (str_ends_with($request->url(), ':fetchPredictOperation')) {
                return Http::response([
                    'done' => true,
                    'response' => ['videos' => [[
                        'bytesBase64Encoded' => base64_encode('video-base'),
                        'mimeType' => 'video/mp4',
                    ]]],
                ]);
            }

            return Http::response([], 404);
        });

        $creative = $this->makeCreative(duration: 8);

        try {
            (new RenderVideoCreativeJob(
                $creative->id,
                $creative->avatar_id,
                $creative->voice_id,
                $creative->script,
                $creative->storyboard,
                $creative->duration,
                $creative->ratio,
            ))->handle(app(VideoAvatarProviderFactory::class));
        } finally {
            @unlink($credentialsPath);
        }

        $creative->refresh();
        $predictRequests = collect(Http::recorded())
            ->map(fn (array $record) => $record[0])
            ->filter(fn ($request) => str_ends_with($request->url(), ':predictLongRunning'))
            ->values();

        $this->assertCount(1, $predictRequests);
        $this->assertSame(8, $predictRequests[0]['parameters']['durationSeconds']);
        $this->assertArrayHasKey('bytesBase64Encoded', $predictRequests[0]['instances'][0]['image']);
        $this->assertSame('completed', $creative->status);
        $this->assertSame('veo', $creative->provider['engine']);
        $this->assertSame('gemini-veo-vertex', $creative->provider['provider']);
        $this->assertSame(0, $creative->provider['extend_calls']);
        $this->assertSame(8, $creative->duration);
        $this->assertSame(8, $creative->provider['requested_duration']);
        Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'generativelanguage.googleapis.com'));
        Storage::disk('public')->assertExists($creative->asset_path);
    }

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

        foreach ([
            ['provider' => 'gemini-veo-vertex', 'duration' => 29, 'extend_calls' => 3, 'requested_duration' => 30],
            ['provider' => 'gemini-veo-vertex', 'duration' => 29, 'extend_calls' => 3],
        ] as $metadata) {
            $creative = $this->makeCreative(duration: 29);
            $creative->update(['status' => 'completed', 'provider' => $metadata]);

            $this->actingAs($creative->user)
                ->post(route('creatives.regenerate', $creative))
                ->assertRedirect(route('creatives.show', $creative));

            Queue::assertPushed(RenderVideoCreativeJob::class, fn (RenderVideoCreativeJob $job): bool => $job->creativeId === $creative->id && $job->duration === 30);
        }
    }

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

        $creative = $this->makeCreative(duration: 12);
        $creative->update([
            'status' => 'completed',
            'asset_path' => 'creatives/videos/original.mp4',
            'provider' => ['provider' => 'openai-sora', 'requested_duration' => 12],
        ]);

        $this->actingAs($creative->user)
            ->post(route('creatives.regenerate', $creative))
            ->assertRedirect(route('creatives.show', $creative))
            ->assertSessionHas('video_error');

        $creative->refresh();
        $this->assertSame('completed', $creative->status);
        $this->assertSame('creatives/videos/original.mp4', $creative->asset_path);
        Queue::assertNotPushed(RenderVideoCreativeJob::class);
    }

    public function test_old_queued_sora_job_fails_without_calling_the_retired_api(): void
    {
        Http::fake();
        $creative = $this->makeCreative(duration: 12);

        try {
            (new RenderVideoCreativeJob(
                $creative->id,
                $creative->avatar_id,
                $creative->voice_id,
                $creative->script,
                $creative->storyboard,
                12,
                $creative->ratio,
            ))->handle(app(VideoAvatarProviderFactory::class));
            $this->fail('Expected the retired duration to be rejected.');
        } catch (\InvalidArgumentException $exception) {
            $this->assertStringContainsString('12s is no longer supported', $exception->getMessage());
        }

        $this->assertSame('failed', $creative->refresh()->status);
        Http::assertNothingSent();
    }

    public function test_it_marks_the_creative_failed_when_the_provider_throws(): void
    {
        config(['ai.providers.gemini.key' => null]);

        $creative = $this->makeCreative(duration: 6);

        try {
            (new RenderVideoCreativeJob(
                $creative->id,
                $creative->avatar_id,
                $creative->voice_id,
                $creative->script,
                $creative->storyboard,
                $creative->duration,
                $creative->ratio,
            ))->handle(app(VideoAvatarProviderFactory::class));
            $this->fail('Expected exception was not thrown.');
        } catch (\Throwable $e) {
            // provider throws ProviderNotConfiguredException when the key is missing
        }

        $creative->refresh();

        $this->assertSame('failed', $creative->status);
        $this->assertNotNull($creative->failure_reason);
    }

    public function test_higgsfield_trims_overlong_dialogue_and_still_submits(): void
    {
        Storage::fake('public');
        config([
            'creative.disk' => 'public',
            'creative.spoken_words_per_second' => 1.7,
            'creative.higgsfield_spoken_words_per_second' => 1.7,
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'wan-3.0',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'creative.higgsfield_seedance.max_poll_attempts' => 2,
        ]);
        Http::fake([
            'api.higgsfield.ai/alibaba/wan-3.0/text-to-video' => Http::response([
                'status' => 'queued',
                'request_id' => 'req_trim',
                'status_url' => 'https://api.higgsfield.ai/requests/req_trim/status',
            ]),
            'api.higgsfield.ai/requests/req_trim/status' => Http::response([
                'status' => 'completed',
                'request_id' => 'req_trim',
                'video' => ['url' => 'https://cdn.example.com/trimmed.mp4'],
            ]),
            'cdn.example.com/trimmed.mp4' => Http::response('TRIMMED', 200),
        ]);

        $creative = $this->makeCreative(duration: 8);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);
        $script = $creative->script;
        $script['spoken_line'] = implode(' ', array_fill(0, 14, 'word'));

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();
        $this->assertSame('completed', $creative->status);
        $this->assertNull($creative->failure_reason);

        Http::assertSent(function ($request): bool {
            if (! str_ends_with($request->url(), '/alibaba/wan-3.0/text-to-video')) {
                return false;
            }

            preg_match('/SPOKEN LINE \(exactly \d+ words, first word "[^"]*", last word "[^"]*"\):\s*"([^"]*)"/', $request['prompt'], $matches);

            return VideoDialogue::wordCount($matches[1] ?? '') <= VideoDialogue::wordBudget(8, 'higgsfield-seedance');
        });
    }

    public function test_vertex_safety_filter_shows_a_useful_failure_instead_of_a_generic_error(): void
    {
        $creative = $this->makeCreative(duration: 15);
        $exception = new VertexSafetyFilteredException('Vertex AI usage guidelines (support code 15236754)');
        $provider = $this->createMock(VideoAvatarProvider::class);
        $provider->method('render')->willThrowException($exception);
        $factory = $this->createMock(VideoAvatarProviderFactory::class);
        $factory->method('forDuration')->willReturn($provider);
        $job = new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        );

        try {
            $job->handle($factory);
            $this->fail('Expected Vertex to reject the render.');
        } catch (VertexSafetyFilteredException $caught) {
            $this->assertSame($exception, $caught);
        }

        $this->assertSame('failed', $creative->refresh()->status);
        $this->assertSame('The video prompt was rejected by the provider safety filter. Please revise the description and try again.', $creative->failure_reason);

        $creative->update(['status' => 'processing', 'failure_reason' => null]);
        $job->failed($exception);
        $this->assertSame('The video prompt was rejected by the provider safety filter. Please revise the description and try again.', $creative->refresh()->failure_reason);
    }

    public function test_it_renders_seedance_without_calling_veo_when_that_engine_is_selected(): void
    {
        Storage::fake('public');
        config([
            'creative.disk' => 'public',
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'wan-3.0',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'creative.higgsfield_seedance.max_poll_attempts' => 2,
            'ai.providers.gemini.key' => 'test-key',
        ]);

        Http::fake([
            'api.higgsfield.ai/alibaba/wan-3.0/text-to-video' => Http::response([
                'status' => 'queued',
                'request_id' => 'req_job',
                'status_url' => 'https://api.higgsfield.ai/requests/req_job/status',
            ]),
            'api.higgsfield.ai/requests/req_job/status' => Http::response([
                'status' => 'completed',
                'request_id' => 'req_job',
                'video' => ['url' => 'https://cdn.example.com/seedance-job.mp4'],
            ]),
            'cdn.example.com/seedance-job.mp4' => Http::response('SEEDANCEJOB', 200),
        ]);

        $creative = $this->makeCreative(duration: 30);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('completed', $creative->status);
        $this->assertSame('higgsfield-seedance', $creative->provider['engine']);
        $this->assertSame('higgsfield-seedance', $creative->provider['provider']);
        $this->assertSame('alibaba/wan-3.0', $creative->provider['model']);
        $this->assertSame(30, $creative->duration);
        $this->assertSame(30, $creative->provider['requested_duration']);
        Storage::disk('public')->assertExists($creative->asset_path);
        Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'generativelanguage.googleapis.com')
            || str_contains($request->url(), 'aiplatform.googleapis.com'));
    }

    public function test_it_is_a_no_op_when_the_creative_is_already_completed(): void
    {
        $creative = $this->makeCreative(duration: 8);
        $creative->update(['status' => 'completed', 'asset_path' => 'creatives/videos/already-done.mp4']);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('completed', $creative->status);
        $this->assertSame('creatives/videos/already-done.mp4', $creative->asset_path);
    }

    public function test_higgsfield_stays_processing_and_keeps_the_request_when_the_poll_window_ends(): void
    {
        Queue::fake();
        config([
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'seedance-2.5',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'creative.higgsfield_seedance.max_poll_attempts' => 2,
        ]);
        Http::fake([
            'api.higgsfield.ai/bytedance/seedance-2.5/text-to-video' => Http::response([
                'status' => 'queued',
                'request_id' => 'req_pending',
                'status_url' => 'https://platform.higgsfield.ai/requests/req_pending/status',
            ]),
            'platform.higgsfield.ai/requests/req_pending/status' => Http::response([
                'status' => 'queued',
                'request_id' => 'req_pending',
            ]),
        ]);

        $creative = $this->makeCreative(duration: 15);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('processing', $creative->status);
        $this->assertNull($creative->failure_reason);
        $this->assertSame('Waiting on Higgsfield', $creative->render_stage);
        $this->assertSame('req_pending', $creative->provider['request_id']);
        $this->assertSame('https://platform.higgsfield.ai/requests/req_pending/status', $creative->provider['status_url']);
        $this->assertSame('bytedance/seedance-2.5', $creative->provider['model']);
        Queue::assertPushed(RenderVideoCreativeJob::class, function (RenderVideoCreativeJob $job) use ($creative): bool {
            return $job->creativeId === $creative->id;
        });
        Http::assertSentCount(3);
    }

    public function test_higgsfield_resume_polls_the_stored_status_url_without_submitting_again(): void
    {
        Storage::fake('public');
        config([
            'creative.disk' => 'public',
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'seedance-2.5',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'creative.higgsfield_seedance.max_poll_attempts' => 2,
        ]);
        Http::fake([
            'platform.higgsfield.ai/requests/req_resume/status' => Http::response([
                'status' => 'completed',
                'request_id' => 'req_resume',
                'video' => ['url' => 'https://cdn.example.com/resumed.mp4'],
            ]),
            'cdn.example.com/resumed.mp4' => Http::response('RESUMED', 200),
        ]);

        $creative = $this->makeCreative(duration: 15);
        $creative->update([
            'status' => 'processing',
            'provider' => [
                'engine' => 'higgsfield-seedance',
                'request_id' => 'req_resume',
                'status_url' => 'https://platform.higgsfield.ai/requests/req_resume/status',
                'model' => 'bytedance/seedance-2.5',
                'spoken_line' => 'Shop the white tee.',
            ],
        ]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('completed', $creative->status);
        $this->assertSame('req_resume', $creative->provider['request_id']);
        $this->assertSame('bytedance/seedance-2.5', $creative->provider['model']);
        Storage::disk('public')->assertExists($creative->asset_path);
        Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'text-to-video'));
    }

    public function test_higgsfield_vendor_failure_falls_back_to_veo_and_logs_the_response(): void
    {
        Queue::fake();
        config([
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'seedance-2.5',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'creative.higgsfield_seedance.max_poll_attempts' => 2,
            'services.slack.SLACK_WEBHOOK_URL' => 'https://hooks.slack.com/services/test',
        ]);
        Http::fake([
            'api.higgsfield.ai/bytedance/seedance-2.5/text-to-video' => Http::response([
                'status' => 'queued',
                'request_id' => 'req_failed',
                'status_url' => 'https://platform.higgsfield.ai/requests/req_failed/status',
            ]),
            'platform.higgsfield.ai/requests/req_failed/status' => Http::response([
                'status' => 'failed',
                'request_id' => 'req_failed',
                'detail' => 'credit balance is too low',
            ]),
            'hooks.slack.com/*' => Http::response('ok', 200),
        ]);

        $logged = null;
        Log::listen(function (MessageLogged $event) use (&$logged): void {
            if ($event->message === 'Higgsfield render failed; falling back to Veo') {
                $logged = $event->context;
            }
        });

        $creative = $this->makeCreative(duration: 15);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('processing', $creative->status);
        $this->assertNull($creative->failure_reason);
        $this->assertSame('Switching to Veo', $creative->render_stage);
        $this->assertSame('veo', $creative->provider['engine']);
        $this->assertSame('higgsfield-seedance', $creative->provider['fell_back_from']);
        $this->assertSame('credits', $creative->provider['higgsfield_failure_reason']);
        $this->assertArrayNotHasKey('request_id', $creative->provider);
        $this->assertSame('req_failed', $logged['request_id']);
        $this->assertSame('credits', $logged['reason']);
        $this->assertSame('failed', $logged['body']['status']);
        $this->assertSame('credit balance is too low', $logged['body']['detail']);
        Queue::assertPushed(RenderVideoCreativeJob::class, fn (RenderVideoCreativeJob $job): bool => $job->creativeId === $creative->id);
        Http::assertSent(function ($request): bool {
            $text = (string) ($request->data()['text'] ?? '');

            return str_contains($request->url(), 'hooks.slack.com/services/test')
                && str_contains($text, 'Request id: req_failed')
                && str_contains($text, 'credit balance is too low');
        });
    }

    public function test_higgsfield_credit_rejection_falls_back_to_veo_before_a_request_is_stored(): void
    {
        Queue::fake();
        config([
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'wan-3.0',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'services.slack.SLACK_WEBHOOK_URL' => 'https://hooks.slack.com/services/test',
        ]);
        Http::fake([
            'api.higgsfield.ai/alibaba/wan-3.0/text-to-video' => Http::response(
                ['detail' => 'Insufficient credits'],
                403,
            ),
            'hooks.slack.com/*' => Http::response('ok', 200),
        ]);

        $creative = $this->makeCreative(duration: 15);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertSame('processing', $creative->status);
        $this->assertSame('Switching to Veo', $creative->render_stage);
        $this->assertSame('veo', $creative->provider['engine']);
        $this->assertSame('credits', $creative->provider['higgsfield_failure_reason']);
        Queue::assertPushed(RenderVideoCreativeJob::class);
        Http::assertSent(function ($request): bool {
            $text = (string) ($request->data()['text'] ?? '');

            return str_contains($request->url(), 'hooks.slack.com/services/test')
                && str_contains($text, 'Request id: none')
                && str_contains($text, 'Insufficient credits');
        });
    }

    public function test_a_failed_slack_alert_still_falls_back_to_veo(): void
    {
        Queue::fake();
        config([
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'wan-3.0',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'services.slack.SLACK_WEBHOOK_URL' => 'https://hooks.slack.com/services/test',
        ]);
        Http::fake([
            'api.higgsfield.ai/alibaba/wan-3.0/text-to-video' => Http::response(
                ['detail' => 'Insufficient credits'],
                403,
            ),
            'hooks.slack.com/*' => Http::response('unavailable', 500),
        ]);

        $logged = false;
        Log::listen(function (MessageLogged $event) use (&$logged): void {
            if ($event->message === 'Slack alert failed' && ($event->context['http_status'] ?? null) === 500) {
                $logged = true;
            }
        });

        $creative = $this->makeCreative(duration: 15);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $creative->refresh();

        $this->assertTrue($logged);
        $this->assertSame('processing', $creative->status);
        $this->assertSame('Switching to Veo', $creative->render_stage);
        $this->assertSame('veo', $creative->provider['engine']);
        Queue::assertPushed(RenderVideoCreativeJob::class);
    }

    public function test_higgsfield_failure_skips_slack_when_the_webhook_is_blank(): void
    {
        Queue::fake();
        config([
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'wan-3.0',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'services.slack.SLACK_WEBHOOK_URL' => '',
        ]);
        Http::fake([
            'api.higgsfield.ai/alibaba/wan-3.0/text-to-video' => Http::response(
                ['detail' => 'Insufficient credits'],
                403,
            ),
            'hooks.slack.com/*' => Http::response('ok', 200),
        ]);

        $creative = $this->makeCreative(duration: 15);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);

        (new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        ))->handle(app(VideoAvatarProviderFactory::class));

        $this->assertSame('Switching to Veo', $creative->refresh()->render_stage);
        Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'hooks.slack.com'));
    }

    public function test_higgsfield_safety_rejection_does_not_fall_back_to_veo(): void
    {
        Queue::fake();
        config([
            'creative.higgsfield_seedance.key_id' => 'test-key-id',
            'creative.higgsfield_seedance.key_secret' => 'test-key-secret',
            'creative.higgsfield_seedance.base_url' => 'https://api.higgsfield.ai',
            'creative.higgsfield_model' => 'wan-3.0',
            'creative.higgsfield_seedance.poll_interval_seconds' => 0,
            'services.slack.SLACK_WEBHOOK_URL' => 'https://hooks.slack.com/services/test',
        ]);
        Http::fake([
            'api.higgsfield.ai/alibaba/wan-3.0/text-to-video' => Http::response([
                'status' => 'queued',
                'request_id' => 'req_nsfw',
                'status_url' => 'https://api.higgsfield.ai/requests/req_nsfw/status',
            ]),
            'api.higgsfield.ai/requests/req_nsfw/status' => Http::response([
                'status' => 'nsfw',
                'request_id' => 'req_nsfw',
            ]),
            'hooks.slack.com/*' => Http::response('ok', 200),
        ]);

        $creative = $this->makeCreative(duration: 8);
        $creative->update(['provider' => ['engine' => 'higgsfield-seedance']]);
        $job = new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        );

        try {
            $job->handle(app(VideoAvatarProviderFactory::class));
            $this->fail('Expected a safety rejection to fail the creative.');
        } catch (VideoPromptFilteredException) {
            // recorded on the creative, then rethrown
        }

        $this->assertSame('failed', $creative->refresh()->status);
        $this->assertSame('higgsfield-seedance', $creative->provider['engine']);
        Queue::assertNothingPushed();
        Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'hooks.slack.com'));
    }

    public function test_a_killed_higgsfield_worker_resumes_the_stored_request_instead_of_failing(): void
    {
        Queue::fake();
        $creative = $this->makeCreative(duration: 15);
        $creative->update([
            'status' => 'processing',
            'provider' => [
                'engine' => 'higgsfield-seedance',
                'request_id' => 'req_killed',
                'status_url' => 'https://platform.higgsfield.ai/requests/req_killed/status',
            ],
        ]);
        $job = new RenderVideoCreativeJob(
            $creative->id,
            $creative->avatar_id,
            $creative->voice_id,
            $creative->script,
            $creative->storyboard,
            $creative->duration,
            $creative->ratio,
        );

        $job->failed(new RuntimeException('worker timed out'));

        $creative->refresh();
        $this->assertSame('processing', $creative->status);
        $this->assertNull($creative->failure_reason);
        $this->assertSame('Waiting on Higgsfield', $creative->render_stage);
        Queue::assertPushed(RenderVideoCreativeJob::class, 1);
    }
}
