<?php

namespace Tests\Feature\Creatives;

use App\Jobs\RenderVideoCreativeJob;
use App\Livewire\Creatives\ChatWizard;
use App\Models\Avatar;
use App\Models\Creative;
use App\Models\User;
use App\Models\Voice;
use Database\Seeders\AvatarSeeder;
use Database\Seeders\VoiceSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
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;

    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_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_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_completes_synchronously_and_shows_result_inline(): void
    {
        Storage::fake('public');
        $this->fakeAiBrief();
        Image::fake();

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

        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');

        $this->assertDatabaseCount('creatives', 1);

        $creative = Creative::sole();
        $this->assertSame($user->id, $creative->user_id);
        $this->assertSame('1:1', $creative->ratio);
        $this->assertSame('prompt', $creative->source);
        $this->assertSame('completed', $creative->status);
        $this->assertNotNull($creative->asset_path);
        $this->assertNull($creative->variation_group_id);
        Storage::disk('public')->assertExists($creative->asset_path);
    }

    public function test_confirm_generate_with_multiple_quantity_shares_a_variation_group(): void
    {
        Storage::fake('public');
        $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->status === 'completed'));
    }

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

        $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('skipStyle')
            ->call('selectQuantity', 1)
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('failed', $creative->status);
        // The raw vendor/exception message is logged, not shown to the user — the UI only
        // ever sees a generic message so vendor error bodies can't leak request detail.
        $this->assertSame('Generation failed. Please try again.', $creative->failure_reason);
    }

    public function test_video_is_selectable_and_walks_through_the_extra_video_steps(): void
    {
        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', 'problem' => 'Problem A', 'product' => 'Product A', 'benefits' => 'Benefits A', 'cta' => 'CTA A'],
            ['hook' => 'Hook B', 'problem' => 'Problem B', 'product' => 'Product B', 'benefits' => 'Benefits B', 'cta' => 'CTA B'],
            ['hook' => 'Hook C', 'problem' => 'Problem C', 'product' => 'Product C', 'benefits' => 'Benefits C', 'cta' => 'CTA C'],
        ]);

        $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', 3)
            ->call('selectScript', 0)
            ->assertSet('step', 'avatar')
            ->call('selectAvatar', $avatar->id)
            ->assertSet('step', 'voice')
            ->call('selectVoice', $voice->id)
            ->assertSet('step', 'storyboard')
            ->assertCount('storyboard', 5)
            ->call('confirmStoryboard')
            ->assertSet('step', 'summary')
            ->call('confirmGenerate')
            ->assertSet('step', 'done');

        $creative = Creative::sole();
        $this->assertSame('video', $creative->type);
        $this->assertSame(8, $creative->duration);
        $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_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_video_ratio_is_restricted_to_veo_and_sora_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(\Illuminate\Database\Eloquent\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', '');
    }
}
