<?php

declare(strict_types=1);

namespace Tests\Feature\Admin;

use App\Http\Middleware\ConditionalValidateCsrfToken;
use App\Models\AcademyAiComparisonResult;
use App\Models\AcademyBillingEvent;
use App\Models\AcademyCategory;
use App\Models\AcademyChallenge;
use App\Models\AcademyChallengeSubmission;
use App\Models\AcademyCourse;
use App\Models\AcademyCourseLesson;
use App\Models\AcademyLesson;
use App\Models\AcademyLessonBlock;
use App\Models\AcademyPromptTemplate;
use App\Models\Artwork;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;

final class AcademyAdminTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        $this->withoutMiddleware(ConditionalValidateCsrfToken::class);
    }

    public function test_admin_can_open_academy_dashboard(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $this->actingAs($admin)
            ->get('/moderation/academy/dashboard')
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/Dashboard')
                ->where('stats.courses', 0)
                ->where('stats.lessons', 0)
                ->where('stats.prompts', 0));
    }

    public function test_admin_can_open_academy_billing_overview_with_live_stats(): void
    {
        config()->set('academy_billing.enabled', true);
        config()->set('academy_billing.subscription_name', 'academy');
        config()->set('academy_billing.plans', [
            'creator_monthly' => [
                'label' => 'Creator Monthly',
                'tier' => 'creator',
                'interval' => 'monthly',
                'stripe_price_id' => 'price_creator_monthly_test',
            ],
            'pro_monthly' => [
                'label' => 'Pro Monthly',
                'tier' => 'pro',
                'interval' => 'monthly',
                'stripe_price_id' => 'price_pro_monthly_test',
            ],
        ]);

        $admin = User::factory()->create(['role' => 'admin']);
        $creatorUser = User::factory()->create();
        $graceUser = User::factory()->create();
        $proUser = User::factory()->create();

        $this->seedAcademySubscription($creatorUser, 'price_creator_monthly_test');
        $this->seedAcademySubscription($graceUser, 'price_creator_monthly_test', 'canceled', now()->addDays(5));
        $this->seedAcademySubscription($proUser, 'price_pro_monthly_test');

        AcademyBillingEvent::query()->create([
            'user_id' => $graceUser->id,
            'stripe_event_id' => 'evt_academy_billing_test_1',
            'stripe_customer_id' => 'cus_academy_test_1',
            'stripe_subscription_id' => 'sub_academy_test_1',
            'event_type' => 'customer.subscription.updated',
            'academy_tier' => 'creator',
            'academy_plan' => 'creator_monthly',
            'payload_summary' => ['status' => 'canceled', 'source' => 'test'],
            'processed_at' => now()->subMinute(),
        ]);

        $this->actingAs($admin)
            ->get('/moderation/academy/dashboard')
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/Dashboard')
                ->where('stats.active_subscribers', 3)
                ->where('stats.creator_subscribers', 2)
                ->where('stats.pro_subscribers', 1)
                ->where('stats.grace_period_subscribers', 1)
                ->where('links.billing', route('admin.academy.billing')));

        $this->actingAs($admin)
            ->get(route('admin.academy.billing'))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/Billing')
                ->where('summary.enabled', true)
                ->where('summary.active_subscribers', 3)
                ->where('summary.creator_subscribers', 2)
                ->where('summary.pro_subscribers', 1)
                ->where('summary.grace_period_subscribers', 1)
                ->where('summary.missing_plan_keys', [])
                ->has('planBreakdown', 2)
                ->where('planBreakdown.0.key', 'creator_monthly')
                ->where('planBreakdown.0.subscribers', 2)
                ->where('planBreakdown.1.key', 'pro_monthly')
                ->where('planBreakdown.1.subscribers', 1)
                ->has('recentEvents', 1)
                ->where('recentEvents.0.event_type', 'customer.subscription.updated')
                ->where('recentEvents.0.user_id', $graceUser->id));
    }

    public function test_admin_can_approve_and_reject_challenge_submission(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $user = User::factory()->create();
        $artwork = Artwork::factory()->create(['user_id' => $user->id]);
        $challenge = AcademyChallenge::query()->create([
            'title' => 'Moderated Challenge',
            'slug' => 'moderated-challenge',
            'access_level' => 'free',
            'status' => 'active',
            'active' => true,
        ]);

        $submission = AcademyChallengeSubmission::query()->create([
            'challenge_id' => $challenge->id,
            'user_id' => $user->id,
            'artwork_id' => $artwork->id,
            'moderation_status' => 'pending',
            'submitted_at' => now(),
        ]);

        $this->from('/moderation/academy/submissions')
            ->actingAs($admin)
            ->post(route('admin.academy.submissions.approve', ['academyChallengeSubmission' => $submission]))
            ->assertRedirect('/moderation/academy/submissions');

        $this->assertDatabaseHas('academy_challenge_submissions', [
            'id' => $submission->id,
            'moderation_status' => 'approved',
        ]);

        $this->from('/moderation/academy/submissions')
            ->actingAs($admin)
            ->post(route('admin.academy.submissions.reject', ['academyChallengeSubmission' => $submission]))
            ->assertRedirect('/moderation/academy/submissions');

        $this->assertDatabaseHas('academy_challenge_submissions', [
            'id' => $submission->id,
            'moderation_status' => 'rejected',
        ]);
    }

    public function test_admin_can_open_all_academy_modules(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        foreach ([
            '/moderation/academy/dashboard',
            '/moderation/academy/billing',
            '/moderation/academy/courses',
            '/moderation/academy/categories',
            '/moderation/academy/lessons',
            '/moderation/academy/prompts',
            '/moderation/academy/packs',
            '/moderation/academy/challenges',
            '/moderation/academy/submissions',
            '/moderation/academy/badges',
            '/moderation/academy/analytics',
            '/moderation/academy/analytics/intelligence',
            '/moderation/academy/analytics/content',
            '/moderation/academy/analytics/prompts',
            '/moderation/academy/analytics/lessons',
            '/moderation/academy/analytics/courses',
            '/moderation/academy/analytics/search',
            '/moderation/academy/analytics/funnel',
        ] as $path) {
            $this->actingAs($admin)->get($path)->assertOk();
        }
    }

    public function test_non_admin_cannot_open_academy_analytics_pages(): void
    {
        $user = User::factory()->create(['role' => 'user']);

        foreach ([
            '/moderation/academy/analytics',
            '/moderation/academy/analytics/intelligence',
            '/moderation/academy/analytics/content',
            '/moderation/academy/analytics/prompts',
            '/moderation/academy/analytics/lessons',
            '/moderation/academy/analytics/courses',
            '/moderation/academy/analytics/search',
            '/moderation/academy/analytics/funnel',
        ] as $path) {
            $this->actingAs($user)->get($path)->assertStatus(302);
        }
    }

    public function test_admin_can_open_academy_intelligence_dashboard(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $this->actingAs($admin)
            ->get('/moderation/academy/analytics/intelligence')
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/AnalyticsIntelligence')
                ->where('range.active', '30d')
                ->has('contentOpportunities.cards')
                ->has('searchGaps.summary')
                ->has('promptInsights.summary')
                ->has('lessonDropoffs.summary')
                ->has('courseHealth.summary')
                ->has('premiumInterest.summary')
                ->has('editorialRecommendations.summary'));
    }

    public function test_admin_can_open_course_builder(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $course = AcademyCourse::query()->create([
            'title' => 'Builder Course',
            'slug' => 'builder-course',
            'excerpt' => 'Course builder test',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);

        $this->actingAs($admin)
            ->get(route('admin.academy.courses.builder.edit', ['academyCourse' => $course]))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CourseBuilder')
                ->where('course.slug', 'builder-course')
                ->where('routes.reorder', route('admin.academy.courses.reorder', ['academyCourse' => $course])));
    }

    public function test_course_builder_attach_rewrites_lesson_numbers_and_course_order(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $course = AcademyCourse::query()->create([
            'title' => 'Ordering Course',
            'slug' => 'ordering-course',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);

        $firstLesson = AcademyLesson::query()->create([
            'title' => 'First Lesson',
            'slug' => 'first-lesson',
            'lesson_number' => 9,
            'course_order' => 9,
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);
        $secondLesson = AcademyLesson::query()->create([
            'title' => 'Second Lesson',
            'slug' => 'second-lesson',
            'lesson_number' => 8,
            'course_order' => 8,
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);
        $newLesson = AcademyLesson::query()->create([
            'title' => 'New Lesson',
            'slug' => 'new-lesson',
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $firstLesson->id,
            'order_num' => 4,
            'is_required' => true,
        ]);
        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $secondLesson->id,
            'order_num' => 7,
            'is_required' => true,
        ]);

        $this->actingAs($admin)
            ->post(route('admin.academy.courses.lessons.attach', ['academyCourse' => $course]), [
                'lesson_id' => $newLesson->id,
                'order_num' => 12,
                'is_required' => true,
            ])
            ->assertRedirect();

        $orderedCourseLessons = AcademyCourseLesson::query()
            ->where('course_id', $course->id)
            ->orderBy('order_num')
            ->get();

        $this->assertSame([0, 1, 2], $orderedCourseLessons->pluck('order_num')->map(static fn ($value) => (int) $value)->all());
        $this->assertSame([1, 2, 3], AcademyLesson::query()->whereIn('id', [$firstLesson->id, $secondLesson->id, $newLesson->id])->orderBy('course_order')->pluck('course_order')->map(static fn ($value) => (int) $value)->all());

        $this->assertSame(1, $firstLesson->fresh()->lesson_number);
        $this->assertSame(1, $firstLesson->fresh()->course_order);
        $this->assertSame(2, $secondLesson->fresh()->lesson_number);
        $this->assertSame(2, $secondLesson->fresh()->course_order);
        $this->assertSame(3, $newLesson->fresh()->lesson_number);
        $this->assertSame(3, $newLesson->fresh()->course_order);
    }

    public function test_course_builder_reorder_rewrites_lesson_numbers_and_course_order(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $course = AcademyCourse::query()->create([
            'title' => 'Reorder Course',
            'slug' => 'reorder-course',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);

        $firstLesson = AcademyLesson::query()->create([
            'title' => 'Alpha Lesson',
            'slug' => 'alpha-lesson',
            'lesson_number' => 1,
            'course_order' => 1,
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);
        $secondLesson = AcademyLesson::query()->create([
            'title' => 'Beta Lesson',
            'slug' => 'beta-lesson',
            'lesson_number' => 2,
            'course_order' => 2,
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $firstCourseLesson = AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $firstLesson->id,
            'order_num' => 0,
            'is_required' => true,
        ]);
        $secondCourseLesson = AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $secondLesson->id,
            'order_num' => 1,
            'is_required' => true,
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.academy.courses.reorder', ['academyCourse' => $course]), [
                'sections' => [],
                'lessons' => [
                    [
                        'id' => $secondCourseLesson->id,
                        'order_num' => 0,
                        'section_id' => null,
                    ],
                    [
                        'id' => $firstCourseLesson->id,
                        'order_num' => 1,
                        'section_id' => null,
                    ],
                ],
            ])
            ->assertRedirect();

        $this->assertSame(1, $secondLesson->fresh()->lesson_number);
        $this->assertSame(1, $secondLesson->fresh()->course_order);
        $this->assertSame(2, $firstLesson->fresh()->lesson_number);
        $this->assertSame(2, $firstLesson->fresh()->course_order);
        $this->assertSame([0, 1], AcademyCourseLesson::query()->where('course_id', $course->id)->orderBy('order_num')->pluck('order_num')->map(static fn ($value) => (int) $value)->all());
        $this->assertSame([$secondLesson->id, $firstLesson->id], AcademyCourseLesson::query()->where('course_id', $course->id)->orderBy('order_num')->pluck('lesson_id')->map(static fn ($value) => (int) $value)->all());
    }

    public function test_admin_can_open_course_create_form_with_editor_context(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $this->actingAs($admin)
            ->get(route('admin.academy.courses.create'))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CrudForm')
                ->where('resource', 'courses')
                ->where('editorContext.coverUploadUrl', route('api.studio.academy.lessons.media.upload'))
                ->where('editorContext.coverDeleteUrl', route('api.studio.academy.lessons.media.destroy'))
                ->where('editorContext.bodyMediaUploadUrl', route('api.studio.academy.lessons.media.upload'))
                ->where('record.cover_image_url', null)
                ->where('record.teaser_image_url', null));
    }

    public function test_admin_can_store_prompt_with_ai_model_comparisons(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.prompts.store'), [
                'title' => 'Prompt Comparison Template',
                'slug' => 'prompt-comparison-template',
                'excerpt' => 'Compare the same prompt across different AI models.',
                'prompt' => 'Create a cinematic sci-fi skyline with reflective rain-soaked streets.',
                'negative_prompt' => 'blurry, low detail, text, watermark',
                'usage_notes' => 'Use this when you want reflective city lighting.',
                'workflow_notes' => 'Best after a composition sketch pass.',
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'aspect_ratio' => '16:9',
                'tags' => ['cinematic', 'city'],
                'tool_notes' => [
                    [
                        'provider' => 'Midjourney',
                        'model_name' => 'V7',
                        'notes' => 'Produces the strongest mood and lighting with minimal retries.',
                        'image_path' => 'academy/lessons/body/aa/bb/prompt-midjourney.webp',
                        'thumb_path' => 'academy/lessons/body/aa/bb/prompt-midjourney-thumb.webp',
                        'settings' => 'Midjourney V7 on Discord, stylize 200, 16:9 upscale.',
                        'strengths' => 'Atmosphere, composition, reflective light.',
                        'weaknesses' => 'Can over-stylize signage and crowd details.',
                        'best_for' => 'Quick concept frames and wallpaper-ready hero shots.',
                        'score' => 9,
                        'active' => true,
                    ],
                ],
                'preview_image' => '',
                'featured' => false,
                'prompt_of_week' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
            ]);

        $prompt = AcademyPromptTemplate::query()->where('slug', 'prompt-comparison-template')->firstOrFail();

        $response->assertRedirect(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]));
        $this->assertSame('Midjourney', $prompt->tool_notes[0]['provider'] ?? null);
        $this->assertSame('V7', $prompt->tool_notes[0]['model_name'] ?? null);
        $this->assertSame('Quick concept frames and wallpaper-ready hero shots.', $prompt->tool_notes[0]['best_for'] ?? null);
        $this->assertSame('academy/lessons/body/aa/bb/prompt-midjourney.webp', $prompt->tool_notes[0]['image_path'] ?? null);
        $this->assertSame(9, $prompt->tool_notes[0]['score'] ?? null);

        $this->actingAs($admin)
            ->get(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CrudForm')
                ->where('editorContext.comparisonMediaUploadUrl', route('api.studio.academy.lessons.media.upload'))
                ->where('record.tool_notes.0.provider', 'Midjourney')
                ->where('record.tool_notes.0.model_name', 'V7')
                ->where('record.tool_notes.0.image_path', 'academy/lessons/body/aa/bb/prompt-midjourney.webp')
                ->where('record.tool_notes.0.score', 9));
    }

    public function test_prompt_comparison_upload_returns_thumbnail_and_medium_variants(): void
    {
        config()->set('uploads.object_storage.disk', 's3');
        Storage::fake('s3');

        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('api.studio.academy.lessons.media.upload'), [
                'slot' => 'body',
                'image' => UploadedFile::fake()->image('comparison-source.png', 1600, 900),
            ]);

        $response
            ->assertOk()
            ->assertJsonPath('slot', 'body')
            ->assertJsonPath('thumb_width', 480)
            ->assertJsonPath('medium_width', 960);

        $payload = $response->json();

        $this->assertIsString($payload['path'] ?? null);
        $this->assertIsString($payload['thumb_path'] ?? null);
        $this->assertIsString($payload['medium_path'] ?? null);
        $this->assertNotSame($payload['path'], $payload['thumb_path']);
        $this->assertNotSame('', $payload['medium_path']);

        Storage::disk('s3')->assertExists($payload['path']);
        Storage::disk('s3')->assertExists($payload['thumb_path']);
        Storage::disk('s3')->assertExists($payload['medium_path']);
    }

    public function test_prompt_thumbnail_backfill_command_generates_missing_variants(): void
    {
        config()->set('uploads.object_storage.disk', 's3');
        Storage::fake('s3');

        $previewUpload = UploadedFile::fake()->image('prompt-preview.png', 1600, 900);
        $comparisonUpload = UploadedFile::fake()->image('prompt-comparison.png', 1400, 1400);

        Storage::disk('s3')->put(
            'academy-prompts/previews/emoji-sticker-pack.webp',
            file_get_contents($previewUpload->getPathname()) ?: ''
        );

        Storage::disk('s3')->put(
            'academy/lessons/body/aa/bb/emoji-sticker-pack.webp',
            file_get_contents($comparisonUpload->getPathname()) ?: ''
        );

        $prompt = AcademyPromptTemplate::query()->create([
            'title' => 'Emoji Sticker Prompt',
            'slug' => 'emoji-sticker-prompt',
            'excerpt' => 'Prompt waiting for thumbs.',
            'prompt' => 'Create a chibi emoji sticker collection.',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'preview_image' => 'academy-prompts/previews/emoji-sticker-pack.webp',
            'tool_notes' => [[
                'provider' => 'ChatGPT',
                'model_name' => '4o Image',
                'image_path' => 'academy/lessons/body/aa/bb/emoji-sticker-pack.webp',
                'thumb_path' => '',
                'active' => true,
            ]],
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $this->artisan('academy:prompts:generate-missing-thumbnails')
            ->expectsOutputToContain('Prompt thumbnail backfill complete.')
            ->assertSuccessful();

        Storage::disk('s3')->assertExists('academy-prompts/previews/emoji-sticker-pack-thumb.webp');
        Storage::disk('s3')->assertExists('academy-prompts/previews/emoji-sticker-pack-md.webp');
        Storage::disk('s3')->assertExists('academy/lessons/body/aa/bb/emoji-sticker-pack-thumb.webp');
        Storage::disk('s3')->assertExists('academy/lessons/body/aa/bb/emoji-sticker-pack-md.webp');

        $this->assertSame(
            'academy/lessons/body/aa/bb/emoji-sticker-pack-thumb.webp',
            $prompt->fresh()->tool_notes[0]['thumb_path'] ?? null,
        );
    }

    public function test_admin_can_store_prompt_with_advanced_prompt_metadata(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.prompts.store'), [
                'title' => 'City Climate Portrait',
                'slug' => 'city-climate-portrait',
                'excerpt' => 'Advanced prompt with structured documentation.',
                'prompt' => 'Create a climate-driven city portrait.',
                'negative_prompt' => 'blurry, low detail',
                'usage_notes' => 'Use real data before generating.',
                'workflow_notes' => 'Internal editorial workflow note.',
                'documentation' => [
                    'summary' => 'This prompt creates a climate-aware city wallpaper.',
                    'best_for' => ['travel wallpapers', 'editorial posters'],
                    'how_to_use' => ['Choose a city', 'Collect climate data', 'Insert placeholders'],
                    'required_inputs' => ['City name', 'Monthly weather data'],
                    'workflow' => ['Research', 'Prompt prep', 'Generation'],
                    'tips' => ['Keep the climate ribbon subtle'],
                    'common_mistakes' => ['Inventing weather data'],
                    'data_accuracy_notes' => ['Use climate normals where possible'],
                    'display_notes' => 'Use the image-safe variant for most models.',
                ],
                'placeholders' => [
                    [
                        'key' => 'CITY_NAME',
                        'label' => 'City name',
                        'description' => 'The featured city.',
                        'required' => true,
                        'example' => 'Paris',
                        'type' => 'text',
                    ],
                ],
                'helper_prompts' => [
                    [
                        'title' => 'Collect city climate data',
                        'description' => 'Gather facts and monthly weather data.',
                        'prompt' => 'Collect city and climate data for [CITY_NAME].',
                        'expected_output' => 'json',
                    ],
                ],
                'prompt_variants' => [
                    [
                        'title' => 'Image-safe version',
                        'slug' => 'image-safe-version',
                        'description' => 'Reduced text pressure for image models.',
                        'prompt' => 'Create an image-safe city climate portrait.',
                        'negative_prompt' => 'tiny text, clutter',
                        'recommended' => true,
                        'recommended_for' => ['general image generation'],
                        'risk_notes' => ['Climate icons may still be abstract'],
                    ],
                ],
                'difficulty' => 'intermediate',
                'access_level' => 'creator',
                'aspect_ratio' => '16:9',
                'tags' => ['city', 'climate'],
                'preview_image' => '',
                'featured' => false,
                'prompt_of_week' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
            ]);

        $prompt = AcademyPromptTemplate::query()->where('slug', 'city-climate-portrait')->firstOrFail();

        $response->assertRedirect(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]));
        $this->assertSame('This prompt creates a climate-aware city wallpaper.', $prompt->documentation['summary'] ?? null);
        $this->assertSame('CITY_NAME', $prompt->placeholders[0]['key'] ?? null);
        $this->assertSame('other', $prompt->helper_prompts[0]['type'] ?? null);
        $this->assertTrue((bool) ($prompt->helper_prompts[0]['active'] ?? false));
        $this->assertSame('Image-safe version', $prompt->prompt_variants[0]['title'] ?? null);
        $this->assertTrue((bool) ($prompt->prompt_variants[0]['recommended'] ?? false));

        $this->actingAs($admin)
            ->get(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CrudForm')
                ->where('record.documentation', json_encode([
                    'summary' => 'This prompt creates a climate-aware city wallpaper.',
                    'display_notes' => 'Use the image-safe variant for most models.',
                    'best_for' => ['travel wallpapers', 'editorial posters'],
                    'how_to_use' => ['Choose a city', 'Collect climate data', 'Insert placeholders'],
                    'required_inputs' => ['City name', 'Monthly weather data'],
                    'workflow' => ['Research', 'Prompt prep', 'Generation'],
                    'tips' => ['Keep the climate ribbon subtle'],
                    'common_mistakes' => ['Inventing weather data'],
                    'data_accuracy_notes' => ['Use climate normals where possible'],
                ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))
                ->where('record.placeholders', json_encode([
                    [
                        'key' => 'CITY_NAME',
                        'label' => 'City name',
                        'description' => 'The featured city.',
                        'required' => true,
                        'example' => 'Paris',
                        'default' => null,
                        'type' => 'text',
                    ],
                ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))
                ->where('record.helper_prompts', json_encode([
                    [
                        'title' => 'Collect city climate data',
                        'type' => 'other',
                        'description' => 'Gather facts and monthly weather data.',
                        'prompt' => 'Collect city and climate data for [CITY_NAME].',
                        'expected_output' => 'json',
                        'active' => true,
                    ],
                ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))
                ->where('record.prompt_variants', json_encode([
                    [
                        'title' => 'Image-safe version',
                        'slug' => 'image-safe-version',
                        'description' => 'Reduced text pressure for image models.',
                        'prompt' => 'Create an image-safe city climate portrait.',
                        'negative_prompt' => 'tiny text, clutter',
                        'recommended' => true,
                        'recommended_for' => ['general image generation'],
                        'risk_notes' => ['Climate icons may still be abstract'],
                        'active' => true,
                    ],
                ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)));
    }

    public function test_admin_can_store_prompt_when_advanced_json_fields_are_single_objects(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.prompts.store'), [
                'title' => 'Single Object Prompt',
                'slug' => 'single-object-prompt',
                'excerpt' => 'Uses single-object advanced payloads.',
                'prompt' => 'Create a clean travel poster.',
                'negative_prompt' => '',
                'usage_notes' => '',
                'workflow_notes' => '',
                'documentation' => [
                    'summary' => 'Documentation still uses an object.',
                ],
                'placeholders' => [
                    'key' => 'CITY_NAME',
                    'label' => 'City name',
                    'description' => 'Featured city.',
                    'required' => true,
                    'example' => 'Paris',
                    'type' => 'text',
                ],
                'helper_prompts' => [
                    'title' => 'Collect city data',
                    'description' => 'Gather source facts.',
                    'prompt' => 'Collect city data for [CITY_NAME].',
                    'expected_output' => 'json',
                ],
                'prompt_variants' => [
                    'title' => 'Image-safe version',
                    'slug' => 'image-safe-version',
                    'description' => 'Safer for image models.',
                    'prompt' => 'Create an image-safe travel poster.',
                    'recommended' => true,
                ],
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'aspect_ratio' => '16:9',
                'tags' => ['travel'],
                'preview_image' => '',
                'featured' => false,
                'prompt_of_week' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
            ]);

        $prompt = AcademyPromptTemplate::query()->where('slug', 'single-object-prompt')->firstOrFail();

        $response->assertRedirect(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]));
        $this->assertSame('CITY_NAME', $prompt->placeholders[0]['key'] ?? null);
        $this->assertSame('Collect city data', $prompt->helper_prompts[0]['title'] ?? null);
        $this->assertSame('Image-safe version', $prompt->prompt_variants[0]['title'] ?? null);
    }

    public function test_admin_can_store_prompt_placeholder_without_key_or_type(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.prompts.store'), [
                'title' => 'Loose Placeholder Prompt',
                'slug' => 'loose-placeholder-prompt',
                'excerpt' => 'Allows descriptive placeholders without a machine key.',
                'prompt' => 'Create a stylized city scene.',
                'negative_prompt' => '',
                'usage_notes' => '',
                'workflow_notes' => '',
                'documentation' => null,
                'placeholders' => [
                    [
                        'label' => 'City name',
                        'description' => 'The city featured in the artwork.',
                        'example' => 'Paris',
                    ],
                ],
                'helper_prompts' => [],
                'prompt_variants' => [],
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'aspect_ratio' => '16:9',
                'tags' => ['travel'],
                'preview_image' => '',
                'featured' => false,
                'prompt_of_week' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
            ]);

        $prompt = AcademyPromptTemplate::query()->where('slug', 'loose-placeholder-prompt')->firstOrFail();

        $response->assertRedirect(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]));
        $this->assertSame('City name', $prompt->placeholders[0]['label'] ?? null);
        $this->assertSame('The city featured in the artwork.', $prompt->placeholders[0]['description'] ?? null);
        $this->assertNull($prompt->placeholders[0]['key'] ?? null);
        $this->assertNull($prompt->placeholders[0]['type'] ?? null);
    }

    public function test_admin_can_store_prompt_placeholder_with_custom_type(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.prompts.store'), [
                'title' => 'Custom Type Prompt',
                'slug' => 'custom-type-prompt',
                'excerpt' => 'Allows custom placeholder type values.',
                'prompt' => 'Create a branded city poster.',
                'negative_prompt' => '',
                'usage_notes' => '',
                'workflow_notes' => '',
                'documentation' => null,
                'placeholders' => [
                    [
                        'label' => 'Location profile',
                        'description' => 'Region-specific context block.',
                        'type' => 'location_profile',
                    ],
                ],
                'helper_prompts' => [],
                'prompt_variants' => [],
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'aspect_ratio' => '16:9',
                'tags' => ['travel'],
                'preview_image' => '',
                'featured' => false,
                'prompt_of_week' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
            ]);

        $prompt = AcademyPromptTemplate::query()->where('slug', 'custom-type-prompt')->firstOrFail();

        $response->assertRedirect(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]));
        $this->assertSame('location_profile', $prompt->placeholders[0]['type'] ?? null);
    }

    public function test_admin_course_edit_form_includes_outline_summary(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $course = AcademyCourse::query()->create([
            'title' => 'Outline Course',
            'slug' => 'outline-course',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);
        $section = $course->sections()->create([
            'title' => 'Introduction',
            'slug' => 'introduction',
            'order_num' => 0,
            'is_visible' => true,
        ]);
        $requiredLesson = AcademyLesson::query()->create([
            'title' => 'Required Lesson',
            'slug' => 'required-lesson',
            'content' => '<p>Body</p>',
            'cover_image' => 'academy/lessons/covers/required-cover.webp',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'published_at' => Carbon::parse('2026-05-10 09:30:00'),
            'active' => true,
        ]);
        $optionalLesson = AcademyLesson::query()->create([
            'title' => 'Optional Lesson',
            'slug' => 'optional-lesson',
            'content' => '<p>Body</p>',
            'article_cover_image' => 'academy/lessons/covers/optional-article-cover.webp',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'published_at' => Carbon::parse('2099-05-18 14:00:00'),
            'active' => true,
        ]);
        $libraryLesson = AcademyLesson::query()->create([
            'title' => 'Library Lesson',
            'slug' => 'library-lesson',
            'content' => '<p>Body</p>',
            'cover_image' => 'academy/lessons/covers/library-cover.webp',
            'difficulty' => 'intermediate',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'published_at' => Carbon::parse('2099-06-01 08:15:00'),
            'active' => false,
        ]);
        $otherCourse = AcademyCourse::query()->create([
            'title' => 'Other Course',
            'slug' => 'other-course',
            'excerpt' => 'Other course',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);
        $otherCourseLesson = AcademyLesson::query()->create([
            'title' => 'Used Elsewhere Lesson',
            'slug' => 'used-elsewhere-lesson',
            'content' => '<p>Body</p>',
            'difficulty' => 'advanced',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
        ]);

        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'section_id' => $section->id,
            'lesson_id' => $requiredLesson->id,
            'order_num' => 0,
            'is_required' => true,
        ]);
        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'section_id' => null,
            'lesson_id' => $optionalLesson->id,
            'order_num' => 1,
            'is_required' => false,
        ]);
        AcademyCourseLesson::query()->create([
            'course_id' => $otherCourse->id,
            'section_id' => null,
            'lesson_id' => $otherCourseLesson->id,
            'order_num' => 0,
            'is_required' => true,
        ]);

        $this->actingAs($admin)
            ->get(route('admin.academy.courses.edit', ['academyCourse' => $course]))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CrudForm')
                ->where('editorContext.outlineSummary.section_count', 1)
                ->where('editorContext.outlineSummary.visible_section_count', 1)
                ->where('editorContext.outlineSummary.lesson_count', 2)
                ->where('editorContext.outlineSummary.required_lesson_count', 1)
                ->where('editorContext.outlineSummary.unsectioned_lesson_count', 1)
                ->where('editorContext.outlineSummary.sections.0.title', 'Introduction')
                ->where('editorContext.outlineSummary.sections.0.lesson_count', 1)
                ->where('editorContext.sectionStoreUrl', route('admin.academy.courses.sections.store', ['academyCourse' => $course]))
                ->where('editorContext.courseSections.0.title', 'Introduction')
                ->where('editorContext.courseSections.0.update_url', route('admin.academy.courses.sections.update', ['academyCourse' => $course, 'academyCourseSection' => $section]))
                ->where('editorContext.courseLessons.0.section_id', $section->id)
                ->where('editorContext.courseLessons.0.cover_image_url', fn ($url) => is_string($url) && str_ends_with($url, '/academy/lessons/covers/required-cover.webp'))
                ->where('editorContext.courseLessons.0.active', true)
                ->where('editorContext.courseLessons.0.publication_state', 'published')
                ->where('editorContext.courseLessons.0.publication_label', 'Published')
                ->where('editorContext.courseLessons.1.cover_image_url', fn ($url) => is_string($url) && str_ends_with($url, '/academy/lessons/covers/optional-article-cover.webp'))
                ->where('editorContext.courseLessons.1.publication_state', 'scheduled')
                ->where('editorContext.courseLessons.1.publication_label', 'Publishes 2099-05-18 14:00')
                ->where('editorContext.availableLessons', fn ($lessons) => count($lessons) === 1)
                ->where('editorContext.availableLessons.0.title', 'Library Lesson')
                ->where('editorContext.availableLessons.0.cover_image_url', fn ($url) => is_string($url) && str_ends_with($url, '/academy/lessons/covers/library-cover.webp'))
                ->where('editorContext.availableLessons.0.active', false)
                ->where('editorContext.availableLessons.0.publication_state', 'scheduled')
                ->where('editorContext.availableLessons.0.publication_label', 'Publishes 2099-06-01 08:15')
                ->where('editorContext.availableLessons.0.edit_url', route('admin.academy.lessons.edit', ['academyLesson' => $libraryLesson])));
    }

    public function test_admin_can_store_course_with_rich_description_and_media_fields(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.courses.store'), [
                'title' => 'Foundations Course',
                'slug' => 'foundations-course',
                'subtitle' => 'A guided path for Skinbase creators',
                'excerpt' => 'A strong introduction to AI-assisted digital art workflows.',
                'description' => '<h2>What you will learn</h2><p>Prompt structure, workflow cleanup, and publication readiness.</p>',
                'cover_image' => 'academy/lessons/covers/aa/bb/course-cover.webp',
                'teaser_image' => 'academy/lessons/covers/cc/dd/course-teaser.webp',
                'access_level' => 'free',
                'difficulty' => 'beginner',
                'status' => 'published',
                'is_featured' => true,
                'order_num' => 5,
                'estimated_minutes' => 90,
                'published_at' => '',
                'seo_title' => 'Foundations Course',
                'seo_description' => 'A guided Academy course for Skinbase creators.',
                'meta_keywords' => 'academy, ai art, workflow',
                'og_title' => 'Foundations Course',
                'og_description' => 'Learn the fundamentals of AI-assisted digital art.',
                'og_image' => 'academy/lessons/covers/ee/ff/course-og.webp',
            ]);

        $course = AcademyCourse::query()->where('slug', 'foundations-course')->firstOrFail();

        $response->assertRedirect(route('admin.academy.courses.edit', ['academyCourse' => $course]));
        $this->assertSame('academy/lessons/covers/aa/bb/course-cover.webp', $course->cover_image);
        $this->assertSame('academy/lessons/covers/cc/dd/course-teaser.webp', $course->teaser_image);
        $this->assertStringContainsString('<h2>What you will learn</h2>', (string) $course->description);
        $this->assertTrue((bool) $course->is_featured);
        $this->assertNotNull($course->published_at);
    }

    public function test_admin_lessons_index_includes_course_names_and_order(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $course = AcademyCourse::query()->create([
            'title' => 'Prompt Foundations',
            'slug' => 'prompt-foundations',
            'excerpt' => 'Prompt course',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);
        $lesson = AcademyLesson::query()->create([
            'title' => 'Subject and Scene Control',
            'slug' => 'subject-and-scene-control',
            'excerpt' => 'Learn how to direct the main subject cleanly.',
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'course_order' => 4,
            'reading_minutes' => 5,
            'active' => true,
        ]);

        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $lesson->id,
            'order_num' => 3,
            'is_required' => true,
        ]);

        $this->actingAs($admin)
            ->get(route('admin.academy.lessons.index'))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CrudIndex')
                ->where('resource', 'lessons')
                ->where('columns.1', 'course_names')
                ->where('columns.2', 'course_order')
                ->where('items.data.0.title', 'Subject and Scene Control')
                ->where('items.data.0.course_names.0', 'Prompt Foundations')
                ->where('items.data.0.course_order', 4)
                ->where('items.data.0.active', true));
    }

    public function test_admin_can_import_course_lessons_from_json_toc(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $category = AcademyCategory::query()->create([
            'type' => 'lesson',
            'name' => 'Wallpaper Prompting',
            'slug' => 'wallpaper-prompting',
            'order_num' => 1,
            'active' => true,
        ]);
        $course = AcademyCourse::query()->create([
            'title' => 'Wallpaper Prompt Engineering',
            'slug' => 'wallpaper-prompt-engineering',
            'excerpt' => 'Learn to structure clean wallpaper prompts.',
            'access_level' => 'free',
            'difficulty' => 'intermediate',
            'status' => 'draft',
        ]);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.courses.lessons.import', ['academyCourse' => $course]), [
                'defaults' => [
                    'difficulty' => 'advanced',
                    'access_level' => 'creator',
                    'lesson_type' => 'article',
                    'active' => false,
                    'category_slug' => 'wallpaper-prompting',
                ],
                'lessons' => [
                    [
                        'title' => 'What Makes a Great Wallpaper Prompt?',
                        'slug' => 'what-makes-a-great-wallpaper-prompt',
                        'goal' => 'Explain what separates random AI images from clean, usable wallpapers.',
                    ],
                    [
                        'title' => 'Composition for Wallpapers',
                        'goal' => 'Cover centered subjects, negative space, cinematic framing, and icon-safe areas.',
                        'difficulty' => 'beginner',
                        'category' => 'Wallpaper Prompting',
                    ],
                ],
            ]);

        $response->assertRedirect(route('admin.academy.courses.edit', ['academyCourse' => $course]));

        $firstLesson = AcademyLesson::query()->where('slug', 'what-makes-a-great-wallpaper-prompt')->firstOrFail();
        $secondLesson = AcademyLesson::query()->where('slug', 'composition-for-wallpapers')->firstOrFail();

        $this->assertSame('Explain what separates random AI images from clean, usable wallpapers.', $firstLesson->excerpt);
        $this->assertSame('Cover centered subjects, negative space, cinematic framing, and icon-safe areas.', $secondLesson->excerpt);
        $this->assertSame((int) $category->id, (int) $firstLesson->category_id);
        $this->assertSame((int) $category->id, (int) $secondLesson->category_id);
        $this->assertSame('advanced', $firstLesson->difficulty);
        $this->assertSame('beginner', $secondLesson->difficulty);
        $this->assertSame('creator', $firstLesson->access_level);
        $this->assertFalse((bool) $firstLesson->active);
        $this->assertFalse((bool) $secondLesson->active);

        $courseLessons = AcademyCourseLesson::query()
            ->where('course_id', $course->id)
            ->orderBy('order_num')
            ->get();

        $this->assertSame([$firstLesson->id, $secondLesson->id], $courseLessons->pluck('lesson_id')->map(static fn ($value) => (int) $value)->all());
        $this->assertSame([0, 1], $courseLessons->pluck('order_num')->map(static fn ($value) => (int) $value)->all());
        $this->assertSame(1, (int) $firstLesson->fresh()->lesson_number);
        $this->assertSame(1, (int) $firstLesson->fresh()->course_order);
        $this->assertSame(2, (int) $secondLesson->fresh()->lesson_number);
        $this->assertSame(2, (int) $secondLesson->fresh()->course_order);
        $this->assertSame(2, (int) $course->fresh()->lessons_count_cache);
    }

    public function test_admin_category_update_clears_academy_cache(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $category = AcademyCategory::query()->create([
            'type' => 'lesson',
            'name' => 'Prompting Basics',
            'slug' => 'prompting-basics',
            'order_num' => 10,
            'active' => true,
        ]);

        Cache::put('academy.home', ['stale' => true], 600);
        Cache::put('academy.categories.lesson', ['stale' => true], 600);

        $this->actingAs($admin)
            ->patch(route('admin.academy.categories.update', ['academyCategory' => $category]), [
                'type' => 'lesson',
                'name' => 'Prompting Basics Updated',
                'slug' => 'prompting-basics',
                'description' => 'Updated description',
                'icon' => 'fa-wand-magic-sparkles',
                'order_num' => 11,
                'active' => true,
            ])
            ->assertRedirect(route('admin.academy.categories.edit', ['academyCategory' => $category]));

        $this->assertNull(Cache::get('academy.home'));
        $this->assertNull(Cache::get('academy.categories.lesson'));
    }

    public function test_admin_can_create_a_lesson_with_ai_comparison_block(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.lessons.store'), [
                'title' => 'AI Comparison Lesson',
                'slug' => 'ai-comparison-lesson',
                'excerpt' => 'Testing comparison block creation.',
                'content' => '<p>Lesson body.</p>',
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'lesson_type' => 'article',
                'cover_image' => '',
                'article_cover_image' => 'academy/lessons/covers/article-cover.webp',
                'tags' => ['ai comparison', 'models'],
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [
                    [
                        'type' => 'ai_comparison',
                        'title' => 'Same Prompt, Different AI Models',
                        'payload' => [
                            'title' => 'Same Prompt, Different AI Models',
                            'intro' => 'Compare multiple tools.',
                            'prompt' => 'A peaceful fantasy forest wallpaper.',
                            'negative_prompt' => 'text, watermark',
                            'aspect_ratio' => '16:9',
                            'criteria' => ['Composition', 'Lighting'],
                        ],
                        'sort_order' => 0,
                        'active' => true,
                        'comparison_results' => [],
                    ],
                ],
            ]);

        $lesson = AcademyLesson::query()->where('slug', 'ai-comparison-lesson')->firstOrFail();

        $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));

        $this->assertDatabaseHas('academy_lesson_blocks', [
            'lesson_id' => $lesson->id,
            'type' => 'ai_comparison',
            'active' => true,
        ]);
    }

    public function test_admin_can_create_a_lesson_from_markdown(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $markdown = <<<'MD'
# Cleaner scene direction

Use **specific nouns** and keep the camera angle stable.

- State the subject
- Add lighting
MD;

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.lessons.store'), [
                'title' => 'Markdown Lesson',
                'slug' => 'markdown-lesson',
                'excerpt' => 'Testing markdown lesson creation.',
                'content' => '',
                'content_markdown' => $markdown,
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'lesson_type' => 'article',
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 6,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ]);

        $lesson = AcademyLesson::query()->where('slug', 'markdown-lesson')->firstOrFail();

        $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
        $this->assertSame($markdown, $lesson->content_markdown);
        $this->assertStringContainsString('<h1>Cleaner scene direction</h1>', (string) $lesson->content);
        $this->assertStringContainsString('<strong>specific nouns</strong>', (string) $lesson->content);
        $this->assertStringContainsString('<li>State the subject</li>', (string) $lesson->content);
    }

    public function test_admin_can_store_lesson_numbering_fields(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $longTag = str_repeat('a', 100);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.lessons.store'), [
                'title' => 'Ordered Lesson',
                'slug' => 'ordered-lesson',
                'lesson_number' => 3,
                'course_order' => 3,
                'series_name' => 'AI Art Basics',
                'excerpt' => 'Testing ordering field persistence.',
                'content' => '<p>Lesson body.</p>',
                'tags' => [$longTag, 'academy'],
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'lesson_type' => 'article',
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ]);

        $lesson = AcademyLesson::query()->where('slug', 'ordered-lesson')->firstOrFail();

        $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
        $this->assertDatabaseHas('academy_lessons', [
            'id' => $lesson->id,
            'lesson_number' => 3,
            'course_order' => 3,
            'series_name' => 'AI Art Basics',
        ]);
        $this->assertSame([$longTag, 'academy'], $lesson->fresh()->tags);
    }

    public function test_admin_lesson_reading_time_is_calculated_from_content(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $body = '<p>'.implode(' ', array_fill(0, 420, 'prompt')).'</p>';

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.lessons.store'), [
                'title' => 'Auto Reading Lesson',
                'slug' => 'auto-reading-lesson',
                'excerpt' => 'Estimate reading time from content.',
                'content' => $body,
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'lesson_type' => 'article',
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 1,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ]);

        $lesson = AcademyLesson::query()->where('slug', 'auto-reading-lesson')->firstOrFail();

        $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
        $this->assertSame(3, (int) $lesson->reading_minutes);
    }

    public function test_admin_can_attach_lesson_to_courses_from_lesson_form(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $courseA = AcademyCourse::query()->create([
            'title' => 'Foundations',
            'slug' => 'foundations',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);
        $courseB = AcademyCourse::query()->create([
            'title' => 'Prompt Engineering',
            'slug' => 'prompt-engineering',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.lessons.store'), [
                'title' => 'Course Attached Lesson',
                'slug' => 'course-attached-lesson',
                'excerpt' => 'Attach this lesson to multiple courses.',
                'content' => '<p>Lesson body.</p>',
                'course_ids' => [$courseA->id, $courseB->id],
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'lesson_type' => 'article',
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ]);

        $lesson = AcademyLesson::query()->where('slug', 'course-attached-lesson')->firstOrFail();

        $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
        $this->assertSame(2, AcademyCourseLesson::query()->where('lesson_id', $lesson->id)->count());
        $this->assertDatabaseHas('academy_course_lessons', ['course_id' => $courseA->id, 'lesson_id' => $lesson->id]);
        $this->assertDatabaseHas('academy_course_lessons', ['course_id' => $courseB->id, 'lesson_id' => $lesson->id]);
        $this->assertSame(1, $courseA->fresh()->lessons_count_cache);
        $this->assertSame(1, $courseB->fresh()->lessons_count_cache);
    }

    public function test_admin_lesson_edit_form_includes_numbering_and_course_outline_context(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $course = AcademyCourse::query()->create([
            'title' => 'Lesson Mapping Course',
            'slug' => 'lesson-mapping-course',
            'access_level' => 'free',
            'difficulty' => 'beginner',
            'status' => 'draft',
        ]);

        AcademyLesson::query()->create([
            'title' => 'Lesson One',
            'slug' => 'lesson-one',
            'lesson_number' => 1,
            'course_order' => 1,
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $currentLesson = AcademyLesson::query()->create([
            'title' => 'Current Lesson',
            'slug' => 'current-lesson',
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $otherCourseLesson = AcademyLesson::query()->create([
            'title' => 'Lesson Three',
            'slug' => 'lesson-three',
            'lesson_number' => 3,
            'course_order' => 3,
            'content' => '<p>Body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'reading_minutes' => 5,
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $otherCourseLesson->id,
            'order_num' => 0,
            'is_required' => true,
        ]);

        AcademyCourseLesson::query()->create([
            'course_id' => $course->id,
            'lesson_id' => $currentLesson->id,
            'order_num' => 1,
            'is_required' => true,
        ]);

        $this->actingAs($admin)
            ->get(route('admin.academy.lessons.edit', ['academyLesson' => $currentLesson]))
            ->assertOk()
            ->assertInertia(fn (AssertableInertia $page) => $page
                ->component('Admin/Academy/CrudForm')
                ->where('resource', 'lessons')
                ->where('record.course_ids.0', (string) $course->id)
                ->where('editorContext.currentLessonId', $currentLesson->id)
                ->where('editorContext.numbering.lesson_number.suggested', 2)
                ->where('editorContext.numbering.lesson_number.missing.0', 2)
                ->where('editorContext.numbering.course_order.suggested', 2)
                ->where('editorContext.courses.0.lesson_count', 2)
                ->where('editorContext.courses.0.attach_url', route('admin.academy.courses.lessons.attach', ['academyCourse' => $course]))
                ->where('editorContext.courses.0.reorder_url', route('admin.academy.courses.reorder', ['academyCourse' => $course]))
                ->where('editorContext.courses.0.lessons.0.order_num', 0)
                ->where('editorContext.courses.0.lessons.1.is_current', true)
                ->where('editorContext.courses.0.lessons.1.destroy_url', fn ($value) => is_string($value) && str_contains($value, "/moderation/academy/courses/{$course->id}/lessons/")));
    }

    public function test_admin_can_store_article_cover_image(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('admin.academy.lessons.store'), [
                'title' => 'Lesson With Article Cover',
                'slug' => 'lesson-with-article-cover',
                'excerpt' => 'Testing article cover persistence.',
                'content' => '<p>Lesson body.</p>',
                'tags' => ['cover', 'article'],
                'difficulty' => 'beginner',
                'access_level' => 'free',
                'lesson_type' => 'article',
                'cover_image' => 'academy/lessons/covers/hero-cover.webp',
                'article_cover_image' => 'academy/lessons/covers/article-cover.webp',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ]);

        $lesson = AcademyLesson::query()->where('slug', 'lesson-with-article-cover')->firstOrFail();

        $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
        $this->assertDatabaseHas('academy_lessons', [
            'id' => $lesson->id,
            'article_cover_image' => 'academy/lessons/covers/article-cover.webp',
        ]);
    }

    public function test_admin_can_upload_lesson_cover_image_at_six_hundred_width(): void
    {
        config()->set('uploads.object_storage.disk', 's3');
        Storage::fake('s3');

        $admin = User::factory()->create(['role' => 'admin']);

        $response = $this->actingAs($admin)
            ->post(route('api.studio.academy.lessons.media.upload'), [
                'slot' => 'cover',
                'image' => UploadedFile::fake()->image('lesson-cover.png', 600, 315),
            ]);

        $response
            ->assertOk()
            ->assertJsonPath('slot', 'cover')
            ->assertJsonPath('width', 600)
            ->assertJsonPath('height', 315);

        $payload = $response->json();

        $this->assertIsString($payload['path'] ?? null);
        $this->assertIsString($payload['thumb_path'] ?? null);

        Storage::disk('s3')->assertExists($payload['path']);
        Storage::disk('s3')->assertExists($payload['thumb_path']);
    }

    public function test_admin_can_add_ai_comparison_result_to_existing_lesson(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $lesson = AcademyLesson::query()->create([
            'title' => 'Existing Lesson',
            'slug' => 'existing-lesson',
            'content' => 'Body',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
                'title' => $lesson->title,
                'slug' => $lesson->slug,
                'excerpt' => '',
                'content' => $lesson->content,
                'difficulty' => $lesson->difficulty,
                'access_level' => $lesson->access_level,
                'lesson_type' => $lesson->lesson_type,
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [
                    [
                        'type' => 'ai_comparison',
                        'title' => 'Same Prompt, Different AI Models',
                        'payload' => [
                            'title' => 'Same Prompt, Different AI Models',
                            'intro' => 'Compare multiple tools.',
                            'prompt' => 'A peaceful fantasy forest wallpaper.',
                            'negative_prompt' => '',
                            'aspect_ratio' => '16:9',
                            'criteria' => ['Composition'],
                        ],
                        'sort_order' => 0,
                        'active' => true,
                        'comparison_results' => [
                            [
                                'provider' => 'OpenAI',
                                'model_name' => 'ChatGPT Images',
                                'image_path' => 'academy/lessons/body/aa/bb/example.webp',
                                'thumb_path' => 'academy/lessons/body/aa/bb/example-thumb.webp',
                                'settings' => 'Default quality',
                                'strengths' => 'Strong composition',
                                'weaknesses' => 'Slightly over-polished',
                                'best_for' => 'Wallpaper concepts',
                                'score' => 9,
                                'sort_order' => 0,
                                'active' => true,
                            ],
                        ],
                    ],
                ],
            ])
            ->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));

        $block = AcademyLessonBlock::query()->where('lesson_id', $lesson->id)->firstOrFail();

        $this->assertDatabaseHas('academy_ai_comparison_results', [
            'lesson_block_id' => $block->id,
            'provider' => 'OpenAI',
            'model_name' => 'ChatGPT Images',
            'score' => 9,
        ]);
    }

    public function test_admin_markdown_update_regenerates_lesson_html(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $lesson = AcademyLesson::query()->create([
            'title' => 'Markdown Update Lesson',
            'slug' => 'markdown-update-lesson',
            'content' => '<p>Old body</p>',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $markdown = <<<'MD'
## Prompt checklist

1. Start with the scene.
2. Add the style.

> Keep one clear subject.
MD;

        $this->actingAs($admin)
            ->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
                'title' => $lesson->title,
                'slug' => $lesson->slug,
                'excerpt' => '',
                'content' => '',
                'content_markdown' => $markdown,
                'difficulty' => $lesson->difficulty,
                'access_level' => $lesson->access_level,
                'lesson_type' => $lesson->lesson_type,
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ])
            ->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));

        $lesson->refresh();

        $this->assertSame($markdown, $lesson->content_markdown);
        $this->assertStringContainsString('<h2>Prompt checklist</h2>', (string) $lesson->content);
        $this->assertStringContainsString('<ol>', (string) $lesson->content);
        $this->assertStringContainsString('<blockquote>', (string) $lesson->content);
    }

    public function test_admin_html_lesson_update_does_not_rewrite_legacy_html_from_generated_markdown(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $legacyHtml = '<h2>Final note</h2><p><strong>Prompting</strong> is a creative skill.</p><ul><li>Keep the best result</li><li>Prepare it</li><li>Present it with care</li></ul>';

        $lesson = AcademyLesson::query()->create([
            'title' => 'Legacy HTML Lesson',
            'slug' => 'legacy-html-lesson',
            'excerpt' => 'Legacy HTML body.',
            'content' => $legacyHtml,
            'content_markdown' => null,
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
                'title' => $lesson->title,
                'slug' => $lesson->slug,
                'excerpt' => 'Updated excerpt only.',
                'content' => $legacyHtml,
                'content_markdown' => '',
                'content_source' => 'html',
                'difficulty' => $lesson->difficulty,
                'access_level' => $lesson->access_level,
                'lesson_type' => $lesson->lesson_type,
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [],
            ])
            ->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));

        $lesson->refresh();

        $this->assertSame('Updated excerpt only.', $lesson->excerpt);
        $this->assertSame($legacyHtml, $lesson->content);
        $this->assertNull($lesson->content_markdown);
    }

    public function test_ai_comparison_score_must_stay_in_range(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $lesson = AcademyLesson::query()->create([
            'title' => 'Validation Lesson',
            'slug' => 'validation-lesson',
            'content' => 'Body',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);

        $this->from(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]))
            ->actingAs($admin)
            ->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
                'title' => $lesson->title,
                'slug' => $lesson->slug,
                'excerpt' => '',
                'content' => $lesson->content,
                'difficulty' => $lesson->difficulty,
                'access_level' => $lesson->access_level,
                'lesson_type' => $lesson->lesson_type,
                'cover_image' => '',
                'video_url' => '',
                'reading_minutes' => 5,
                'featured' => false,
                'active' => true,
                'published_at' => now()->subMinute()->toDateTimeString(),
                'seo_title' => '',
                'seo_description' => '',
                'blocks' => [
                    [
                        'type' => 'ai_comparison',
                        'title' => 'Invalid score block',
                        'payload' => [
                            'title' => 'Invalid score block',
                            'prompt' => 'Prompt',
                            'criteria' => ['Composition'],
                        ],
                        'sort_order' => 0,
                        'active' => true,
                        'comparison_results' => [
                            [
                                'provider' => 'OpenAI',
                                'model_name' => 'ChatGPT Images',
                                'image_path' => 'academy/lessons/body/aa/bb/example.webp',
                                'score' => 11,
                                'sort_order' => 0,
                                'active' => true,
                            ],
                        ],
                    ],
                ],
            ])
            ->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]))
            ->assertSessionHasErrors(['blocks.0.comparison_results.0.score']);
    }

    public function test_lesson_delete_soft_deletes_ai_comparison_children(): void
    {
        $admin = User::factory()->create(['role' => 'admin']);
        $lesson = AcademyLesson::query()->create([
            'title' => 'Delete Lesson',
            'slug' => 'delete-lesson',
            'content' => 'Body',
            'difficulty' => 'beginner',
            'access_level' => 'free',
            'lesson_type' => 'article',
            'active' => true,
            'published_at' => now()->subMinute(),
        ]);
        $block = AcademyLessonBlock::query()->create([
            'lesson_id' => $lesson->id,
            'type' => 'ai_comparison',
            'title' => 'Delete Block',
            'payload' => ['title' => 'Delete Block', 'prompt' => 'Prompt'],
            'sort_order' => 0,
            'active' => true,
        ]);
        $result = AcademyAiComparisonResult::query()->create([
            'lesson_block_id' => $block->id,
            'provider' => 'OpenAI',
            'model_name' => 'ChatGPT Images',
            'image_path' => 'academy/lessons/body/aa/bb/example.webp',
            'score' => 8,
            'sort_order' => 0,
            'active' => true,
        ]);

        $this->actingAs($admin)
            ->delete(route('admin.academy.lessons.destroy', ['academyLesson' => $lesson]))
            ->assertRedirect(route('admin.academy.lessons.index'));

        $this->assertSoftDeleted('academy_lessons', ['id' => $lesson->id]);
        $this->assertSoftDeleted('academy_lesson_blocks', ['id' => $block->id]);
        $this->assertSoftDeleted('academy_ai_comparison_results', ['id' => $result->id]);
    }

    private function seedAcademySubscription(User $user, string $priceId, string $status = 'active', ?Carbon $endsAt = null): void
    {
        $subscriptionId = DB::table('subscriptions')->insertGetId([
            'user_id' => $user->id,
            'type' => 'academy',
            'stripe_id' => 'sub_'.$user->id.'_'.md5($priceId.$status.($endsAt?->toISOString() ?? 'active')),
            'stripe_status' => $status,
            'stripe_price' => $priceId,
            'quantity' => 1,
            'trial_ends_at' => null,
            'ends_at' => $endsAt,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        DB::table('subscription_items')->insert([
            'subscription_id' => $subscriptionId,
            'stripe_id' => 'si_'.$user->id.'_'.md5($priceId.$status),
            'stripe_product' => 'prod_'.md5($priceId),
            'stripe_price' => $priceId,
            'quantity' => 1,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }
}
