<?php

namespace Tests\Unit\Services;

use App\Exceptions\ProviderNotConfiguredException;
use App\Models\Avatar;
use App\Models\Voice;
use App\Services\AI\GeminiVeoVideoAvatarProvider;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Mockery;
use RuntimeException;
use Tests\TestCase;

class GeminiVeoVideoAvatarProviderTest extends TestCase
{
    private function script(): array
    {
        return [
            'hook' => 'Tired of slow browsing?',
            'problem' => 'Ads slow you down.',
            'product' => 'Chromium Browser',
            'benefits' => 'Faster pages, no clutter.',
            'cta' => 'Download free today.',
        ];
    }

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

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

        (new GeminiVeoVideoAvatarProvider)->render(
            new Avatar(['name' => 'Ava', 'gender' => 'female', 'accent' => 'American']),
            new Voice(['name' => 'Kore', 'tone' => 'warm']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 15,
            ratio: '9:16',
        );
    }

    public function test_it_submits_polls_and_downloads_the_generated_video(): 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,
        ]);

        $logContexts = [];
        $logger = Mockery::mock();
        $logger->shouldReceive('info')->andReturnUsing(function (string $message, array $context) use (&$logContexts): void {
            $logContexts[] = $context;
        });
        Log::shouldReceive('channel')->with('ai_video')->andReturn($logger);

        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),
        ]);

        $avatarImage = glob(public_path('assets/avatars/*.png'))[0];
        $avatarPath = 'assets/avatars/'.basename($avatarImage);

        $result = (new GeminiVeoVideoAvatarProvider)->render(
            new Avatar(['name' => 'Ava', 'gender' => 'female', 'accent' => 'American', 'source_path' => $avatarPath]),
            new Voice(['name' => 'Kore', 'tone' => 'warm']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 15,
            ratio: '9:16',
        );

        $this->assertSame('gemini-veo', $result->meta['provider']);
        Http::assertSent(fn ($request): bool => str_contains($request->url(), 'predictLongRunning')
            && str_contains($request['instances'][0]['prompt'], 'preserve their face, hair, complexion, and outfit consistently'));
        $this->assertSame(8, $result->meta['duration']);
        $this->assertArrayNotHasKey('prompt', $result->meta);
        $this->assertSame(64, strlen($result->meta['prompt_hash']));
        $this->assertNotEmpty($logContexts);
        foreach ($logContexts as $context) {
            $this->assertArrayNotHasKey('prompt', $context);
            $this->assertArrayNotHasKey('payload', $context);
            $this->assertArrayNotHasKey('body', $context);
        }
        Storage::disk('public')->assertExists($result->assetPath);
        $this->assertSame('FAKEVIDEOBYTES', Storage::disk('public')->get($result->assetPath));
        Http::assertSent(fn ($request) => str_contains($request->url(), 'predictLongRunning')
            && $request['parameters']['durationSeconds'] === 8
            && $request['parameters']['aspectRatio'] === '9:16'
            && $request['instances'][0]['referenceImages'][0]['referenceType'] === 'asset'
            && base64_decode($request['instances'][0]['referenceImages'][0]['image']['inlineData']['data']) === file_get_contents($avatarImage));
    }

    public function test_it_throws_when_polling_times_out(): void
    {
        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' => 2,
        ]);

        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' => false,
            ]),
        ]);

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('Veo video generation timed out');

        (new GeminiVeoVideoAvatarProvider)->render(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 8,
            ratio: '16:9',
        );
    }

    public function test_it_keeps_spoken_copy_in_storyboard_order(): void
    {
        config(['creative.spoken_words_per_second' => 10]);

        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 8,
        );

        $this->assertMatchesRegularExpression(
            '/Tired of slow browsing\?.*Ads slow you down\..*Chromium Browser.*Faster pages, no clutter\..*Download free today\./s',
            $prompt,
        );
    }

    public function test_it_builds_an_extension_prompt_with_only_the_supplied_dialogue_and_scenes(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            $this->script(),
            [['scene' => 'Show the final benefit'], ['scene' => 'Show the call to action']],
            duration: 7,
            spokenLine: 'Faster pages, no clutter. Download free today.',
            stageInstruction: 'Continue seamlessly. Do not repeat earlier dialogue.',
        );

        $this->assertStringContainsString('Continue seamlessly. Do not repeat earlier dialogue.', $prompt);
        $this->assertStringContainsString('Faster pages, no clutter. Download free today.', $prompt);
        $this->assertStringNotContainsString('Tired of slow browsing?', $prompt);
        $this->assertStringContainsString('Scene 1: Show the final benefit', $prompt);
        $this->assertStringContainsString('Scene 2: Show the call to action', $prompt);
    }

    public function test_it_uses_the_catalogue_video_production_prompt(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 8,
            style: 'Splash Energy',
        );

        $this->assertStringContainsString('high-speed liquid splash inserts', $prompt);
        $this->assertStringContainsString('product and splash shots only between dialogue beats', $prompt);
        $this->assertStringNotContainsString('Visual style: Splash Energy', $prompt);
    }

    public function test_it_uses_technical_ugc_video_direction(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 8,
            style: 'UGC',
        );

        $this->assertStringContainsString('stable front-facing phone footage', $prompt);
        $this->assertStringContainsString('minimal cuts while speaking', $prompt);
    }

    public function test_it_normalizes_domains_acronyms_and_symbols_for_speech(): void
    {
        config(['creative.spoken_words_per_second' => 10]);

        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            [
                'hook' => 'AOLL saves 20% at StockPrices.com.',
                'problem' => 'Research & execution feel disconnected.',
                'product' => '',
                'benefits' => '',
                'cta' => '',
            ],
            [['scene' => 'Opening shot']],
            duration: 8,
        );

        $this->assertStringContainsString('A O L L saves 20 percent at Stock Prices dot com.', $prompt);
        $this->assertStringContainsString('Research and execution feel disconnected.', $prompt);
        $this->assertStringContainsString('Pronounce separated capital letters individually', $prompt);
    }

    public function test_it_includes_commercial_context_and_product_integration_in_prompt(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava', 'gender' => 'female']),
            new Voice(['name' => 'Kore', 'tone' => 'friendly']),
            $this->script(),
            [['scene' => 'Opening hook shot']],
            duration: 8,
            style: 'Clean Beauty',
            context: [
                'project_name' => 'GlowCare Organics',
                'brief' => [
                    'product_name' => 'Hydrating Vitamin C Serum',
                    'features' => ['Cold-pressed rosehip oil', '15% stabilized L-ascorbic acid'],
                    'benefits' => ['Instantly brightens dull skin', '24-hour moisture lock'],
                    'messaging' => 'Dermatologist tested clean skincare.',
                ],
            ],
        );

        $this->assertStringContainsString('Brand/Project: GlowCare Organics', $prompt);
        $this->assertStringContainsString('Featured Product: Hydrating Vitamin C Serum', $prompt);
        $this->assertStringContainsString('Key Features: Cold-pressed rosehip oil; 15% stabilized L-ascorbic acid', $prompt);
        $this->assertStringContainsString('Core Benefits: Instantly brightens dull skin; 24-hour moisture lock', $prompt);
        $this->assertStringContainsString('Campaign Context: Dermatologist tested clean skincare.', $prompt);
        $this->assertStringContainsString('Commercial Context & Product Integration:', $prompt);
        $this->assertStringContainsString('realistic physical textures, authentic lighting', $prompt);
    }

    public function test_it_describes_the_presenter_without_using_the_avatar_given_name(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar([
                'name' => 'Priya',
                'gender' => 'female',
                'accent' => 'Indian',
                'meta' => ['visual_description' => 'a poised 28-year-old South Asian woman with glowing warm skin'],
            ]),
            new Voice(['name' => 'Kore', 'tone' => 'friendly']),
            $this->script(),
            [['scene' => 'Opening shot']],
            duration: 8,
        );

        $this->assertStringNotContainsString('Priya', $prompt);
        $this->assertStringContainsString('a poised 28-year-old South Asian woman with glowing warm skin', $prompt);
        $this->assertStringContainsString('- Presenter:', $prompt);
        $this->assertStringContainsString('Do not depict any real celebrity', $prompt);
    }

    public function test_it_retries_a_likeness_filter_without_named_people_or_brands_and_keeps_product_look(): void
    {
        Storage::fake('public');
        config([
            'ai.providers.gemini.key' => 'test-key',
            'ai.providers.gemini.url' => 'https://generativelanguage.googleapis.com/v1beta/',
            'creative.gemini_veo.model' => 'veo-3.1-generate-preview',
            'creative.gemini_veo.poll_interval_seconds' => 0,
            'creative.gemini_veo.max_poll_attempts' => 3,
            'creative.spoken_words_per_second' => 10,
        ]);

        $warningContexts = [];
        $logger = Mockery::mock();
        $logger->shouldReceive('info')->zeroOrMoreTimes();
        $logger->shouldReceive('warning')->andReturnUsing(function (string $message, array $context) use (&$warningContexts): void {
            $warningContexts[$message] = $context;
        });
        Log::shouldReceive('channel')->with('ai_video')->andReturn($logger);

        $operation = 0;
        Http::fake(function (Request $request) use (&$operation) {
            if (str_contains($request->url(), 'predictLongRunning')) {
                $operation++;

                return Http::response(['name' => "models/veo-3.1-generate-preview/operations/op{$operation}"]);
            }

            if (str_contains($request->url(), 'operations/op1')) {
                return Http::response([
                    'done' => true,
                    'response' => [
                        'generateVideoResponse' => [
                            'raiMediaFilteredCount' => 1,
                            'raiMediaFilteredReasons' => [
                                "Sorry, we can't create videos with real people's names or likenesses. Please remove the celebrity reference and try again.",
                            ],
                        ],
                    ],
                ]);
            }

            if (str_contains($request->url(), 'operations/op2')) {
                return Http::response([
                    'done' => true,
                    'response' => [
                        'generateVideoResponse' => [
                            'generatedSamples' => [
                                ['video' => ['uri' => 'https://generativelanguage.googleapis.com/v1beta/files/retry:download']],
                            ],
                        ],
                    ],
                ]);
            }

            if (str_contains($request->url(), 'files/retry')) {
                return Http::response('FAKEVIDEOBYTES', 200);
            }

            return Http::response(['unexpected' => $request->url()], 404);
        });

        $visual = 'pale mint-green structured jacket with a paisley scarf';

        $result = (new GeminiVeoVideoAvatarProvider)->render(
            new Avatar([
                'name' => 'Priya',
                'gender' => 'female',
                'accent' => 'Indian',
                'meta' => ['visual_description' => 'a poised 28-year-old South Asian woman with glowing warm skin'],
            ]),
            new Voice(['name' => 'Kore', 'tone' => 'friendly']),
            [
                'hook' => 'Waiting for shipping?',
                'problem' => 'Style delayed?',
                'product' => 'Shop Nordstrom Rack.',
                'benefits' => 'Save up to 70 percent.',
                'cta' => 'Shop now, pick up free.',
            ],
            [
                ['scene' => 'Presenter delivers the hook: "Waiting for shipping?"'],
                ['scene' => 'Product hero: "Shop Nordstrom Rack."'],
            ],
            duration: 8,
            ratio: '16:9',
            style: 'UGC',
            context: [
                'project_name' => 'Nordstrom Rack',
                'brief' => [
                    'product_name' => 'Nordstrom Rack Online Sale with Free Store Pickup',
                    'visual_description' => $visual,
                    'features' => ['Up to 70% off select merchandise'],
                    'benefits' => ['Access discounted apparel from brands including Nike, Vince, Madewell, and adidas'],
                    'messaging' => 'Energetic fashion-forward value messaging.',
                ],
            ],
        );

        $prompts = collect(Http::recorded())
            ->map(fn (array $record) => $record[0])
            ->filter(fn (Request $request) => str_contains($request->url(), 'predictLongRunning'))
            ->map(fn (Request $request) => $request['instances'][0]['prompt'])
            ->values();

        $this->assertSame(
            "Sorry, we can't create videos with real people's names or likenesses. Please remove the celebrity reference and try again.",
            $warningContexts['Veo safety-filtered the video']['reason'],
        );
        $this->assertArrayNotHasKey('prompt', $warningContexts['Veo safety-filtered the video']);
        $this->assertCount(2, $prompts);
        $this->assertStringNotContainsString('Priya', $prompts[0]);
        $this->assertStringContainsString('Nordstrom Rack', $prompts[0]);
        $this->assertStringContainsString($visual, $prompts[0]);
        $this->assertStringContainsString('stable front-facing phone footage', $prompts[0]);
        $this->assertStringNotContainsString('Nordstrom Rack', $prompts[1]);
        $this->assertStringNotContainsString('Nike', $prompts[1]);
        $this->assertStringContainsString($visual, $prompts[1]);
        $this->assertStringContainsString('stable front-facing phone footage', $prompts[1]);
        $this->assertStringContainsString('original fictional commercial actor', $prompts[1]);
        $this->assertArrayNotHasKey('prompt', $result->meta);
        $this->assertSame(hash('sha256', $prompts[1]), $result->meta['prompt_hash']);
        Storage::disk('public')->assertExists($result->assetPath);
        $this->assertSame('FAKEVIDEOBYTES', Storage::disk('public')->get($result->assetPath));
    }

    public function test_it_logs_vendor_reasons_for_failed_generation_and_poll_requests(): void
    {
        config([
            'ai.providers.gemini.key' => 'test-key',
            'ai.providers.gemini.url' => 'https://generativelanguage.googleapis.com/v1beta/',
        ]);

        $errorContexts = [];
        $logger = Mockery::mock();
        $logger->shouldReceive('info')->zeroOrMoreTimes();
        $logger->shouldReceive('error')->andReturnUsing(function (string $message, array $context) use (&$errorContexts): void {
            $errorContexts[$message] = $context;
        });
        Log::shouldReceive('channel')->with('ai_video')->andReturn($logger);

        $stage = 'generation';
        Http::fake(function (Request $request) use (&$stage) {
            if ($stage === 'generation') {
                return Http::response(['error' => ['code' => 400, 'message' => 'The requested duration is not supported.']], 400);
            }

            if (str_contains($request->url(), 'predictLongRunning')) {
                return Http::response(['name' => 'models/veo-3.1-generate-preview/operations/rate-limited']);
            }

            return Http::response(['error' => ['code' => 429, 'message' => 'The polling quota has been exceeded.']], 429);
        });

        foreach (['generation', 'poll'] as $stage) {
            try {
                (new GeminiVeoVideoAvatarProvider)->render(
                    new Avatar(['name' => 'Ava']),
                    new Voice(['name' => 'Kore']),
                    $this->script(),
                    [['scene' => 'Opening shot']],
                    duration: 8,
                    ratio: '16:9',
                );
                $this->fail("Expected the {$stage} request to fail.");
            } catch (RuntimeException $exception) {
                $this->assertStringNotContainsString('The requested duration is not supported.', $exception->getMessage());
                $this->assertStringNotContainsString('The polling quota has been exceeded.', $exception->getMessage());
            }
        }

        $this->assertSame('The requested duration is not supported.', $errorContexts['Veo generation request failed']['reason']);
        $this->assertSame('The polling quota has been exceeded.', $errorContexts['Veo operation polling failed']['reason']);
        $this->assertArrayNotHasKey('prompt', $errorContexts['Veo generation request failed']);
        $this->assertArrayNotHasKey('prompt', $errorContexts['Veo operation polling failed']);
    }

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

        $errorContexts = [];
        $logger = Mockery::mock();
        $logger->shouldReceive('info')->zeroOrMoreTimes();
        $logger->shouldReceive('error')->andReturnUsing(function (string $message, array $context) use (&$errorContexts): void {
            $errorContexts[$message] = $context;
        });
        Log::shouldReceive('channel')->with('ai_video')->andReturn($logger);

        Http::fake([
            'generativelanguage.googleapis.com/v1beta/models/*:predictLongRunning' => Http::response([
                'name' => 'models/veo-3.1-generate-preview/operations/failed',
            ]),
            'generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview/operations/failed' => Http::response([
                'done' => true,
                'error' => ['code' => 400, 'message' => 'Veo rejected this scene because it violates the video safety policy.'],
            ]),
        ]);

        try {
            (new GeminiVeoVideoAvatarProvider)->render(
                new Avatar(['name' => 'Ava']),
                new Voice(['name' => 'Kore']),
                $this->script(),
                [['scene' => 'Opening shot']],
                duration: 8,
                ratio: '16:9',
            );
            $this->fail('Expected the failed operation to raise an exception.');
        } catch (RuntimeException $exception) {
            $this->assertSame('Veo generation failed. See logs for the vendor error.', $exception->getMessage());
        }

        $this->assertSame(400, $errorContexts['Veo generation failed']['error_code']);
        $this->assertSame(
            'Veo rejected this scene because it violates the video safety policy.',
            $errorContexts['Veo generation failed']['reason'],
        );
        $this->assertArrayNotHasKey('prompt', $errorContexts['Veo generation failed']);
    }

    public function test_it_does_not_retry_when_veo_completes_without_a_likeness_filter_reason(): void
    {
        config([
            'ai.providers.gemini.key' => 'test-key',
            'ai.providers.gemini.url' => 'https://generativelanguage.googleapis.com/v1beta/',
            'creative.gemini_veo.model' => 'veo-3.1-generate-preview',
            '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.1-generate-preview/operations/abc123',
            ]),
            'generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview/operations/abc123' => Http::response([
                'done' => true,
                'response' => ['generateVideoResponse' => []],
            ]),
        ]);

        try {
            (new GeminiVeoVideoAvatarProvider)->render(
                new Avatar(['name' => 'Ava']),
                new Voice(['name' => 'Kore']),
                $this->script(),
                [['scene' => 'Opening shot']],
                duration: 8,
                ratio: '16:9',
            );
            $this->fail('Expected render to fail when Veo returns no video URI.');
        } catch (RuntimeException $e) {
            $this->assertSame('Veo operation completed without a video URI.', $e->getMessage());
        }

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

        $this->assertCount(1, $predictRequests);
    }

    public function test_it_rejects_overlong_spoken_dialogue_before_calling_veo(): void
    {
        config([
            'ai.providers.gemini.key' => 'test-key',
            'creative.spoken_words_per_second' => 1.7,
        ]);
        Http::fake();

        $script = $this->script();
        $script['spoken_line'] = implode(' ', array_fill(0, 12, 'word'));

        try {
            (new GeminiVeoVideoAvatarProvider)->render(
                new Avatar(['name' => 'Ava']),
                new Voice(['name' => 'Kore']),
                $script,
                [['scene' => 'Opening shot']],
                duration: 8,
                ratio: '16:9',
            );
            $this->fail('Expected dialogue validation to fail.');
        } catch (\InvalidArgumentException $exception) {
            $this->assertStringContainsString('too long', $exception->getMessage());
        }

        Http::assertNothingSent();
    }

    public function test_it_uses_the_selected_voice_accent_in_the_video_prompt(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Noah', 'gender' => 'male', 'accent' => 'British']),
            new Voice(['name' => 'Echo', 'gender' => 'male', 'accent' => 'American', 'tone' => 'upbeat']),
            ['spoken_line' => 'Discover faster browsing. Download today.'],
            [['scene' => 'Presenter addresses the camera']],
            duration: 8,
        );

        $this->assertStringContainsString('American accent, speaking with upbeat delivery', $prompt);
        $this->assertStringNotContainsString('British accent', $prompt);
    }

    public function test_it_does_not_claim_an_avatar_reference_image_was_supplied(): void
    {
        $prompt = (new GeminiVeoVideoAvatarProvider)->buildPromptPublic(
            new Avatar(['name' => 'Ava']),
            new Voice(['name' => 'Kore']),
            ['spoken_line' => 'Discover faster browsing. Download today.'],
            [['scene' => 'Presenter addresses the camera']],
            duration: 8,
        );

        $this->assertStringNotContainsString('supplied reference image', $prompt);
        $this->assertStringNotContainsString('Ava', $prompt);
    }
}
