<?php

declare(strict_types=1);

use App\Models\Artwork;
use App\Models\CreatorAiBiography;
use App\Models\User;
use App\Services\AiBiography\AiBiographyInputBuilder;
use App\Services\AiBiography\AiBiographyGenerator;
use App\Services\AiBiography\AiBiographyPromptBuilder;
use App\Services\AiBiography\AiBiographyService;
use App\Services\AiBiography\AiBiographyValidator;
use App\Services\AiBiography\VisionLlmClient;
use App\Services\AiBiography\VisionLlmException;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;

uses(RefreshDatabase::class);

// ─────────────────────────────────────────────────────────────────────────────
// AiBiographyInputBuilder
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyInputBuilder', function () {
    it('includes only public uploads in uploads_count', function () {
        $user = User::factory()->create(['created_at' => '2020-01-01']);

        // Public artwork
        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2021-06-01',
            'deleted_at'   => null,
        ]);

        // Private artwork — must not be counted
        Artwork::factory()->for($user)->create([
            'is_public'    => false,
            'is_approved'  => false,
            'published_at' => '2021-08-01',
            'deleted_at'   => null,
        ]);

        // Soft-deleted artwork — must not be counted
        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2021-09-01',
            'deleted_at'   => now(),
        ]);

        $builder = new AiBiographyInputBuilder();
        $input   = $builder->build($user);

        expect($input['uploads_count'])->toBe(1);
    });

    it('derives member_since_year from user created_at', function () {
        $user    = User::factory()->create(['created_at' => '2004-03-15']);
        $builder = new AiBiographyInputBuilder();
        $input   = $builder->build($user);

        expect($input['member_since_year'])->toBe(2004);
    });

    it('source hash changes when upload count changes', function () {
        $user    = User::factory()->create(['created_at' => '2015-01-01']);
        $builder = new AiBiographyInputBuilder();

        $hash1 = $builder->sourceHash($builder->build($user));

        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2021-06-01',
            'deleted_at'   => null,
        ]);

        $hash2 = $builder->sourceHash($builder->build($user));

        expect($hash1)->not()->toBe($hash2);
    });

    it('source hash is stable for the same data', function () {
        $user    = User::factory()->create(['created_at' => '2015-01-01']);
        $builder = new AiBiographyInputBuilder();

        $hash1 = $builder->sourceHash($builder->build($user));
        $hash2 = $builder->sourceHash($builder->build($user));

        expect($hash1)->toBe($hash2);
    });

    it('source hash is stable when only downloads_count changes', function () {
        $user    = User::factory()->create(['created_at' => '2015-01-01']);
        $builder = new AiBiographyInputBuilder();

        $input1 = $builder->build($user);
        $input2 = array_merge($input1, ['downloads_count' => ($input1['downloads_count'] ?? 0) + 5000]);

        expect($builder->sourceHash($input1))->toBe($builder->sourceHash($input2));
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// AiBiographyValidator
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyValidator', function () {
    $validator = fn () => new AiBiographyValidator();

    it('accepts a valid single paragraph', function () use ($validator) {
        $text = 'Gregor has been part of Skinbase since 2004, building a long-running portfolio centered on wallpapers and digital art. Over the years his work has earned featured selections, with Blue Eclipse standing out as a strong piece. His creator journey shows both long-term consistency and a notable comeback after a significant hiatus.';

        expect($validator()->isValid($text))->toBeTrue();
    });

    it('rejects empty text', function () use ($validator) {
        expect($validator()->validate(''))->not()->toBeEmpty();
    });

    it('rejects text that is too short', function () use ($validator) {
        expect($validator()->validate('Too short.'))->not()->toBeEmpty();
    });

    it('rejects text that is too long', function () use ($validator) {
        $long = str_repeat('This is a very long sentence about an artist creator person who lives and works. ', 30);
        expect($validator()->validate($long))->not()->toBeEmpty();
    });

    it('rejects markdown headings', function () use ($validator) {
        $text = "## About the Creator\n\nCreator has been active since 2010 with many quality artworks.";
        expect($validator()->validate($text))->not()->toBeEmpty();
    });

    it('rejects bullet points', function () use ($validator) {
        $text = "- Member since 2010\n- Featured works: 3\n- Top category: wallpapers";
        expect($validator()->validate($text))->not()->toBeEmpty();
    });

    it('rejects multiple paragraphs', function () use ($validator) {
        $text = "First paragraph about the creator and their history on the platform.\n\nSecond paragraph providing additional made-up details about style and themes.";
        expect($validator()->validate($text))->not()->toBeEmpty();
    });

    it('rejects forbidden hype phrases', function () use ($validator) {
        $text = 'This legendary world-class creator has been active since 2010 and has defined the platform with their iconic visionary approach to digital art.';
        expect($validator()->validate($text))->not()->toBeEmpty();
    });

    it('rejects AI apology patterns', function () use ($validator) {
        $text = 'I cannot write a biography for this user as I do not have enough information about their artistic achievements.';
        expect($validator()->validate($text))->not()->toBeEmpty();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// AiBiographyService — storage and visibility
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyService', function () {
    function makeService(?string $generatedText = null, bool $generationSuccess = true): AiBiographyService
    {
        $mockGenerator = Mockery::mock(AiBiographyGenerator::class);
        $mockGenerator->shouldReceive('generate')->andReturn([
            'success'        => $generationSuccess,
            'text'           => $generatedText,
            'errors'         => $generationSuccess ? [] : ['Mocked failure'],
            'model'          => 'test-model',
            'prompt_version' => 'v1.1',
            'was_retried'    => false,
        ]);

        return new AiBiographyService(new AiBiographyInputBuilder(), $mockGenerator);
    }

    it('stores biography and marks it active on successful generation', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);
        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subMonths($i + 1), 'deleted_at' => null]);
        }
        $service = makeService('A valid biography text with enough words to pass the test validation check here.');

        $result = $service->generate($user);

        expect($result['success'])->toBeTrue();
        expect(CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->exists())->toBeTrue();
    });

    it('does not overwrite a user-edited biography on automatic generate', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);
        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subMonths($i + 1), 'deleted_at' => null]);
        }

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Custom written bio by the creator.',
            'source_hash'    => 'abc123',
            'model'          => null,
            'status'         => CreatorAiBiography::STATUS_EDITED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => true,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService('A newly generated biography text for the creator.');
        $result  = $service->generate($user);

        // The draft is stored, but the original edited bio remains active.
        expect($result['success'])->toBeTrue();
        expect($result['action'])->toBe('draft_stored');

        $active = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($active->is_user_edited)->toBeTrue();
        expect($active->text)->toBe('Custom written bio by the creator.');
    });

    it('does not expose hidden biography in publicPayload', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'A hidden biography that should not be shown.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => true,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService();
        $payload = $service->publicPayload($user);

        expect($payload)->toBeNull();
    });

    it('returns null publicPayload when no biography exists', function () {
        $user    = User::factory()->create(['created_at' => '2015-01-01']);
        $service = makeService();

        expect($service->publicPayload($user))->toBeNull();
    });

    it('returns structured payload when biography is visible', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'A valid biography for public display about the creator here.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService();
        $payload = $service->publicPayload($user);

        expect($payload)->toBeArray();
        expect($payload['is_visible'])->toBeTrue();
        expect($payload['text'])->not()->toBeEmpty();
    });

    it('hide makes biography invisible', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Visible biography about this creator with enough words here.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService();
        $service->hide($user);

        expect($service->publicPayload($user))->toBeNull();
    });

    it('show restores biography visibility', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'A hidden biography about the creator for testing purposes.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => true,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService();
        $service->show($user);

        $payload = $service->publicPayload($user);
        expect($payload)->not()->toBeNull();
        expect($payload['is_visible'])->toBeTrue();
    });

    it('updateText marks biography as user-edited', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Generated biography text here about the creator.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService();
        $service->updateText($user, 'My custom biography text that I wrote myself and is accurate.');

        $record = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($record->is_user_edited)->toBeTrue();
        expect($record->status)->toBe(CreatorAiBiography::STATUS_EDITED);
        expect($record->text)->toBe('My custom biography text that I wrote myself and is accurate.');
    });

    it('failed generation does not create a biography record', function () {
        $user    = User::factory()->create(['created_at' => '2015-01-01']);
        $service = makeService(null, false);

        $result = $service->generate($user);

        expect($result['success'])->toBeFalse();
        expect(CreatorAiBiography::where('user_id', $user->id)->exists())->toBeFalse();
    });

    it('regenerate with force replaces user-edited biography', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);
        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subMonths($i + 1), 'deleted_at' => null]);
        }

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Custom written bio by the creator.',
            'source_hash'    => 'abc123',
            'model'          => null,
            'status'         => CreatorAiBiography::STATUS_EDITED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => true,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $service = makeService('A newly generated fresh biography text for the creator who has many works here.');
        $result  = $service->regenerate($user, true);

        expect($result['success'])->toBeTrue();
        expect($result['action'])->toBe('generated');

        $active = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($active->is_user_edited)->toBeFalse();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// VisionLlmClient — HTTP error handling
// ─────────────────────────────────────────────────────────────────────────────

describe('VisionLlmClient', function () {
    it('throws VisionLlmException on 401', function () {
        config(['vision.gateway.base_url' => 'http://vision.test', 'vision.gateway.api_key' => 'test-key']);

        Http::fake([
            'vision.test/*' => Http::response(['error' => 'Unauthorized'], 401),
        ]);

        $client = new VisionLlmClient();
        expect(fn () => $client->chat([
            'messages'    => [['role' => 'user', 'content' => 'test']],
            'max_tokens'  => 100,
            'temperature' => 0.5,
            'stream'      => false,
        ]))->toThrow(VisionLlmException::class);
    });

    it('throws VisionLlmException on 503', function () {
        config(['vision.gateway.base_url' => 'http://vision.test', 'vision.gateway.api_key' => 'test-key']);

        Http::fake([
            'vision.test/*' => Http::response(['error' => 'Service unavailable'], 503),
        ]);

        $client = new VisionLlmClient();
        expect(fn () => $client->chat([
            'messages'    => [['role' => 'user', 'content' => 'test']],
            'max_tokens'  => 100,
            'temperature' => 0.5,
            'stream'      => false,
        ]))->toThrow(VisionLlmException::class);
    });

    it('returns generated text on success', function () {
        config([
            'ai_biography.provider' => 'vision_gateway',
            'vision.gateway.base_url' => 'http://vision.test',
            'vision.gateway.api_key' => 'test-key',
            'ai_biography.llm_endpoint' => '/ai/chat',
        ]);

        Http::fake([
            'vision.test/ai/chat' => Http::response([
                'choices' => [[
                    'message' => ['content' => 'A creator biography from the gateway.'],
                ]],
            ], 200),
        ]);

        $client = new VisionLlmClient();
        $text   = $client->chat([
            'messages'    => [['role' => 'user', 'content' => 'test']],
            'max_tokens'  => 100,
            'temperature' => 0.5,
            'stream'      => false,
        ]);

        expect($text)->toBe('A creator biography from the gateway.');
    });

    it('throws VisionLlmException when gateway is not configured', function () {
        config(['vision.gateway.base_url' => '', 'vision.gateway.api_key' => '']);

        $client = new VisionLlmClient();
        expect(fn () => $client->chat([
            'messages'    => [['role' => 'user', 'content' => 'test']],
            'max_tokens'  => 100,
            'temperature' => 0.5,
            'stream'      => false,
        ]))->toThrow(VisionLlmException::class);
    });

    it('maps biography payload to Gemini generateContent and returns text', function () {
        config([
            'ai_biography.provider' => 'gemini',
            'ai_biography.gemini.base_url' => 'https://generativelanguage.googleapis.com',
            'ai_biography.gemini.api_key' => 'gemini-test-key',
            'ai_biography.gemini.model' => 'gemini-flash-latest',
        ]);

        Http::fake(function ($request) {
            expect($request->url())->toBe('https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent');
            expect($request->hasHeader('X-goog-api-key', 'gemini-test-key'))->toBeTrue();
            expect($request['systemInstruction']['parts'][0]['text'])->toBe('System rules.');
            expect($request['contents'][0]['role'])->toBe('user');
            expect($request['contents'][0]['parts'][0]['text'])->toBe('Write a grounded creator biography.');
            expect($request['generationConfig']['maxOutputTokens'])->toBe(450);
            expect($request['generationConfig']['temperature'])->toBe(0.2);

            return Http::response([
                'candidates' => [[
                    'content' => [
                        'parts' => [
                            ['text' => 'A creator biography from Gemini.'],
                        ],
                    ],
                ]],
            ], 200);
        });

        $client = new VisionLlmClient();
        $text   = $client->chat([
            'messages' => [
                ['role' => 'system', 'content' => 'System rules.'],
                ['role' => 'user', 'content' => 'Write a grounded creator biography.'],
            ],
            'max_tokens' => 450,
            'temperature' => 0.2,
            'stream' => false,
        ]);

        expect($text)->toBe('A creator biography from Gemini.');
    });

    it('throws VisionLlmException when Gemini is not configured', function () {
        config([
            'ai_biography.provider' => 'gemini',
            'ai_biography.gemini.base_url' => 'https://generativelanguage.googleapis.com',
            'ai_biography.gemini.api_key' => '',
            'ai_biography.gemini.model' => 'gemini-flash-latest',
        ]);

        $client = new VisionLlmClient();
        expect(fn () => $client->chat([
            'messages'    => [['role' => 'user', 'content' => 'test']],
            'max_tokens'  => 100,
            'temperature' => 0.5,
            'stream'      => false,
        ]))->toThrow(VisionLlmException::class);
    });

    it('maps biography payload to Home LM Studio OpenAI-compatible API and returns text', function () {
        config([
            'ai_biography.provider' => 'home',
            'ai_biography.home.base_url' => 'http://home.klevze.si:8200',
            'ai_biography.home.endpoint' => '/v1/chat/completions',
            'ai_biography.home.model' => 'google/gemma-3-4b',
            'ai_biography.home.api_key' => '',
            'ai_biography.home.verify_ssl' => true,
        ]);

        Http::fake(function ($request) {
            expect($request->url())->toBe('http://home.klevze.si:8200/v1/chat/completions');
            expect($request['model'])->toBe('google/gemma-3-4b');
            expect($request['messages'][0]['role'])->toBe('system');
            expect($request['messages'][1]['content'])->toBe('Write a grounded creator biography.');
            expect($request['max_tokens'])->toBe(450);
            expect($request['temperature'])->toBe(0.2);
            expect($request['stream'])->toBeFalse();

            return Http::response([
                'choices' => [[
                    'message' => ['content' => 'A creator biography from Home LM Studio.'],
                ]],
            ], 200);
        });

        $client = new VisionLlmClient();
        $text   = $client->chat([
            'messages' => [
                ['role' => 'system', 'content' => 'System rules.'],
                ['role' => 'user', 'content' => 'Write a grounded creator biography.'],
            ],
            'max_tokens' => 450,
            'temperature' => 0.2,
            'stream' => false,
        ]);

        expect($text)->toBe('A creator biography from Home LM Studio.');
    });

    it('throws VisionLlmException when Home LM Studio is not configured', function () {
        config([
            'ai_biography.provider' => 'home',
            'ai_biography.home.base_url' => '',
            'ai_biography.home.model' => '',
        ]);

        $client = new VisionLlmClient();
        expect(fn () => $client->chat([
            'messages'    => [['role' => 'user', 'content' => 'test']],
            'max_tokens'  => 100,
            'temperature' => 0.5,
            'stream'      => false,
        ]))->toThrow(VisionLlmException::class);
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// Public API endpoint
// ─────────────────────────────────────────────────────────────────────────────

describe('Public AI biography endpoint', function () {
    it('returns null data when no visible biography exists', function () {
        $user = User::factory()->create([
            'username'  => 'nobiouser',
            'is_active' => true,
        ]);

        $response = $this->getJson('/api/profile/nobiouser/ai-biography');

        $response->assertOk();
        $response->assertJsonPath('data', null);
    });

    it('returns biography data when biography is visible', function () {
        $user = User::factory()->create([
            'username'  => 'biovisibleuser',
            'is_active' => true,
        ]);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'A publicly visible biography about this creator with enough text here.',
            'source_hash'    => 'def456',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $response = $this->getJson('/api/profile/biovisibleuser/ai-biography');

        $response->assertOk();
        $response->assertJsonPath('data.is_visible', true);
        $response->assertJsonStructure(['data' => ['text', 'is_visible', 'is_user_edited', 'generated_at']]);
    });

    it('returns null data when biography is hidden', function () {
        $user = User::factory()->create([
            'username'  => 'biohiddenuser',
            'is_active' => true,
        ]);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'This biography is hidden from public view by the creator.',
            'source_hash'    => 'ghi789',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => true,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $response = $this->getJson('/api/profile/biohiddenuser/ai-biography');

        $response->assertOk();
        $response->assertJsonPath('data', null);
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// Creator management endpoints
// ─────────────────────────────────────────────────────────────────────────────

describe('Creator AI biography management endpoints', function () {
    it('generate endpoint requires authentication', function () {
        $this->postJson('/api/creator/profile/ai-biography/generate')
            ->assertStatus(401);
    });

    it('generate endpoint queues a job for authenticated user', function () {
        Queue::fake();

        $user = User::factory()->create(['is_active' => true]);

        $this->actingAs($user)
            ->postJson('/api/creator/profile/ai-biography/generate')
            ->assertStatus(202);

        Queue::assertPushed(\App\Jobs\GenerateAiBiographyJob::class, fn ($job) => $job->userId === (int) $user->id);
    });

    it('update endpoint saves user-edited biography', function () {
        $user = User::factory()->create(['is_active' => true]);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Original generated text here about the creator.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $this->actingAs($user)
            ->patchJson('/api/creator/profile/ai-biography', [
                'text' => 'This is my personal custom biography that I have written myself.',
            ])
            ->assertOk();

        $record = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($record->is_user_edited)->toBeTrue();
    });

    it('hide endpoint hides biography', function () {
        $user = User::factory()->create(['is_active' => true]);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Biography to be hidden by the creator for privacy reasons.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $this->actingAs($user)
            ->postJson('/api/creator/profile/ai-biography/hide')
            ->assertOk();

        $record = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($record->is_hidden)->toBeTrue();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — AiBiographyInputBuilder quality tiers and threshold
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyInputBuilder v1.1 — quality tiers', function () {
    it('classifies a sparse profile as sparse', function () {
        $user    = User::factory()->create(['created_at' => now()->subYear()]);
        $builder = new AiBiographyInputBuilder();
        $input   = $builder->build($user);

        expect($builder->qualityTier($input))->toBe('sparse');
    });

    it('classifies a creator with many uploads and featured works as rich', function () {
        $user    = User::factory()->create(['created_at' => now()->subYears(5)]);
        $builder = new AiBiographyInputBuilder();

        // Inject rich input manually to avoid needing 20+ DB rows.
        $richInput = [
            'uploads_count'     => 50,
            'featured_count'    => 3,
            'years_on_skinbase' => 5,
            'milestones'        => ['has_comeback' => true, 'best_upload_streak_months' => 6],
            'eras'              => [['title' => 'Early'], ['title' => 'Recent']],
            'evolution_count'   => 5,
        ];

        expect($builder->qualityTier($richInput))->toBe('rich');
    });

    it('classifies a medium-signal creator as medium', function () {
        $builder   = new AiBiographyInputBuilder();
        $input = [
            'uploads_count'     => 10,
            'featured_count'    => 0,
            'years_on_skinbase' => 2,
            'milestones'        => ['has_comeback' => false, 'best_upload_streak_months' => 0],
            'eras'              => [],
            'evolution_count'   => 0,
        ];

        expect($builder->qualityTier($input))->toBe('medium');
    });

    it('returns false for minimum threshold on new account with no uploads', function () {
        $user    = User::factory()->create(['created_at' => now()->subMonths(1)]);
        $builder = new AiBiographyInputBuilder();
        $input   = $builder->build($user);

        expect($builder->meetsMinimumThreshold($input))->toBeFalse();
    });

    it('returns true for minimum threshold when creator has enough uploads', function () {
        $user = User::factory()->create(['created_at' => now()->subYear()]);

        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create([
                'is_public'    => true,
                'is_approved'  => true,
                'published_at' => now()->subMonths($i + 1),
                'deleted_at'   => null,
            ]);
        }

        $builder = new AiBiographyInputBuilder();
        $input   = $builder->build($user);

        expect($builder->meetsMinimumThreshold($input))->toBeTrue();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — AiBiographyValidator hardening
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyValidator v1.1 — extended rules', function () {
    $validator = fn () => new AiBiographyValidator();

    it('rejects "renowned for" as forbidden hype', function () use ($validator) {
        $text = 'This creator is renowned for producing some of the finest digital art on the platform since joining.';
        $errors = $validator()->validate($text);
        expect($errors)->not()->toBeEmpty();
    });

    it('rejects "celebrated by" as forbidden hype', function () use ($validator) {
        $text = 'Celebrated by the community, this creator has produced consistent digital work across many categories.';
        $errors = $validator()->validate($text);
        expect($errors)->not()->toBeEmpty();
    });

    it('rejects output that repeats "creator journey" twice', function () use ($validator) {
        $text = 'The creator journey of this artist spans many years. The creator journey shows both evolution and comeback milestones that define their long platform history.';
        $errors = $validator()->validate($text);
        expect($errors)->not()->toBeEmpty();
    });

    it('rejects sparse-profile bio that references too many rich signals', function () use ($validator) {
        $text = 'This creator has featured artworks, a notable comeback, a strong upload streak, and several remastered evolution works with high downloads and a most productive year.';
        $errors = $validator()->validate($text, 'sparse');
        expect($errors)->not()->toBeEmpty();
    });

    it('accepts a valid sparse-profile bio with modest claims', function () use ($validator) {
        $text = 'A Skinbase creator building an early digital portfolio of public artworks and uploads. Their work spans a growing set of categories on the platform, with new pieces added on a regular basis.';
        expect($validator()->isValid($text, 'sparse'))->toBeTrue();
    });

    it('accepts a rich bio with rich tier tier context', function () use ($validator) {
        $text = 'Active since 2005, this creator has built a portfolio featuring over thirty public uploads across wallpapers and digital art, with several featured selections and a notable return to publishing after a long break.';
        expect($validator()->isValid($text, 'rich'))->toBeTrue();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — AiBiographyPromptBuilder versioning
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyPromptBuilder v1.1 — versioning', function () {
    it('returns v1.1 as prompt_version', function () {
        $builder = new AiBiographyPromptBuilder();
        $payload = $builder->build(['username' => 'test', 'uploads_count' => 10, 'member_since_year' => 2010, 'years_on_skinbase' => 15]);

        expect($payload['prompt_version'])->toBe('v1.1');
    });

    it('returns lower max_tokens and temperature in strict mode', function () {
        $builder = new AiBiographyPromptBuilder();
        $normal  = $builder->build(['username' => 'test']);
        $strict  = $builder->build(['username' => 'test'], strict: true);

        expect($strict['max_tokens'])->toBeLessThan($normal['max_tokens']);
        expect($strict['temperature'])->toBeLessThan($normal['temperature']);
    });

    it('returns even lower max_tokens in sparse mode', function () {
        $builder = new AiBiographyPromptBuilder();
        $strict  = $builder->build(['username' => 'test'], strict: true);
        $sparse  = $builder->build(['username' => 'test'], sparse: true);

        expect($sparse['max_tokens'])->toBeLessThanOrEqual($strict['max_tokens']);
    });

    it('uses a stricter sparse prompt on retry and enforces the minimum word floor', function () {
        $builder = new AiBiographyPromptBuilder();
        $sparse = $builder->build([
            'username' => 'test',
            'member_since_year' => 2007,
            'years_on_skinbase' => 19,
            'uploads_count' => 1,
            'top_categories' => ['GTK+'],
        ], sparse: true);
        $sparseStrict = $builder->build([
            'username' => 'test',
            'member_since_year' => 2007,
            'years_on_skinbase' => 19,
            'uploads_count' => 1,
            'top_categories' => ['GTK+'],
        ], strict: true, sparse: true);

        expect($sparseStrict['max_tokens'])->toBeLessThan($sparse['max_tokens']);
        expect($sparseStrict['temperature'])->toBeLessThan($sparse['temperature']);
        expect($sparse['messages'][0]['content'])->toContain('Minimum 30 words.');
        expect($sparseStrict['messages'][0]['content'])->toContain('Prefer two short factual sentences.');
        expect($sparseStrict['messages'][1]['content'])->toContain('Write at least 30 words.');
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — AiBiographyGenerator retry logic
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyGenerator v1.1 — retry', function () {
    it('retries exactly once when first attempt fails validation', function () {
        config([
            'ai_biography.provider' => 'vision_gateway',
            'vision.gateway.base_url' => 'http://vision.test',
            'vision.gateway.api_key' => 'test-key',
            'ai_biography.llm_endpoint' => '/ai/chat',
        ]);

        $callCount = 0;
        $validBio   = 'Active on Skinbase for many years, this creator has built a portfolio spanning wallpapers and digital art, with several featured selections and a consistent upload history reflecting dedication to the platform and craft.';

        Http::fake(function ($request) use (&$callCount, $validBio) {
            $callCount++;
            if ($callCount === 1) {
                // First call: return something too short that will fail validation.
                return Http::response(['choices' => [['message' => ['content' => 'Too short.']]]], 200);
            }
            // Second call (retry): return a valid biography.
            return Http::response(['choices' => [['message' => ['content' => $validBio]]]], 200);
        });

        $generator = new AiBiographyGenerator(
            new AiBiographyPromptBuilder(),
            new VisionLlmClient(),
            new AiBiographyValidator(),
        );

        $result = $generator->generate(['username' => 'test', 'user_id' => 99, 'uploads_count' => 20, 'member_since_year' => 2004, 'years_on_skinbase' => 21], 'rich');

        expect($result['success'])->toBeTrue();
        expect($result['was_retried'])->toBeTrue();
        expect($callCount)->toBe(2);
    });

    it('returns failure after both attempts fail', function () {
        config([
            'ai_biography.provider' => 'vision_gateway',
            'vision.gateway.base_url' => 'http://vision.test',
            'vision.gateway.api_key' => 'test-key',
            'ai_biography.llm_endpoint' => '/ai/chat',
        ]);

        // Both calls return text that is too short to pass validation.
        Http::fake([
            'vision.test/ai/chat' => Http::response(['choices' => [['message' => ['content' => 'Short.']]]], 200),
        ]);

        $generator = new AiBiographyGenerator(
            new AiBiographyPromptBuilder(),
            new VisionLlmClient(),
            new AiBiographyValidator(),
        );

        $result = $generator->generate(['username' => 'test', 'user_id' => 99], 'rich');

        expect($result['success'])->toBeFalse();
        expect($result['was_retried'])->toBeTrue();
    });

    it('returns prompt_version in result', function () {
        config([
            'ai_biography.provider' => 'vision_gateway',
            'vision.gateway.base_url' => 'http://vision.test',
            'vision.gateway.api_key' => 'test-key',
            'ai_biography.llm_endpoint' => '/ai/chat',
        ]);

        Http::fake([
            'vision.test/ai/chat' => Http::response(['choices' => [['message' => ['content' => 'Active on Skinbase since 2010, this creator has developed a consistent portfolio across digital art categories with several featured artworks and a strong upload history.']]]], 200),
        ]);

        $generator = new AiBiographyGenerator(
            new AiBiographyPromptBuilder(),
            new VisionLlmClient(),
            new AiBiographyValidator(),
        );

        $result = $generator->generate(['username' => 'test', 'user_id' => 99, 'uploads_count' => 15, 'member_since_year' => 2010, 'years_on_skinbase' => 15]);

        expect($result['prompt_version'])->toBe('v1.1');
    });

    it('retries sparse biographies with a stricter sparse prompt after a too-short first attempt', function () {
        config([
            'ai_biography.provider' => 'vision_gateway',
            'vision.gateway.base_url' => 'http://vision.test',
            'vision.gateway.api_key' => 'test-key',
            'ai_biography.llm_endpoint' => '/ai/chat',
        ]);

        $requests = [];
        $firstAttempt = 'szerencsefia has been a member of Skinbase since 2007. They have uploaded one public artwork categorized under GTK+.';
        $retryAttempt = 'szerencsefia has been part of Skinbase since 2007. They have one public artwork on the platform, and that published work is categorized under GTK+, giving a modest but concrete snapshot of their public activity.';

        Http::fake(function ($request) use (&$requests, $firstAttempt, $retryAttempt) {
            $requests[] = $request->data();

            if (count($requests) === 1) {
                return Http::response(['choices' => [['message' => ['content' => $firstAttempt]]]], 200);
            }

            return Http::response(['choices' => [['message' => ['content' => $retryAttempt]]]], 200);
        });

        $generator = new AiBiographyGenerator(
            new AiBiographyPromptBuilder(),
            new VisionLlmClient(),
            new AiBiographyValidator(),
        );

        $result = $generator->generate([
            'username' => 'szerencsefia',
            'user_id' => 99,
            'member_since_year' => 2007,
            'years_on_skinbase' => 19,
            'uploads_count' => 1,
            'top_categories' => ['GTK+'],
        ], 'sparse');

        expect($result['success'])->toBeTrue();
        expect($result['was_retried'])->toBeTrue();
        expect($requests)->toHaveCount(2);
        expect($requests[0]['messages'][0]['content'])->not->toBe($requests[1]['messages'][0]['content']);
        expect($requests[1]['messages'][0]['content'])->toContain('Prefer two short factual sentences.');
    });

    it('returns configured Gemini model in result metadata', function () {
        config([
            'ai_biography.provider' => 'gemini',
            'ai_biography.gemini.base_url' => 'https://generativelanguage.googleapis.com',
            'ai_biography.gemini.api_key' => 'gemini-test-key',
            'ai_biography.gemini.model' => 'gemini-flash-latest',
        ]);

        Http::fake([
            'generativelanguage.googleapis.com/*' => Http::response([
                'candidates' => [[
                    'content' => [
                        'parts' => [
                            ['text' => 'Active on Skinbase for many years, this creator has built a portfolio spanning wallpapers and digital art, with several featured selections and a consistent upload history reflecting dedication to the platform and craft.'],
                        ],
                    ],
                ]],
            ], 200),
        ]);

        $generator = new AiBiographyGenerator(
            new AiBiographyPromptBuilder(),
            new VisionLlmClient(),
            new AiBiographyValidator(),
        );

        $result = $generator->generate(['username' => 'test', 'user_id' => 99, 'uploads_count' => 15, 'member_since_year' => 2010, 'years_on_skinbase' => 15]);

        expect($result['success'])->toBeTrue();
        expect($result['model'])->toBe('gemini-flash-latest');
    });

    it('returns configured Home model in result metadata', function () {
        config([
            'ai_biography.provider' => 'home',
            'ai_biography.home.base_url' => 'http://home.klevze.si:8200',
            'ai_biography.home.endpoint' => '/v1/chat/completions',
            'ai_biography.home.model' => 'google/gemma-3-4b',
        ]);

        Http::fake([
            'http://home.klevze.si:8200/*' => Http::response([
                'choices' => [[
                    'message' => [
                        'content' => 'Active on Skinbase for many years, this creator has built a portfolio spanning wallpapers and digital art, with several featured selections and a consistent upload history reflecting dedication to the platform and craft.',
                    ],
                ]],
            ], 200),
        ]);

        $generator = new AiBiographyGenerator(
            new AiBiographyPromptBuilder(),
            new VisionLlmClient(),
            new AiBiographyValidator(),
        );

        $result = $generator->generate(['username' => 'test', 'user_id' => 99, 'uploads_count' => 15, 'member_since_year' => 2010, 'years_on_skinbase' => 15]);

        expect($result['success'])->toBeTrue();
        expect($result['model'])->toBe('google/gemma-3-4b');
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — AiBiographyService sparse suppression and metadata
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyService v1.1 — sparse suppression and metadata', function () {
    it('suppresses generation for sparse profile below threshold', function () {
        // User with no artworks at all — well below the minimum threshold.
        $user = User::factory()->create(['created_at' => now()->subMonths(2)]);

        $mockGenerator = Mockery::mock(AiBiographyGenerator::class);
        $mockGenerator->shouldNotReceive('generate'); // must NOT be called

        $service = new AiBiographyService(new AiBiographyInputBuilder(), $mockGenerator);
        $result  = $service->generate($user);

        expect($result['success'])->toBeFalse();
        expect($result['action'])->toBe('suppressed_low_signal');
        expect(CreatorAiBiography::where('user_id', $user->id)->exists())->toBeFalse();
    });

    it('stores prompt_version and input_quality_tier on successful generation', function () {
        $user = User::factory()->create(['created_at' => now()->subYears(3)]);

        // Give the user enough artworks to meet the threshold.
        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create([
                'is_public'    => true,
                'is_approved'  => true,
                'published_at' => now()->subMonths($i + 1),
                'deleted_at'   => null,
            ]);
        }

        $mockGenerator = Mockery::mock(AiBiographyGenerator::class);
        $mockGenerator->shouldReceive('generate')->andReturn([
            'success'        => true,
            'text'           => 'Valid biography text for this creator with enough words to be accepted.',
            'errors'         => [],
            'model'          => 'test-model',
            'prompt_version' => 'v1.1',
            'was_retried'    => false,
        ]);

        $service = new AiBiographyService(new AiBiographyInputBuilder(), $mockGenerator);
        $result  = $service->generate($user, 'admin_batch');

        expect($result['success'])->toBeTrue();

        $record = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($record)->not()->toBeNull();
        expect($record->prompt_version)->toBe('v1.1');
        expect($record->generation_reason)->toBe('admin_batch');
        expect($record->input_quality_tier)->toBeString();
    });

    it('flags needs_review on existing user-edited bio when stale draft is stored', function () {
        $user = User::factory()->create(['created_at' => now()->subYears(3)]);

        // Give the user enough artworks to meet the threshold.
        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create([
                'is_public'    => true,
                'is_approved'  => true,
                'published_at' => now()->subMonths($i + 1),
                'deleted_at'   => null,
            ]);
        }

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'My hand-written biography that I prefer to keep.',
            'source_hash'    => 'oldhash',
            'model'          => null,
            'status'         => CreatorAiBiography::STATUS_EDITED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => true,
            'generated_at'   => now()->subDays(30),
            'approved_at'    => now()->subDays(30),
        ]);

        $mockGenerator = Mockery::mock(AiBiographyGenerator::class);
        $mockGenerator->shouldReceive('generate')->andReturn([
            'success'        => true,
            'text'           => 'Refreshed AI biography text for this active creator with notable works.',
            'errors'         => [],
            'model'          => 'test-model',
            'prompt_version' => 'v1.1',
            'was_retried'    => false,
        ]);

        $service = new AiBiographyService(new AiBiographyInputBuilder(), $mockGenerator);
        $result  = $service->generate($user);

        expect($result['action'])->toBe('draft_stored');

        // Original active user-edited record should be flagged needs_review.
        $active = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($active->is_user_edited)->toBeTrue();
        expect($active->needs_review)->toBeTrue();

        // A non-active draft should now exist.
        $draft = CreatorAiBiography::where('user_id', $user->id)->where('is_active', false)->where('is_user_edited', false)->first();
        expect($draft)->not()->toBeNull();
        expect($draft->prompt_version)->toBe('v1.1');
    });

    it('creatorStatusPayload returns needs_review and prompt_version fields', function () {
        $user = User::factory()->create(['created_at' => '2015-01-01']);

        CreatorAiBiography::create([
            'user_id'            => $user->id,
            'text'               => 'A stored biography about this creator.',
            'source_hash'        => 'abc',
            'model'              => 'test',
            'prompt_version'     => 'v1.1',
            'input_quality_tier' => 'medium',
            'generation_reason'  => 'initial_generate',
            'status'             => CreatorAiBiography::STATUS_GENERATED,
            'is_active'          => true,
            'is_hidden'          => false,
            'is_user_edited'     => false,
            'needs_review'       => false,
            'generated_at'       => now(),
            'approved_at'        => now(),
        ]);

        $mockGenerator = Mockery::mock(AiBiographyGenerator::class);
        $service = new AiBiographyService(new AiBiographyInputBuilder(), $mockGenerator);
        $payload = $service->creatorStatusPayload($user);

        expect($payload['has_biography'])->toBeTrue();
        expect($payload['prompt_version'])->toBe('v1.1');
        expect($payload['input_quality_tier'])->toBe('medium');
        expect($payload['generation_reason'])->toBe('initial_generate');
        expect($payload['needs_review'])->toBeFalse();
    });

    it('hidden biography remains hidden after stale refresh with draft_stored action', function () {
        $user = User::factory()->create(['created_at' => now()->subYears(3)]);

        for ($i = 0; $i < 5; $i++) {
            Artwork::factory()->for($user)->create([
                'is_public'    => true,
                'is_approved'  => true,
                'published_at' => now()->subMonths($i + 1),
                'deleted_at'   => null,
            ]);
        }

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'My edited hidden biography text for privacy reasons.',
            'source_hash'    => 'oldhash',
            'model'          => null,
            'status'         => CreatorAiBiography::STATUS_EDITED,
            'is_active'      => true,
            'is_hidden'      => true,   // ← hidden
            'is_user_edited' => true,
            'generated_at'   => now()->subDays(30),
            'approved_at'    => now()->subDays(30),
        ]);

        $mockGenerator = Mockery::mock(AiBiographyGenerator::class);
        $mockGenerator->shouldReceive('generate')->andReturn([
            'success'        => true,
            'text'           => 'New draft text for a very active creator with portfolio history here.',
            'errors'         => [],
            'model'          => 'test-model',
            'prompt_version' => 'v1.1',
            'was_retried'    => false,
        ]);

        $service = new AiBiographyService(new AiBiographyInputBuilder(), $mockGenerator);
        $service->generate($user);

        // The active record must still be hidden — draft storage must not un-hide it.
        $active = CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first();
        expect($active->is_hidden)->toBeTrue();

        // Public payload must still be null.
        expect($service->publicPayload($user))->toBeNull();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — ValidateAiBiographyCommand
// ─────────────────────────────────────────────────────────────────────────────

use App\Console\Commands\ValidateAiBiographyCommand;

describe('ValidateAiBiographyCommand v1.1', function () {
    it('flags needs_review on a stored bio that fails current validator rules', function () {
        $user = User::factory()->create();

        CreatorAiBiography::create([
            'user_id'            => $user->id,
            'text'               => 'A legendary and world-class creator whose iconic work has made them a widely recognized platform icon.',
            'source_hash'        => 'abc',
            'model'              => 'test',
            'status'             => CreatorAiBiography::STATUS_GENERATED,
            'is_active'          => true,
            'is_hidden'          => false,
            'is_user_edited'     => false,
            'needs_review'       => false,
            'input_quality_tier' => 'rich',
            'generated_at'       => now(),
            'approved_at'        => now(),
        ]);

        $this->artisan(ValidateAiBiographyCommand::class, ['user_id' => $user->id])
            ->assertSuccessful();

        expect(
            CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first()->needs_review
        )->toBeTrue();
    });

    it('leaves needs_review false for a bio that passes validation', function () {
        $user = User::factory()->create();

        CreatorAiBiography::create([
            'user_id'            => $user->id,
            'text'               => 'Active on Skinbase for over a decade, this creator has built a portfolio of public artworks spanning wallpapers and digital illustrations, with several pieces selected as featured works across multiple categories.',
            'source_hash'        => 'abc',
            'model'              => 'test',
            'status'             => CreatorAiBiography::STATUS_GENERATED,
            'is_active'          => true,
            'is_hidden'          => false,
            'is_user_edited'     => false,
            'needs_review'       => false,
            'input_quality_tier' => 'rich',
            'generated_at'       => now(),
            'approved_at'        => now(),
        ]);

        $this->artisan(ValidateAiBiographyCommand::class, ['user_id' => $user->id])
            ->assertSuccessful();

        expect(
            CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first()->needs_review
        )->toBeFalse();
    });

    it('dry-run does not update needs_review', function () {
        $user = User::factory()->create();

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'A legendary and world-class iconic creator.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'needs_review'   => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $this->artisan(ValidateAiBiographyCommand::class, ['user_id' => $user->id, '--dry-run' => true])
            ->assertSuccessful();

        expect(
            CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first()->needs_review
        )->toBeFalse();
    });

    it('skips user-edited biographies', function () {
        $user = User::factory()->create();

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'A legendary world-class iconic creator.',
            'source_hash'    => 'abc',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_EDITED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => true,
            'needs_review'   => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $this->artisan(ValidateAiBiographyCommand::class, ['user_id' => $user->id])
            ->assertSuccessful();

        // User-edited bio must never be touched by the validate command.
        expect(
            CreatorAiBiography::where('user_id', $user->id)->where('is_active', true)->first()->needs_review
        )->toBeFalse();
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — source hash ordering stability
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyInputBuilder v1.1 — hash stability', function () {
    it('source hash is stable when downloads_count changes', function () {
        $user    = User::factory()->create(['created_at' => '2015-01-01']);
        $builder = new AiBiographyInputBuilder();

        $input1 = $builder->build($user);
        $input2 = array_merge($input1, ['downloads_count' => ($input1['downloads_count'] ?? 0) + 99999]);

        expect($builder->sourceHash($input1))->toBe($builder->sourceHash($input2));
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// v1.1 — GenerateAiBiographyCommand default missing-only batch
// ─────────────────────────────────────────────────────────────────────────────

describe('GenerateAiBiographyCommand v1.1 — missing batch', function () {
    it('outputs the built prompt for a single user when --prompt is used', function () {
        config(['ai_biography.provider' => 'vision_gateway']);

        $user = User::factory()->create(['username' => 'prompt_creator', 'created_at' => '2019-01-01']);

        for ($i = 0; $i < 3; $i++) {
            Artwork::factory()->for($user)->create([
                'is_public'    => true,
                'is_approved'  => true,
                'published_at' => now()->subMonths($i + 1),
                'deleted_at'   => null,
            ]);
        }

        $exitCode = Artisan::call('ai-biography:generate', [
            'user_id'    => $user->id,
            '--prompt'   => true,
            '--dry-run'  => true,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Prompt preview');
        expect($output)->toContain('Provider       : vision_gateway');
        expect($output)->toContain('System prompt:');
        expect($output)->toContain('You are a concise writing assistant for Skinbase');
        expect($output)->toContain('User prompt:');
        expect($output)->toContain('Write a creator biography in 70 to 130 words');
    });

    it('prints the generated biography text when --result is used', function () {
        $user = User::factory()->create(['username' => 'result_creator', 'created_at' => '2019-01-01']);

        for ($i = 0; $i < 3; $i++) {
            Artwork::factory()->for($user)->create([
                'is_public'    => true,
                'is_approved'  => true,
                'published_at' => now()->subMonths($i + 1),
                'deleted_at'   => null,
            ]);
        }

        config([
            'ai_biography.provider' => 'home',
            'ai_biography.home.base_url' => 'http://home.test',
            'ai_biography.home.endpoint' => '/v1/chat/completions',
            'ai_biography.home.model' => 'qwen/qwen3.5-9b',
            'ai_biography.home.api_key' => '',
        ]);

        $validBio = 'Active on Skinbase for many years, this creator has built a portfolio spanning wallpapers and digital art, with several featured selections and a consistent upload history reflecting dedication to the platform and craft.';

        Http::fake([
            'http://home.test/v1/chat/completions' => Http::response([
                'choices' => [
                    [
                        'message' => [
                            'role' => 'assistant',
                            'content' => $validBio,
                        ],
                    ],
                ],
            ], 200),
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            'user_id'   => $user->id,
            '--result'  => true,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Generated biography text:');
        expect($output)->toContain($validBio);
    });

    it('skips single-user generation when --skip-existing is used and an active biography already exists', function () {
        $user = User::factory()->create(['username' => 'skip_existing_creator']);

        CreatorAiBiography::create([
            'user_id'        => $user->id,
            'text'           => 'Existing biography text that should remain untouched during a skip-existing run.',
            'source_hash'    => 'skip-existing-hash',
            'model'          => 'test-model',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'needs_review'   => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            'user_id' => $user->id,
            '--skip-existing' => true,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain("Processing user #{$user->id} ({$user->username})");
        expect($output)->toContain('skipped_existing_active');
    });

    it('shows when no prompt will be sent for low-signal profiles', function () {
        $user = User::factory()->create(['username' => 'low_signal_prompt_creator']);

        $exitCode = Artisan::call('ai-biography:generate', [
            'user_id'    => $user->id,
            '--prompt'   => true,
            '--dry-run'  => true,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Prompt preview');
        expect($output)->toContain('Meets threshold: no');
        expect($output)->toContain('No prompt will be sent because this profile is below the minimum generation threshold.');
    });

    it('accepts --provider=gemini in dry-run batch mode', function () {
        $user = User::factory()->create(['username' => 'gemini_batch_creator']);

        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-08-10 12:00:00',
            'deleted_at'   => null,
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            '--provider' => 'gemini',
            '--dry-run'  => true,
            '--limit'    => 1,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Using AI biography provider override: gemini');
        expect($output)->toContain("[{$user->id}] {$user->username}");
    });

    it('accepts --provider=home in dry-run batch mode', function () {
        $user = User::factory()->create(['username' => 'home_batch_creator']);

        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-08-11 12:00:00',
            'deleted_at'   => null,
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            '--provider' => 'home',
            '--dry-run'  => true,
            '--limit'    => 1,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Using AI biography provider override: home');
        expect($output)->toContain("[{$user->id}] {$user->username}");
    });

    it('passes provider override into queued jobs', function () {
        Queue::fake();

        $user = User::factory()->create(['username' => 'queued_gemini_creator']);

        $exitCode = Artisan::call('ai-biography:generate', [
            'user_id'     => $user->id,
            '--provider'  => 'gemini',
            '--queue'     => true,
        ]);

        expect($exitCode)->toBe(0);

        Queue::assertPushed(
            \App\Jobs\GenerateAiBiographyJob::class,
            fn ($job) => $job->userId === (int) $user->id
                && $job->provider === 'gemini'
        );
    });

    it('rejects unsupported provider values', function () {
        $exitCode = Artisan::call('ai-biography:generate', [
            '--provider' => 'openai',
            '--dry-run'  => true,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(1);
        expect($output)->toContain('Invalid provider [openai]. Supported values: vision_gateway, vision, gemini, home.');
    });

    it('defaults to missing biographies ordered by latest public upload and skips existing biographies', function () {
        $older = User::factory()->create(['username' => 'older_creator']);
        $latest = User::factory()->create(['username' => 'latest_creator']);
        $existing = User::factory()->create(['username' => 'existing_creator']);

        Artwork::factory()->for($older)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-03-10 12:00:00',
            'deleted_at'   => null,
        ]);

        Artwork::factory()->for($latest)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-06-15 12:00:00',
            'deleted_at'   => null,
        ]);

        Artwork::factory()->for($existing)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-07-20 12:00:00',
            'deleted_at'   => null,
        ]);

        CreatorAiBiography::create([
            'user_id'        => $existing->id,
            'text'           => 'Existing biography text that should cause this creator to be skipped in missing-only batch mode.',
            'source_hash'    => 'existing-hash',
            'model'          => 'test',
            'status'         => CreatorAiBiography::STATUS_GENERATED,
            'is_active'      => true,
            'is_hidden'      => false,
            'is_user_edited' => false,
            'needs_review'   => false,
            'generated_at'   => now(),
            'approved_at'    => now(),
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            '--dry-run' => true,
            '--limit'   => 10,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Generating missing AI biographies ordered by latest public upload');
        expect($output)->toContain("[{$latest->id}] {$latest->username}");
        expect($output)->toContain("[{$older->id}] {$older->username}");
        expect($output)->not->toContain($existing->username);
        expect(strpos($output, "[{$latest->id}] {$latest->username}"))
            ->toBeLessThan(strpos($output, "[{$older->id}] {$older->username}"));
    });

    it('treats --all as an alias for the default missing-only batch mode', function () {
        $user = User::factory()->create(['username' => 'alias_creator']);

        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-05-01 12:00:00',
            'deleted_at'   => null,
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            '--all'     => true,
            '--dry-run' => true,
            '--limit'   => 1,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('`--all` is now an alias for the default missing-only batch mode.');
        expect($output)->toContain("[{$user->id}] {$user->username}");
    });

    it('normalizes --provider=vision to vision_gateway', function () {
        $user = User::factory()->create(['username' => 'vision_alias_creator']);

        Artwork::factory()->for($user)->create([
            'is_public'    => true,
            'is_approved'  => true,
            'published_at' => '2024-09-01 12:00:00',
            'deleted_at'   => null,
        ]);

        $exitCode = Artisan::call('ai-biography:generate', [
            '--provider' => 'vision',
            '--dry-run'  => true,
            '--limit'    => 1,
        ]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('Using AI biography provider override: vision_gateway');
        expect($output)->toContain("[{$user->id}] {$user->username}");
    });
});

// ─────────────────────────────────────────────────────────────────────────────
// AiBiographyProvidersCommand
// ─────────────────────────────────────────────────────────────────────────────

describe('AiBiographyProvidersCommand', function () {
    it('reports provider health and lists models', function () {
        config([
            'vision.gateway.base_url' => 'http://vision.test',
            'vision.gateway.api_key' => 'vision-key',
            'ai_biography.llm_model' => 'vision-gateway',
            'ai_biography.gemini.base_url' => 'https://generativelanguage.googleapis.com',
            'ai_biography.gemini.api_key' => 'gemini-key',
            'ai_biography.gemini.model' => 'gemini-flash-latest',
            'ai_biography.home.base_url' => 'http://home.klevze.si:8200',
            'ai_biography.home.model' => 'qwen/qwen3.5-9b',
        ]);

        Http::fake([
            'http://vision.test/v1/models' => Http::response([
                'data' => [
                    ['id' => 'vision-gateway'],
                    ['id' => 'vision-fallback'],
                ],
            ], 200),
            'https://generativelanguage.googleapis.com/v1beta/models' => Http::response([
                'models' => [
                    ['name' => 'models/gemini-2.0-flash'],
                    ['name' => 'models/gemini-1.5-flash'],
                ],
            ], 200),
            'http://home.klevze.si:8200/v1/models' => Http::response([
                'data' => [
                    ['id' => 'qwen/qwen3.5-9b'],
                    ['id' => 'google/gemma-3-4b'],
                ],
            ], 200),
        ]);

        $exitCode = Artisan::call('ai-biography:providers', ['--limit' => 2]);

        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('vision_gateway');
        expect($output)->toContain('gemini');
        expect($output)->toContain('home');
        expect($output)->toContain('online');
        expect($output)->toContain('vision-gateway');
        expect($output)->toContain('models/gemini-2.0-flash');
        expect($output)->toContain('qwen/qwen3.5-9b');
    });

    it('filters to a single provider alias', function () {
        config([
            'ai_biography.home.base_url' => 'http://home.klevze.si:8200',
            'ai_biography.home.model' => 'qwen/qwen3.5-9b',
        ]);

        Http::fake([
            'http://home.klevze.si:8200/v1/models' => Http::response([
                'data' => [
                    ['id' => 'qwen/qwen3.5-9b'],
                ],
            ], 200),
        ]);

        $exitCode = Artisan::call('ai-biography:providers', ['--provider' => 'lmstudio']);
        $output = Artisan::output();

        expect($exitCode)->toBe(0);
        expect($output)->toContain('home');
        expect($output)->toContain('qwen/qwen3.5-9b');
        expect($output)->not->toContain('gemini');
        expect($output)->not->toContain('vision_gateway');
    });

    it('rejects unsupported provider values', function () {
        $exitCode = Artisan::call('ai-biography:providers', ['--provider' => 'claude']);
        $output = Artisan::output();

        expect($exitCode)->toBe(1);
        expect($output)->toContain('Invalid provider [claude]. Supported values: together, vision_gateway, vision, gemini, home.');
    });
});
