<?php

declare(strict_types=1);

use App\Models\User;
use App\Models\World;
use App\Models\WorldRewardGrant;
use App\Models\WorldSubmission;
use App\Models\WorldWebStory;
use App\Models\Artwork;
use App\Models\Group;
use App\Models\GroupChallenge;
use App\Services\HomepageService;
use App\Services\Worlds\WorldService;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use cPad\Plugins\News\Models\NewsArticle;
use cPad\Plugins\News\Models\NewsCategory;

function publicWorld(array $attributes = []): World
{
    $creator = $attributes['creator'] ?? User::factory()->create([
        'username' => 'publicworlds-' . Str::lower(Str::random(6)),
        'name' => 'Public Worlds',
    ]);

    unset($attributes['creator']);

    return World::query()->create(array_merge([
        'title' => 'Summer Slam 2026',
        'slug' => 'summer-slam-2026',
        'tagline' => 'Sunlit publishing and warm-color campaigns.',
        'summary' => 'A bright world for summer culture across the platform.',
        'teaser_title' => 'Now live: Summer Slam',
        'teaser_summary' => 'A bright world for summer culture across the platform.',
        'description' => 'Public world description',
        'theme_key' => 'summer',
        'status' => World::STATUS_PUBLISHED,
        'type' => World::TYPE_SEASONAL,
        'is_featured' => true,
        'is_active_campaign' => true,
        'is_homepage_featured' => true,
        'campaign_priority' => 250,
        'campaign_label' => 'Seasonal spotlight',
        'starts_at' => Carbon::now()->subDays(2),
        'ends_at' => Carbon::now()->addDays(14),
        'promotion_starts_at' => Carbon::now()->subDay(),
        'promotion_ends_at' => Carbon::now()->addDays(7),
        'published_at' => Carbon::now()->subDays(10),
        'created_by_user_id' => $creator->id,
    ], $attributes));
}

function worldNewsCategory(array $attributes = []): NewsCategory
{
    return NewsCategory::query()->create(array_merge([
        'name' => 'World Updates',
        'slug' => 'world-updates-' . Str::lower(Str::random(6)),
        'description' => 'Editorial context for worlds and linked campaigns.',
        'position' => 0,
        'is_active' => true,
    ], $attributes));
}

function publishedWorldNews(User $author, NewsCategory $category, array $attributes = []): NewsArticle
{
    return NewsArticle::query()->create(array_merge([
        'title' => 'World challenge update',
        'slug' => 'world-challenge-update-' . Str::lower(Str::random(6)),
        'excerpt' => 'An editorial update for the linked world challenge.',
        'content' => "# World challenge update\n\nEditorial context for the linked challenge.",
        'author_id' => $author->id,
        'category_id' => $category->id,
        'type' => NewsArticle::TYPE_ANNOUNCEMENT,
        'status' => 'published',
        'editorial_status' => NewsArticle::EDITORIAL_STATUS_PUBLISHED,
        'published_at' => now()->subHour(),
        'is_featured' => true,
        'is_pinned' => false,
    ], $attributes));
}

it('renders public worlds index and detail pages', function (): void {
    $world = publicWorld();

    $this->get(route('worlds.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldIndex')
            ->where('spotlightWorld.title', 'Summer Slam 2026')
            ->where('spotlightWorld.campaign_state_label', 'Live now')
            ->has('activeWorlds'));

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Summer Slam 2026')
            ->where('world.slug', 'summer-slam-2026')
            ->where('seo.canonical', route('worlds.show', ['world' => $world->slug])));
});

it('returns a relative latest world navigation link regardless of request host', function (): void {
    Cache::forget('worlds.navigation_campaign');

    $world = publicWorld([
        'title' => 'Hello Again',
        'slug' => 'hello-again',
        'campaign_priority' => 999,
    ]);

    $this->get('https://fooyd0.skinbase.org/worlds')
        ->assertOk();

    $campaign = app(WorldService::class)->navigationCampaign();

    expect($campaign)->not->toBeNull()
        ->and($campaign['title'])->toBe('Hello Again')
        ->and($campaign['url'])->toBe('/worlds/hello-again');
});

it('includes rewarded contributors on public world pages', function (): void {
    $creator = User::factory()->create([
        'username' => 'rewardedcreator-' . Str::lower(Str::random(6)),
    ]);
    $world = publicWorld();
    $artwork = \App\Models\Artwork::factory()->for($creator)->create([
        'title' => 'World Winner Artwork',
        'slug' => 'world-winner-artwork',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => \App\Models\Artwork::VISIBILITY_PUBLIC,
    ]);

    WorldRewardGrant::query()->create([
        'user_id' => $creator->id,
        'world_id' => $world->id,
        'artwork_id' => $artwork->id,
        'reward_type' => 'winner',
        'grant_source' => 'manual',
        'granted_at' => now()->subHour(),
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('rewardedContributors.count', 1)
            ->where('rewardedContributors.creator_count', 1)
            ->where('rewardedContributors.counts.winner', 1)
            ->where('rewardedContributors.items.0.badge_label', $world->title . ' Winner'));
});

it('exposes a published web story on the public world page payload', function (): void {
    $world = publicWorld([
        'title' => 'Fantasy Realms',
        'slug' => 'fantasy-realms',
    ]);

    $story = WorldWebStory::factory()->visible()->for($world)->create([
        'slug' => 'fantasy-realms',
        'title' => 'Fantasy Realms',
        'excerpt' => 'A cinematic journey through magical wallpapers and dreamlike digital art.',
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('webStory.slug', $story->slug)
            ->where('webStory.url', route('web-stories.show', ['slug' => $story->slug])));
});

it('renders recap payloads for ended worlds with published recaps', function (): void {
    $creator = User::factory()->create([
        'username' => 'recapcreator-' . Str::lower(Str::random(6)),
        'name' => 'Recap Creator',
    ]);
    $world = publicWorld([
        'creator' => $creator,
        'title' => 'Summer Slam 2025',
        'slug' => 'summer-slam-2025',
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'accepts_submissions' => true,
        'community_section_enabled' => true,
        'starts_at' => now()->subDays(30),
        'ends_at' => now()->subDays(5),
        'recap_status' => World::RECAP_STATUS_PUBLISHED,
        'recap_title' => 'Summer Slam 2025 recap',
        'recap_summary' => 'A tighter archive-facing summary for the edition.',
        'recap_intro' => '<p>The edition closed with standout artworks, community participation, and a published editorial recap.</p>',
        'recap_cover_path' => 'worlds/recaps/summer-slam-2025-cover.jpg',
        'recap_published_at' => now()->subDay(),
        'recap_stats_snapshot_json' => [
            'captured_at' => now()->subDay()->toIso8601String(),
            'summary' => [
                'views' => 1200,
                'unique_visitors' => 640,
                'submissions' => 18,
                'live_participations' => 18,
                'featured_participations' => 3,
                'reward_grants' => 1,
                'challenge_clicks' => 42,
                'winner_count' => 1,
                'finalist_count' => 0,
                'featured_artwork_count' => 2,
            ],
        ],
    ]);

    $category = worldNewsCategory([
        'name' => 'Recap Stories',
        'slug' => 'recap-stories-' . Str::lower(Str::random(6)),
    ]);
    $article = publishedWorldNews($creator, $category, [
        'title' => 'Summer Slam 2025 closing recap',
        'slug' => 'summer-slam-2025-closing-recap-' . Str::lower(Str::random(6)),
        'excerpt' => 'The final recap story for Summer Slam 2025.',
    ]);
    $world->update(['recap_article_id' => $article->id]);

    $featuredArtwork = Artwork::factory()->for($creator)->create([
        'title' => 'Curated Edition Highlight',
        'slug' => 'curated-edition-highlight-' . Str::lower(Str::random(6)),
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);
    $communityArtwork = Artwork::factory()->for($creator)->create([
        'title' => 'Community Spotlight Piece',
        'slug' => 'community-spotlight-piece-' . Str::lower(Str::random(6)),
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

    $group = Group::factory()->for($creator, 'owner')->create([
        'name' => 'Recap Crew',
        'slug' => 'recap-crew-' . Str::lower(Str::random(6)),
    ]);

    $world->worldRelations()->create([
        'section_key' => 'featured_artworks',
        'related_type' => 'artwork',
        'related_id' => $featuredArtwork->id,
        'context_label' => 'Editorial highlight',
        'sort_order' => 0,
        'is_featured' => true,
    ]);
    $world->worldRelations()->create([
        'section_key' => 'featured_creators',
        'related_type' => 'user',
        'related_id' => $creator->id,
        'context_label' => 'Edition lead',
        'sort_order' => 0,
        'is_featured' => true,
    ]);
    $world->worldRelations()->create([
        'section_key' => 'featured_groups',
        'related_type' => 'group',
        'related_id' => $group->id,
        'context_label' => 'Community group',
        'sort_order' => 0,
        'is_featured' => true,
    ]);
    $world->worldRelations()->create([
        'section_key' => 'news',
        'related_type' => 'news',
        'related_id' => $article->id,
        'context_label' => 'Closing story',
        'sort_order' => 0,
        'is_featured' => true,
    ]);

    $submission = WorldSubmission::query()->create([
        'world_id' => $world->id,
        'artwork_id' => $communityArtwork->id,
        'submitted_by_user_id' => $creator->id,
        'status' => WorldSubmission::STATUS_LIVE,
        'is_featured' => true,
        'featured_at' => now()->subHours(12),
        'created_at' => now()->subDay(),
        'updated_at' => now()->subDay(),
    ]);

    WorldRewardGrant::query()->create([
        'user_id' => $creator->id,
        'world_id' => $world->id,
        'world_submission_id' => $submission->id,
        'artwork_id' => $communityArtwork->id,
        'reward_type' => 'winner',
        'grant_source' => 'manual',
        'granted_at' => now()->subHours(6),
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Summer Slam 2025')
            ->where('world.has_recap', true)
            ->where('world.cta_label', 'Read full recap')
            ->where('world.cta_url', route('news.show', ['slug' => $article->slug]))
            ->where('recap.status', 'published')
            ->where('recap.title', 'Summer Slam 2025 recap')
            ->where('recap.summary', 'A tighter archive-facing summary for the edition.')
            ->where('recap.cover_url', rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/worlds/recaps/summer-slam-2025-cover.jpg')
            ->where('recap.article.title', 'Summer Slam 2025 closing recap')
            ->where('recap.featured_artworks.items.0.title', 'Curated Edition Highlight')
            ->where('recap.community_highlights.items.0.title', 'Community Spotlight Piece')
            ->where('recap.creators.items.0.title', 'Recap Creator')
            ->where('recap.creators.rewarded.0.badge_label', 'Summer Slam 2025 Winner')
            ->where('recap.stats.source', 'snapshot')
            ->where('recap.stats.items.0.key', 'views')
            ->where('sections', []));
});

it('exposes linked challenge panels, derived entries, and challenge backlinks', function (): void {
    $owner = User::factory()->create([
        'username' => 'challenge-owner-' . Str::lower(Str::random(6)),
    ]);
    $group = Group::factory()->for($owner, 'owner')->create();
    $world = publicWorld([
        'linked_challenge_id' => null,
        'show_linked_challenge_section' => true,
        'show_linked_challenge_entries' => true,
        'show_linked_challenge_winners' => true,
        'challenge_teaser_override' => 'World-specific framing for the linked challenge.',
    ]);

    $winner = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Challenge Champion',
        'slug' => 'challenge-champion',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);
    $entry = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Challenge Entry Two',
        'slug' => 'challenge-entry-two',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);
    $finalist = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Challenge Finalist',
        'slug' => 'challenge-finalist',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'World Challenge Finals',
        'slug' => 'world-challenge-finals-' . Str::lower(Str::random(6)),
        'summary' => 'Challenge summary',
        'description' => 'Challenge description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'start_at' => now()->subDay(),
        'end_at' => now()->addDay(),
        'created_by_user_id' => $owner->id,
        'featured_artwork_id' => null,
    ]);

    $challenge->artworks()->attach($winner->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 0]);
    $challenge->artworks()->attach($finalist->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 1]);
    $challenge->artworks()->attach($entry->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 2]);
    $challenge->outcomes()->create([
        'artwork_id' => $winner->id,
        'user_id' => $owner->id,
        'outcome_type' => 'winner',
        'position' => 1,
        'sort_order' => 0,
        'title_override' => 'Grand Winner',
        'awarded_by_user_id' => $owner->id,
        'awarded_at' => now(),
    ]);
    $challenge->outcomes()->create([
        'artwork_id' => $finalist->id,
        'user_id' => $owner->id,
        'outcome_type' => 'finalist',
        'sort_order' => 1,
        'note' => 'Outstanding finalist selection.',
        'awarded_by_user_id' => $owner->id,
        'awarded_at' => now(),
    ]);

    $world->update([
        'linked_challenge_id' => $challenge->id,
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('linkedChallenge.title', 'World Challenge Finals')
            ->where('linkedChallenge.summary', 'World-specific framing for the linked challenge.')
            ->where('linkedChallenge.state_label', 'Winners announced')
            ->where('linkedChallengeEntries.items.0.title', 'Challenge Champion')
            ->where('linkedChallengeWinners.item.title', 'Challenge Champion')
            ->where('linkedChallengeFinalists.items.0.title', 'Challenge Finalist')
            ->where('world.challenge_cta_label', 'See results'));

    $this->get(route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupChallengeShow')
            ->where('linkedWorld.title', $world->title)
            ->where('linkedWorld.public_url', route('worlds.show', ['world' => $world->slug]))
            ->where('linkedWorld.campaign_label', 'Seasonal spotlight')
            ->where('linkedWorld.challenge_cta_label', 'See results'));
});

it('hides selected linked challenge entries from the derived world feed', function (): void {
    $owner = User::factory()->create([
        'username' => 'challenge-hide-owner-' . Str::lower(Str::random(6)),
    ]);
    $group = Group::factory()->for($owner, 'owner')->create();
    $world = publicWorld();

    $hiddenEntry = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Hidden Challenge Entry',
        'slug' => 'hidden-challenge-entry',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);
    $visibleEntry = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Visible Challenge Entry',
        'slug' => 'visible-challenge-entry',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Hidden Entries Challenge',
        'slug' => 'hidden-entries-challenge-' . Str::lower(Str::random(6)),
        'summary' => 'Challenge summary',
        'description' => 'Challenge description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'start_at' => now()->subDay(),
        'end_at' => now()->addDay(),
        'created_by_user_id' => $owner->id,
    ]);

    $challenge->artworks()->attach($hiddenEntry->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 0]);
    $challenge->artworks()->attach($visibleEntry->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 1]);

    $world->update([
        'linked_challenge_id' => $challenge->id,
        'hidden_linked_challenge_artwork_ids_json' => [$hiddenEntry->id],
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('linkedChallengeEntries.hidden_count', 1)
            ->where('linkedChallengeEntries.items.0.title', 'Visible Challenge Entry')
            ->has('linkedChallengeEntries.items', 1));
});

it('maps community-vote challenges to a voting state on the world page', function (): void {
    $owner = User::factory()->create([
        'username' => 'challenge-vote-owner-' . Str::lower(Str::random(6)),
    ]);
    $group = Group::factory()->for($owner, 'owner')->create();
    $world = publicWorld();

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Vote Live Challenge',
        'slug' => 'vote-live-challenge-' . Str::lower(Str::random(6)),
        'summary' => 'Vote for the best entry.',
        'description' => 'Challenge description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ENDED,
        'judging_mode' => 'community_vote',
        'start_at' => now()->subDays(7),
        'end_at' => now()->subHour(),
        'created_by_user_id' => $owner->id,
    ]);

    $world->update([
        'linked_challenge_id' => $challenge->id,
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('linkedChallenge.state', 'voting')
            ->where('linkedChallenge.state_label', 'Voting live')
            ->where('linkedChallenge.cta_label', 'View entries')
            ->where('world.challenge_cta_label', 'View entries'));
});

it('shifts linked challenge CTAs into recap mode for archived worlds', function (): void {
    $owner = User::factory()->create([
        'username' => 'challenge-archive-owner-' . Str::lower(Str::random(6)),
    ]);
    $group = Group::factory()->for($owner, 'owner')->create();
    $world = publicWorld([
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => now()->subDays(20),
        'ends_at' => now()->subDays(3),
    ]);

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Archive Recap Challenge',
        'slug' => 'archive-recap-challenge-' . Str::lower(Str::random(6)),
        'summary' => 'Challenge summary',
        'description' => 'Challenge description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'start_at' => now()->subDays(5),
        'end_at' => now()->addDays(2),
        'created_by_user_id' => $owner->id,
    ]);

    $world->update([
        'linked_challenge_id' => $challenge->id,
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('linkedChallenge.state', 'closed')
            ->where('linkedChallenge.cta_label', 'View challenge recap')
            ->where('world.challenge_cta_label', 'View challenge recap'));
});

it('surfaces linked challenge recap stories and keeps derived sections in recap mode for archived worlds', function (): void {
    $owner = User::factory()->create([
        'username' => 'challenge-recap-owner-' . Str::lower(Str::random(6)),
    ]);
    $group = Group::factory()->for($owner, 'owner')->create();
    $world = publicWorld([
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => now()->subDays(30),
        'ends_at' => now()->subDays(5),
        'show_linked_challenge_section' => true,
        'show_linked_challenge_entries' => true,
        'show_linked_challenge_winners' => true,
        'show_linked_challenge_finalists' => true,
    ]);

    $winner = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Recap Winner',
        'slug' => 'recap-winner-' . Str::lower(Str::random(6)),
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

    $finalist = Artwork::factory()->for($owner)->create([
        'group_id' => $group->id,
        'title' => 'Recap Finalist',
        'slug' => 'recap-finalist-' . Str::lower(Str::random(6)),
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'World Recap Challenge',
        'slug' => 'world-recap-challenge-' . Str::lower(Str::random(6)),
        'summary' => 'Challenge summary',
        'description' => 'Challenge description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'start_at' => now()->subDays(10),
        'end_at' => now()->subDays(2),
        'created_by_user_id' => $owner->id,
        'featured_artwork_id' => null,
    ]);

    $challenge->artworks()->attach($winner->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 0]);
    $challenge->artworks()->attach($finalist->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 1]);
    $challenge->outcomes()->create([
        'artwork_id' => $winner->id,
        'user_id' => $owner->id,
        'outcome_type' => 'winner',
        'position' => 1,
        'sort_order' => 0,
        'awarded_by_user_id' => $owner->id,
        'awarded_at' => now(),
    ]);
    $challenge->outcomes()->create([
        'artwork_id' => $finalist->id,
        'user_id' => $owner->id,
        'outcome_type' => 'finalist',
        'sort_order' => 1,
        'awarded_by_user_id' => $owner->id,
        'awarded_at' => now(),
    ]);

    $category = worldNewsCategory([
        'name' => 'Challenge Recaps',
        'slug' => 'challenge-recaps-' . Str::lower(Str::random(6)),
    ]);
    $recap = publishedWorldNews($owner, $category, [
        'title' => 'World Recap Challenge results recap',
        'slug' => 'world-recap-challenge-results-' . Str::lower(Str::random(6)),
        'excerpt' => 'Winner highlights and finalist recap from the linked challenge.',
        'content' => "# Results recap\n\nWinner highlights and finalist recap.",
    ]);

    $world->update([
        'linked_challenge_id' => $challenge->id,
    ]);
    $world->worldRelations()->create([
        'section_key' => 'news',
        'related_type' => 'news',
        'related_id' => $recap->id,
        'context_label' => 'Challenge recap',
        'sort_order' => 0,
        'is_featured' => true,
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('linkedChallenge.state', 'closed')
            ->where('linkedChallenge.cta_label', 'View challenge recap')
            ->where('linkedChallenge.cta_url', route('news.show', ['slug' => $recap->slug]))
            ->where('linkedChallenge.challenge_url', route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge]))
            ->where('linkedChallenge.story.title', 'World Recap Challenge results recap')
            ->where('linkedChallenge.story.intent', 'recap')
            ->where('linkedChallengeEntries.description', 'Entries from the linked challenge remain visible here so the world recap preserves the full field of work.')
            ->where('linkedChallengeWinners.description', 'This world is carrying the linked challenge result forward so the campaign recap stays visible here too.')
            ->where('linkedChallengeFinalists.description', 'Finalists from the linked challenge remain visible here so the archived world keeps the complete recap in view.')
            ->where('world.challenge_cta_label', 'View challenge recap')
            ->where('world.challenge_cta_url', route('news.show', ['slug' => $recap->slug])));

    $this->get(route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupChallengeShow')
            ->where('linkedWorld.title', $world->title)
            ->where('linkedWorld.challenge_cta_label', 'View challenge recap')
            ->where('linkedWorld.challenge_cta_url', route('news.show', ['slug' => $recap->slug])));
});

it('falls back to the theme icon when the stored world icon is blank whitespace', function (): void {
    $world = publicWorld([
        'title' => 'Spring Vibes',
        'slug' => 'spring-vibes',
        'theme_key' => 'summer',
        'icon_name' => '   ',
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Spring Vibes')
            ->where('world.icon_name', 'fa-solid fa-sun')
            ->where('world.theme.icon_name', 'fa-solid fa-sun'));
});

it('omits disabled sections from the public world payload', function (): void {
    $world = publicWorld([
        'title' => 'Curated Autumn 2026',
        'slug' => 'curated-autumn-2026',
        'section_visibility_json' => [
            'featured_creators' => false,
        ],
    ]);

    $world->worldRelations()->create([
        'section_key' => 'featured_creators',
        'related_type' => 'user',
        'related_id' => $world->created_by_user_id,
        'context_label' => 'Editorial spotlight',
        'sort_order' => 0,
        'is_featured' => true,
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Curated Autumn 2026')
            ->where('sections', []));
});

it('keeps archived worlds publicly visible', function (): void {
    $world = publicWorld([
        'title' => 'Halloween World 2025',
        'slug' => 'halloween-world-2025',
        'theme_key' => 'halloween',
        'status' => World::STATUS_ARCHIVED,
        'starts_at' => Carbon::parse('2025-10-01 00:00:00'),
        'ends_at' => Carbon::parse('2025-11-01 00:00:00'),
        'published_at' => Carbon::parse('2025-09-20 10:00:00'),
    ]);

    $this->get(route('worlds.show', ['world' => $world->slug]))
        ->assertOk()
        ->assertSee('Halloween World 2025');
});

it('resolves recurring family and archived edition routes to the correct edition', function (): void {
    publicWorld([
        'title' => 'Spring Vibes 2025',
        'slug' => 'spring-vibes-2025',
        'is_recurring' => true,
        'recurrence_key' => 'spring-vibes',
        'edition_year' => 2025,
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => Carbon::parse('2025-03-01 00:00:00'),
        'ends_at' => Carbon::parse('2025-04-01 00:00:00'),
        'published_at' => Carbon::parse('2025-02-20 10:00:00'),
    ]);

    $currentEdition = publicWorld([
        'title' => 'Spring Vibes 2026',
        'slug' => 'spring-vibes-2026',
        'is_recurring' => true,
        'recurrence_key' => 'spring-vibes',
        'edition_year' => 2026,
        'campaign_priority' => 600,
    ]);

    $this->get(route('worlds.show', ['world' => 'spring-vibes']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Spring Vibes 2026')
            ->where('world.public_url', route('worlds.show', ['world' => 'spring-vibes']))
            ->has('archiveEditions', 1)
            ->where('archiveEditions.0.title', 'Spring Vibes 2025'));

    $this->get(route('worlds.editions.show', ['world' => 'spring-vibes', 'year' => 2025]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Spring Vibes 2025')
            ->where('currentEdition.title', 'Spring Vibes 2026')
            ->where('archiveNotice.current_edition.title', 'Spring Vibes 2026'));

    $this->get(route('worlds.show', ['world' => $currentEdition->slug]))
        ->assertRedirect(route('worlds.show', ['world' => 'spring-vibes']));

    $this->get(route('worlds.show', ['world' => 'spring-vibes-2025']))
        ->assertRedirect(route('worlds.editions.show', ['world' => 'spring-vibes', 'year' => 2025]));
});

it('exposes adjacent previous and next editions inside the archive payload', function (): void {
    publicWorld([
        'title' => 'Retro Month 2024',
        'slug' => 'retro-month-2024',
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'edition_year' => 2024,
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => Carbon::parse('2024-04-01 00:00:00'),
        'ends_at' => Carbon::parse('2024-04-30 00:00:00'),
        'published_at' => Carbon::parse('2024-03-20 10:00:00'),
    ]);

    publicWorld([
        'title' => 'Retro Month 2025',
        'slug' => 'retro-month-2025',
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'edition_year' => 2025,
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => Carbon::parse('2025-04-01 00:00:00'),
        'ends_at' => Carbon::parse('2025-04-30 00:00:00'),
        'published_at' => Carbon::parse('2025-03-20 10:00:00'),
    ]);

    publicWorld([
        'title' => 'Retro Month 2026',
        'slug' => 'retro-month-2026',
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'edition_year' => 2026,
        'campaign_priority' => 200,
    ]);

    $this->get(route('worlds.editions.show', ['world' => 'retro-month', 'year' => 2025]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('world.title', 'Retro Month 2025')
            ->where('previousEdition.title', 'Retro Month 2024')
            ->where('nextEdition.title', 'Retro Month 2026'));
});

it('exposes a homepage world spotlight when a featured world exists', function (): void {
    publicWorld([
        'title' => 'Pixel Week 2026',
        'slug' => 'pixel-week-2026',
        'theme_key' => 'pixel-week',
        'teaser_title' => 'Pixel Week is open for submissions',
    ]);

    app(HomepageService::class)->clearGuestPayloadCache();

    $this->get(route('index'))
        ->assertOk()
        ->assertSee(route('worlds.index'), false)
        ->assertSee('pixel-week-2026')
        ->assertSee('Pixel Week 2026');
});

it('splits live, upcoming, and archived worlds on the public index', function (): void {
    publicWorld([
        'title' => 'Spring Vibes',
        'slug' => 'spring-vibes',
        'is_recurring' => true,
        'recurrence_key' => 'spring-vibes',
        'edition_year' => 2026,
        'campaign_priority' => 500,
    ]);

    publicWorld([
        'title' => 'Spring Vibes 2025',
        'slug' => 'spring-vibes-2025',
        'is_recurring' => true,
        'recurrence_key' => 'spring-vibes',
        'edition_year' => 2025,
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => Carbon::now()->subDays(400),
        'ends_at' => Carbon::now()->subDays(365),
        'promotion_starts_at' => null,
        'promotion_ends_at' => null,
        'published_at' => Carbon::now()->subDays(420),
    ]);

    publicWorld([
        'title' => 'Retro Month 2026',
        'slug' => 'retro-month-2026',
        'starts_at' => Carbon::now()->addDays(10),
        'ends_at' => Carbon::now()->addDays(24),
        'promotion_starts_at' => Carbon::now()->addDays(8),
        'promotion_ends_at' => Carbon::now()->addDays(18),
        'teaser_title' => 'Retro Month is coming up',
    ]);

    publicWorld([
        'title' => 'Halloween World 2025',
        'slug' => 'halloween-world-2025',
        'theme_key' => 'halloween',
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => false,
        'is_homepage_featured' => false,
        'starts_at' => Carbon::now()->subDays(40),
        'ends_at' => Carbon::now()->subDays(20),
        'promotion_starts_at' => null,
        'promotion_ends_at' => null,
        'published_at' => Carbon::now()->subDays(50),
    ]);

    $this->get(route('worlds.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldIndex')
            ->where('spotlightWorld.title', 'Spring Vibes')
            ->has('upcomingWorlds', 1)
            ->has('recurringWorldFamilies', 1)
            ->where('recurringWorldFamilies.0.title', 'Spring Vibes')
            ->has('archivedWorlds', 2));
});
