<?php

declare(strict_types=1);

use App\Models\User;
use App\Models\Artwork;
use App\Models\ArtworkStats;
use App\Models\Category;
use App\Models\Collection;
use App\Models\ContentType;
use App\Models\Group;
use App\Models\GroupChallenge;
use App\Models\GroupChallengeOutcome;
use App\Models\Tag;
use App\Models\World;
use App\Models\WorldAnalyticsEvent;
use App\Models\WorldEditorialSuggestionState;
use App\Models\WorldRelation;
use App\Models\WorldRewardGrant;
use App\Models\WorldSubmission;
use App\Services\Worlds\WorldAnalyticsService;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use cPad\Plugins\News\Models\NewsArticle;
use cPad\Plugins\News\Models\NewsCategory;

function studioWorld(array $attributes = []): World
{
    $creator = $attributes['creator'] ?? User::factory()->create([
        'username' => 'worldbuilder',
        'name' => 'World Builder',
    ]);

    unset($attributes['creator']);

    return World::query()->create(array_merge([
        'title' => 'Halloween World 2026',
        'slug' => 'halloween-world-2026',
        'tagline' => 'Night drives, haunted pixels, and autumn launches.',
        'summary' => 'A curated seasonal destination for Halloween programming.',
        'description' => 'World description',
        'theme_key' => 'halloween',
        'status' => World::STATUS_DRAFT,
        'type' => World::TYPE_SEASONAL,
        'is_featured' => true,
        'created_by_user_id' => $creator->id,
    ], $attributes));
}

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

function studioPublishedWorldNews(User $author, NewsCategory $category, array $attributes = []): NewsArticle
{
    return NewsArticle::query()->create(array_merge([
        'title' => 'Studio recap story',
        'slug' => 'studio-recap-story-' . Str::lower(Str::random(6)),
        'excerpt' => 'A published recap story linked from the world editor.',
        'content' => "# Studio recap story\n\nLinked from the world editor.",
        '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));
}

function studioSuggestionArtwork(User $creator, Tag $tag, array $attributes = [], array $stats = []): Artwork
{
    $title = $attributes['title'] ?? ('Retro Artwork ' . Str::title(Str::lower(Str::random(4))));

    $artwork = Artwork::factory()->for($creator)->create(array_merge([
        'title' => $title,
        'slug' => Str::slug($title) . '-' . Str::lower(Str::random(6)),
        'description' => 'Retro neon artwork for world suggestion scoring.',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ], $attributes));

    $artwork->tags()->attach($tag->id, ['source' => 'user']);

    ArtworkStats::query()->create(array_merge([
        'artwork_id' => $artwork->id,
        'views' => 320,
        'downloads' => 14,
        'favorites' => 42,
        'rating_avg' => 4.8,
        'rating_count' => 18,
        'comments_count' => 6,
        'shares_count' => 4,
    ], $stats));

    return $artwork->fresh(['tags', 'stats', 'user.profile']);
}

function studioWorldSuggestionFixture(User $moderator): array
{
    $tag = Tag::factory()->create([
        'name' => 'Retro',
        'slug' => 'retro',
    ]);

    $communityCreator = User::factory()->create([
        'username' => 'retrocaptain',
        'name' => 'Retro Captain',
        'nova_featured_creator' => true,
    ]);

    $artworkCreator = User::factory()->create([
        'username' => 'neondrifter',
        'name' => 'Neon Drifter',
    ]);

    $previousWorld = studioWorld([
        'creator' => $moderator,
        'title' => 'Retro Month 2025',
        'slug' => 'retro-month-2025',
        'summary' => 'An archived edition with strong retro signals.',
        'status' => World::STATUS_ARCHIVED,
        'published_at' => now()->subDays(45),
        'starts_at' => now()->subDays(40),
        'ends_at' => now()->subDays(30),
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'edition_year' => 2025,
        'related_tags_json' => ['retro', 'neon'],
    ]);

    $previousWorld->worldRelations()->create([
        'section_key' => 'featured_creators',
        'related_type' => WorldRelation::TYPE_USER,
        'related_id' => $communityCreator->id,
        'sort_order' => 0,
        'is_featured' => true,
    ]);

    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Retro Month 2026',
        'slug' => 'retro-month-2026',
        'summary' => 'Editors are curating retro neon stories, creators, and challenge highlights.',
        'description' => 'A recurring world focused on retro, neon, and synth aesthetics.',
        'status' => World::STATUS_PUBLISHED,
        'published_at' => now()->subDays(3),
        'starts_at' => now()->subDay(),
        'ends_at' => now()->addDays(8),
        'accepts_submissions' => true,
        'community_section_enabled' => true,
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'edition_year' => 2026,
        'related_tags_json' => ['retro', 'neon'],
    ]);

    $communityArtwork = studioSuggestionArtwork($communityCreator, $tag, [
        'title' => 'Retro Skyline Community Entry',
        'description' => 'A retro neon skyline submitted to the community showcase.',
        'published_at' => now()->subHours(12),
    ], [
        'views' => 620,
        'favorites' => 88,
        'comments_count' => 11,
        'shares_count' => 9,
    ]);

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

    $challengeGroup = Group::factory()->create([
        'owner_user_id' => $communityCreator->id,
        'name' => 'Retro Signal Crew',
        'slug' => 'retro-signal-crew-' . Str::lower(Str::random(6)),
        'headline' => 'Retro and synth challenge specialists.',
        'bio' => 'A public group behind the linked retro challenge.',
        'followers_count' => 240,
        'artworks_count' => 12,
        'collections_count' => 4,
        'is_verified' => true,
    ]);

    $linkedChallenge = GroupChallenge::query()->create([
        'group_id' => $challengeGroup->id,
        'title' => 'Retro Signal Finals',
        'slug' => 'retro-signal-finals-' . Str::lower(Str::random(6)),
        'summary' => 'Linked challenge for retro-world finalists.',
        'description' => 'Editors can highlight finalists from this challenge.',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ENDED,
        'start_at' => now()->subDays(6),
        'end_at' => now()->subDays(1),
        'created_by_user_id' => $moderator->id,
    ]);

    $world->forceFill(['linked_challenge_id' => $linkedChallenge->id])->save();

    $challengeArtwork = studioSuggestionArtwork($communityCreator, $tag, [
        'title' => 'Retro Circuit Finalist',
        'description' => 'A finalist artwork from the linked retro challenge.',
        'published_at' => now()->subDays(2),
    ], [
        'views' => 540,
        'favorites' => 73,
        'comments_count' => 8,
        'shares_count' => 6,
    ]);

    $linkedChallenge->artworks()->attach($challengeArtwork->id, [
        'submitted_by_user_id' => $communityCreator->id,
        'sort_order' => 0,
    ]);

    GroupChallengeOutcome::query()->create([
        'group_challenge_id' => $linkedChallenge->id,
        'artwork_id' => $challengeArtwork->id,
        'user_id' => $communityCreator->id,
        'outcome_type' => GroupChallengeOutcome::TYPE_FINALIST,
        'position' => 1,
        'sort_order' => 0,
        'awarded_by_user_id' => $moderator->id,
        'awarded_at' => now()->subHours(18),
    ]);

    $artworkSuggestion = studioSuggestionArtwork($artworkCreator, $tag, [
        'title' => 'Neon Drift Poster',
        'description' => 'A retro neon poster aligned with the world brief.',
        'published_at' => now()->subDays(3),
    ], [
        'views' => 410,
        'favorites' => 57,
        'comments_count' => 5,
        'shares_count' => 5,
    ]);

    $collection = Collection::factory()->create([
        'user_id' => $communityCreator->id,
        'title' => 'Retro Signal Collection',
        'slug' => 'retro-signal-collection-' . Str::lower(Str::random(6)),
        'summary' => 'A curated collection of retro signal artwork.',
        'description' => 'Strong collection engagement around retro and neon work.',
        'views_count' => 520,
        'likes_count' => 140,
        'followers_count' => 86,
        'saves_count' => 33,
        'is_featured' => true,
        'published_at' => now()->subDays(2),
        'featured_at' => now()->subDay(),
    ]);

    $category = studioWorldNewsCategory([
        'name' => 'Retro World Updates',
    ]);

    $article = studioPublishedWorldNews($moderator, $category, [
        'title' => 'Retro Month results roundup',
        'slug' => 'retro-month-results-' . Str::lower(Str::random(6)),
        'excerpt' => 'Recap and results for the latest retro month challenge and showcase.',
        'content' => '# Retro Month results roundup\n\nRetro month recap with challenge finalists and community highlights.',
        'published_at' => now()->subHours(8),
    ]);

    return [
        'world' => $world->fresh(['worldRelations', 'linkedChallenge']),
        'previous_world' => $previousWorld,
        'community_creator' => $communityCreator,
        'artwork_creator' => $artworkCreator,
        'community_artwork' => $communityArtwork,
        'challenge_artwork' => $challengeArtwork,
        'artwork_suggestion' => $artworkSuggestion,
        'collection' => $collection,
        'group' => $challengeGroup,
        'challenge' => $linkedChallenge,
        'article' => $article,
    ];
}

it('forbids world studio pages for non moderators', function (): void {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->get(route('studio.worlds.index'))
        ->assertForbidden();

    $this->actingAs($user)
        ->get(route('studio.worlds.create'))
        ->assertRedirect(route('worlds.index'));

    $this->actingAs($user)
        ->get('/worlds/create')
        ->assertRedirect(route('worlds.index'));
});

it('sends guests from the public worlds create shortcut to login', function (): void {
    $this->get('/worlds/create')
        ->assertRedirect(route('login'));
});

it('renders world studio pages for moderators', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'modworlds',
        'name' => 'Moderator Worlds',
    ]);
    $world = studioWorld([
        'creator' => $moderator,
        'status' => World::STATUS_PUBLISHED,
        'published_at' => Carbon::parse('2026-10-01 10:00:00'),
        'starts_at' => Carbon::parse('2026-10-15 00:00:00'),
    ]);

    $this->actingAs($moderator)
        ->get(route('studio.worlds.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldsIndex')
            ->where('title', 'Worlds')
            ->where('listing.items.0.title', 'Halloween World 2026')
            ->where('analytics.default_range', '30d')
            ->where('createUrl', route('studio.worlds.create')));

    $this->actingAs($moderator)
        ->get('/worlds/create')
        ->assertRedirect(route('studio.worlds.create'));

    $this->actingAs($moderator)
        ->get(route('studio.worlds.create'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldEditor')
            ->where('title', 'Create world')
            ->has('themeOptions')
            ->has('sectionOptions')
            ->has('relationTypeOptions')
            ->where('mediaSupport.picker_available', false));

    $this->actingAs($moderator)
        ->get(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldEditor')
            ->where('world.title', 'Halloween World 2026')
            ->where('world.slug', 'halloween-world-2026')
            ->where('world.section_visibility_json.featured_artworks', true)
            ->where('duplicateActions.canCreateEdition', false)
            ->where('duplicateActions.duplicateModeOptions.0.value', 'structure_only'));

    $this->actingAs($moderator)
        ->get(route('studio.worlds.preview', ['world' => $world->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('previewMode', true)
            ->where('world.title', 'Halloween World 2026'));
});

it('renders world studio pages for legacy admin accounts', function (): void {
    $admin = User::factory()->create([
        'role' => 'user',
        'username' => 'legacyadminworlds',
        'name' => 'Legacy Admin Worlds',
    ]);

    DB::table('users')
        ->where('id', $admin->id)
        ->update(['isAdmin' => 1]);

    $admin->refresh();

    $this->actingAs($admin)
        ->get(route('studio.worlds.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldsIndex')
            ->where('title', 'Worlds')
            ->where('createUrl', route('studio.worlds.create')));

    $this->actingAs($admin)
        ->get(route('studio.worlds.create'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldEditor')
            ->where('title', 'Create world'));
});

it('surfaces recap workflow data in the studio editor and preview payload', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'recapstudiomod',
        'name' => 'Recap Studio Moderator',
    ]);
    $category = studioWorldNewsCategory();
    $article = studioPublishedWorldNews($moderator, $category, [
        'title' => 'Halloween World 2025 recap story',
    ]);
    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Halloween World 2025',
        'slug' => 'halloween-world-2025',
        'status' => World::STATUS_ARCHIVED,
        'starts_at' => now()->subDays(20),
        'ends_at' => now()->subDays(3),
        'published_at' => now()->subDays(25),
        'recap_status' => World::RECAP_STATUS_DRAFT,
        'recap_title' => 'Halloween World 2025 recap',
        'recap_summary' => 'Draft summary for the archived edition.',
        'recap_intro' => '<p>Draft recap intro.</p>',
        'recap_editor_note' => 'Internal recap note for archive cleanup.',
        'recap_cover_path' => 'worlds/recaps/halloween-world-2025-cover.jpg',
        'recap_article_id' => $article->id,
    ]);

    $this->actingAs($moderator)
        ->get(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldEditor')
            ->where('world.recap_status', 'draft')
            ->where('world.recap_status_label', 'Draft recap')
            ->where('world.recap_title', 'Halloween World 2025 recap')
            ->where('world.recap_editor_note', 'Internal recap note for archive cleanup.')
            ->where('world.recap_cover_path', 'worlds/recaps/halloween-world-2025-cover.jpg')
            ->where('world.recap_cover_url', rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/worlds/recaps/halloween-world-2025-cover.jpg')
            ->where('world.recap_article.title', 'Halloween World 2025 recap story')
            ->where('publishRecapUrl', route('studio.worlds.recap.publish', ['world' => $world])));

    $this->actingAs($moderator)
        ->get(route('studio.worlds.preview', ['world' => $world->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('World/WorldShow')
            ->where('previewMode', true)
            ->where('recap.status', 'draft_preview')
            ->where('recap.title', 'Halloween World 2025 recap')
            ->where('recap.cover_url', rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/worlds/recaps/halloween-world-2025-cover.jpg')
            ->where('recap.article.title', 'Halloween World 2025 recap story'));
});

it('publishes recap snapshots for ended worlds in studio', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'publishrecapmod',
        'name' => 'Publish Recap Moderator',
    ]);
    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Retro Month 2025',
        'slug' => 'retro-month-2025',
        'status' => World::STATUS_ARCHIVED,
        'is_active_campaign' => true,
        'is_homepage_featured' => true,
        'starts_at' => now()->subDays(35),
        'ends_at' => now()->subDays(7),
        'published_at' => now()->subDays(40),
        'accepts_submissions' => true,
        'community_section_enabled' => true,
    ]);

    $artwork = Artwork::factory()->for($moderator)->create([
        'title' => 'Retro Month Winner',
        'slug' => 'retro-month-winner-' . Str::lower(Str::random(6)),
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

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

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

    $this->actingAs($moderator)
        ->from(route('studio.worlds.edit', ['world' => $world->id]))
        ->post(route('studio.worlds.recap.publish', ['world' => $world]))
        ->assertRedirect(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertSessionHas('success', 'World recap published.');

    $world->refresh();

    expect($world->recap_status)->toBe(World::RECAP_STATUS_PUBLISHED);
    expect($world->recap_published_at)->not->toBeNull();
    expect($world->recap_stats_snapshot_json)->toBeArray();
    expect($world->is_active_campaign)->toBeFalse();
    expect($world->is_homepage_featured)->toBeFalse();
    expect(data_get($world->recap_stats_snapshot_json, 'summary.live_participations'))->toBeGreaterThanOrEqual(1);
    expect(data_get($world->recap_stats_snapshot_json, 'summary.reward_grants'))->toBeGreaterThanOrEqual(1);
});

it('includes portfolio analytics leaderboards on the studio worlds index', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'portfolioanalyticsmod',
        'name' => 'Portfolio Analytics Moderator',
    ]);
    $artwork = Artwork::factory()->for($moderator)->create([
        'title' => 'Portfolio Artwork',
        'slug' => 'portfolio-artwork',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);
    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Portfolio World',
        'slug' => 'portfolio-world',
        'status' => World::STATUS_PUBLISHED,
        'published_at' => now()->subDays(5),
    ]);

    WorldAnalyticsEvent::query()->create([
        'world_id' => $world->id,
        'world_slug' => $world->slug,
        'world_type' => $world->type,
        'recurrence_key' => $world->recurrence_key,
        'edition_year' => $world->edition_year,
        'event_type' => 'world_source_impression',
        'section_key' => 'spotlight',
        'source_surface' => 'homepage_spotlight',
        'source_detail' => 'primary',
        'viewer_type' => 'guest',
        'visitor_key' => hash('sha256', 'visitor:portfolio-impression'),
        'occurred_at' => now()->subHours(3),
    ]);

    WorldAnalyticsEvent::query()->create([
        'world_id' => $world->id,
        'world_slug' => $world->slug,
        'world_type' => $world->type,
        'recurrence_key' => $world->recurrence_key,
        'edition_year' => $world->edition_year,
        'event_type' => 'world_viewed',
        'source_surface' => 'homepage_spotlight',
        'source_detail' => 'primary',
        'viewer_type' => 'guest',
        'visitor_key' => hash('sha256', 'visitor:portfolio-viewer'),
        'occurred_at' => now()->subHours(2),
    ]);

    WorldAnalyticsEvent::query()->create([
        'world_id' => $world->id,
        'world_slug' => $world->slug,
        'world_type' => $world->type,
        'recurrence_key' => $world->recurrence_key,
        'edition_year' => $world->edition_year,
        'event_type' => 'world_source_clicked',
        'source_surface' => 'homepage_spotlight',
        'source_detail' => 'primary',
        'viewer_type' => 'guest',
        'visitor_key' => hash('sha256', 'visitor:portfolio-click'),
        'occurred_at' => now()->subHours(2),
    ]);

    WorldSubmission::query()->create([
        'world_id' => $world->id,
        'artwork_id' => $artwork->id,
        'submitted_by_user_id' => $moderator->id,
        'status' => WorldSubmission::STATUS_LIVE,
        'created_at' => now()->subHour(),
        'updated_at' => now()->subHour(),
    ]);

    WorldRewardGrant::query()->create([
        'user_id' => $moderator->id,
        'world_id' => $world->id,
        'artwork_id' => $artwork->id,
        'reward_type' => 'featured',
        'grant_source' => 'manual',
        'granted_at' => now()->subMinutes(30),
    ]);

    $this->actingAs($moderator)
        ->get(route('studio.worlds.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldsIndex')
            ->where('analytics.ranges.30d.summary.tracked_worlds', 1)
            ->where('analytics.ranges.30d.summary.promotion_impressions', 1)
            ->where('analytics.ranges.30d.leaderboards.views.0.world_id', $world->id)
            ->where('analytics.ranges.30d.leaderboards.submissions.0.world_id', $world->id)
            ->where('analytics.ranges.30d.leaderboards.conversion.0.world_id', $world->id));
});

it('searches artwork relations by creator and project context in the worlds picker', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'searchworldsmod',
        'name' => 'Search Worlds Moderator',
    ]);

    $creator = User::factory()->create([
        'username' => 'springartist',
        'name' => 'Spring Artist',
    ]);

    $group = Group::factory()->create([
        'name' => 'Spring Project',
        'slug' => 'spring-project',
    ]);

    $contentType = ContentType::query()->create([
        'name' => 'Pixel Art',
        'slug' => 'pixel-art',
        'description' => 'Pixel art content type',
    ]);

    $category = Category::query()->create([
        'content_type_id' => $contentType->id,
        'name' => 'Seasonal Spring',
        'slug' => 'seasonal-spring',
        'description' => 'Spring showcase',
        'is_active' => true,
        'sort_order' => 1,
    ]);

    $artwork = Artwork::factory()->for($creator)->create([
        'group_id' => $group->id,
        'title' => 'Morning Dew',
        'slug' => 'morning-dew',
        'description' => 'A calm scene with no direct spring keyword in the artwork copy.',
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
    ]);

    $artwork->categories()->attach($category->id);

    $this->actingAs($moderator)
        ->getJson(route('studio.worlds.entity-search', ['type' => 'artwork', 'q' => 'spring']))
        ->assertOk()
        ->assertJsonPath('items.0.id', $artwork->id)
        ->assertJsonPath('items.0.title', 'Morning Dew')
        ->assertJsonPath('items.0.subtitle', 'Spring Artist');
});

it('stores a world draft through the studio flow', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'editorworlds',
        'name' => 'Editor Worlds',
    ]);

    $response = $this->actingAs($moderator)->post(route('studio.worlds.store'), [
        'title' => 'Retro Month 2026',
        'slug' => 'retro-month-2026',
        'tagline' => 'Scanlines, diskmag culture, and old-school launches.',
        'summary' => 'A recurring world for retro platform activity.',
        'description' => 'World body copy',
        'theme_key' => 'retro-month',
        'status' => World::STATUS_DRAFT,
        'type' => World::TYPE_CAMPAIGN,
        'is_featured' => true,
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'recurrence_rule' => 'annual:04',
        'edition_year' => 2026,
        'cta_label' => 'Explore Retro Month',
        'cta_url' => 'https://skinbase.test/worlds/retro-month-2026',
        'badge_label' => 'Editorial pick',
        'badge_description' => 'Featured by the Nova editorial team.',
        'badge_url' => 'https://skinbase.test/badges/retro',
        'seo_title' => 'Retro Month 2026 - Skinbase',
        'seo_description' => 'Retro Month seasonal campaign',
        'published_at' => '2026-03-20T10:00',
        'related_tags_json' => ['retro', 'demoscene'],
        'section_order_json' => ['featured_artworks', 'featured_collections', 'news'],
        'section_visibility_json' => [
            'featured_artworks' => true,
            'featured_collections' => true,
            'featured_creators' => false,
            'featured_groups' => false,
            'news' => true,
            'challenge' => false,
            'events' => false,
            'releases' => false,
            'cards' => false,
        ],
        'relations' => [],
    ]);

    $world = World::query()->where('slug', 'retro-month-2026')->firstOrFail();

    $response->assertRedirect(route('studio.worlds.edit', ['world' => $world->id]));

    $this->assertDatabaseHas('worlds', [
        'id' => $world->id,
        'title' => 'Retro Month 2026',
        'slug' => 'retro-month-2026',
        'status' => World::STATUS_DRAFT,
        'type' => World::TYPE_CAMPAIGN,
        'is_featured' => true,
        'is_recurring' => true,
        'recurrence_key' => 'retro-month',
        'edition_year' => 2026,
        'published_at' => '2026-03-20 10:00:00',
        'created_by_user_id' => $moderator->id,
    ]);

    expect($world->fresh()->section_visibility_json)->toMatchArray([
        'featured_artworks' => true,
        'featured_collections' => true,
        'featured_creators' => false,
        'news' => true,
    ]);
});

it('stores hidden linked challenge entries and exposes them back in the editor payload', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'challengeoverridemod',
        'name' => 'Challenge Override Moderator',
    ]);
    $groupOwner = User::factory()->create([
        'username' => 'challengeoverrideowner',
    ]);
    $group = Group::factory()->for($groupOwner, 'owner')->create();
    $hiddenEntry = Artwork::factory()->for($groupOwner)->create([
        'group_id' => $group->id,
        'title' => 'Hide Me',
        'slug' => 'hide-me',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);
    $visibleEntry = Artwork::factory()->for($groupOwner)->create([
        'group_id' => $group->id,
        'title' => 'Keep Me Live',
        'slug' => 'keep-me-live',
        'artwork_status' => 'published',
        'published_at' => now()->subDay(),
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
    ]);

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Studio Override Challenge',
        'slug' => 'studio-override-challenge',
        '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()->addDays(2),
        'created_by_user_id' => $groupOwner->id,
    ]);

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

    $response = $this->actingAs($moderator)->post(route('studio.worlds.store'), [
        'title' => 'Challenge Override World',
        'slug' => 'challenge-override-world',
        'summary' => 'World summary',
        'description' => 'World description',
        'status' => World::STATUS_DRAFT,
        'type' => World::TYPE_CAMPAIGN,
        'linked_challenge_id' => $challenge->id,
        'hidden_linked_challenge_artwork_ids_json' => [$hiddenEntry->id],
        'relations' => [],
    ]);

    $world = World::query()->where('slug', 'challenge-override-world')->firstOrFail();

    $response->assertRedirect(route('studio.worlds.edit', ['world' => $world->id]));

    expect($world->fresh()->hidden_linked_challenge_artwork_ids_json)->toBe([$hiddenEntry->id]);

    $this->actingAs($moderator)
        ->get(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldEditor')
            ->where('world.hidden_linked_challenge_artwork_ids_json.0', $hiddenEntry->id)
            ->where('world.linked_challenge.entry_preview_items.0.title', 'Hide Me')
            ->where('world.linked_challenge.entry_preview_items.1.title', 'Keep Me Live'));
});

it('rejects archived and time-mismatched linked challenges for published worlds', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'linkedchallengevalidationmod',
        'name' => 'Linked Challenge Validation Moderator',
    ]);
    $groupOwner = User::factory()->create([
        'username' => 'linkedchallengevalidationowner',
    ]);
    $group = Group::factory()->for($groupOwner, 'owner')->create();

    $archivedChallenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Archived Challenge',
        'slug' => 'archived-challenge',
        'summary' => 'Archived summary',
        'description' => 'Archived description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ARCHIVED,
        'start_at' => now()->subDays(10),
        'end_at' => now()->subDays(5),
        'created_by_user_id' => $groupOwner->id,
    ]);

    $this->actingAs($moderator)
        ->from(route('studio.worlds.create'))
        ->post(route('studio.worlds.store'), [
            'title' => 'Live World With Archived Challenge',
            'slug' => 'live-world-with-archived-challenge',
            'summary' => 'World summary',
            'description' => 'World description',
            'status' => World::STATUS_PUBLISHED,
            'type' => World::TYPE_CAMPAIGN,
            'starts_at' => now()->subDay()->format('Y-m-d H:i:s'),
            'ends_at' => now()->addDays(5)->format('Y-m-d H:i:s'),
            'linked_challenge_id' => $archivedChallenge->id,
            'relations' => [],
        ])
        ->assertRedirect(route('studio.worlds.create'))
        ->assertSessionHasErrors(['linked_challenge_id']);

    $activeMismatchChallenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Mismatched Challenge',
        'slug' => 'mismatched-challenge',
        'summary' => 'Mismatch summary',
        'description' => 'Mismatch description',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'start_at' => now()->addDays(30),
        'end_at' => now()->addDays(40),
        'created_by_user_id' => $groupOwner->id,
    ]);

    $this->actingAs($moderator)
        ->from(route('studio.worlds.create'))
        ->post(route('studio.worlds.store'), [
            'title' => 'Live World With Mismatched Challenge',
            'slug' => 'live-world-with-mismatched-challenge',
            'summary' => 'World summary',
            'description' => 'World description',
            'status' => World::STATUS_PUBLISHED,
            'type' => World::TYPE_CAMPAIGN,
            'starts_at' => now()->subDay()->format('Y-m-d H:i:s'),
            'ends_at' => now()->addDays(5)->format('Y-m-d H:i:s'),
            'linked_challenge_id' => $activeMismatchChallenge->id,
            'relations' => [],
        ])
        ->assertRedirect(route('studio.worlds.create'))
        ->assertSessionHasErrors(['linked_challenge_id']);
});

it('rejects reserved world slugs in the studio flow', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'reservedslugmod',
        'name' => 'Reserved Slug Moderator',
    ]);

    $this->actingAs($moderator)
        ->from(route('studio.worlds.create'))
        ->post(route('studio.worlds.store'), [
            'title' => 'Create',
            'slug' => 'create',
            'summary' => 'Reserved slug attempt',
            'description' => 'Should fail validation',
            'theme_key' => 'retro-month',
            'status' => World::STATUS_DRAFT,
            'type' => World::TYPE_CAMPAIGN,
            'relations' => [],
        ])
        ->assertRedirect(route('studio.worlds.create'))
        ->assertSessionHasErrors(['slug']);
});

it('requires recurrence metadata and blocks duplicate recurrence editions', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'recurrencemod',
        'name' => 'Recurrence Moderator',
    ]);

    studioWorld([
        'creator' => $moderator,
        'title' => 'Halloween World 2026',
        'slug' => 'halloween-world-2026-existing',
        'is_recurring' => true,
        'recurrence_key' => 'halloween',
        'edition_year' => 2026,
    ]);

    $this->actingAs($moderator)
        ->from(route('studio.worlds.create'))
        ->post(route('studio.worlds.store'), [
            'title' => 'Recurring World Without Metadata',
            'status' => World::STATUS_DRAFT,
            'type' => World::TYPE_CAMPAIGN,
            'is_recurring' => true,
            'relations' => [],
        ])
        ->assertRedirect(route('studio.worlds.create'))
        ->assertSessionHasErrors(['recurrence_key', 'edition_year']);

    $this->actingAs($moderator)
        ->from(route('studio.worlds.create'))
        ->post(route('studio.worlds.store'), [
            'title' => 'Halloween World Clone',
            'slug' => 'halloween-world-clone',
            'status' => World::STATUS_DRAFT,
            'type' => World::TYPE_SEASONAL,
            'is_recurring' => true,
            'recurrence_key' => 'halloween',
            'edition_year' => 2026,
            'relations' => [],
        ])
        ->assertRedirect(route('studio.worlds.create'))
        ->assertSessionHasErrors(['edition_year']);

    studioWorld([
        'creator' => $moderator,
        'title' => 'Halloween World Current',
        'slug' => 'halloween-world-current',
        'status' => World::STATUS_PUBLISHED,
        'is_recurring' => true,
        'recurrence_key' => 'halloween',
        'edition_year' => 2027,
        'starts_at' => now()->subDay(),
        'ends_at' => now()->addDays(5),
        'published_at' => now()->subDays(2),
    ]);

    $this->actingAs($moderator)
        ->from(route('studio.worlds.create'))
        ->post(route('studio.worlds.store'), [
            'title' => 'Halloween World Duplicate Current',
            'slug' => 'halloween-world-duplicate-current',
            'status' => World::STATUS_PUBLISHED,
            'type' => World::TYPE_SEASONAL,
            'is_recurring' => true,
            'recurrence_key' => 'halloween',
            'edition_year' => 2028,
            'starts_at' => now()->subHour(),
            'ends_at' => now()->addDays(7),
            'relations' => [],
        ])
        ->assertRedirect(route('studio.worlds.create'))
        ->assertSessionHasErrors(['status']);
});

it('duplicates worlds and preserves editorial structure in a new draft', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'duplicateworldmod',
        'name' => 'Duplicate World Moderator',
    ]);

    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Pixel Week 2026',
        'slug' => 'pixel-week-2026',
        'theme_key' => 'pixel-week',
        'section_visibility_json' => [
            'featured_artworks' => true,
            'featured_collections' => false,
            'events' => true,
        ],
        'starts_at' => Carbon::parse('2026-07-01 09:00:00'),
        'published_at' => Carbon::parse('2026-06-28 18:00:00'),
        'recap_status' => World::RECAP_STATUS_PUBLISHED,
        'recap_title' => 'Pixel Week 2026 recap',
        'recap_summary' => 'Archived summary',
        'recap_intro' => '<p>Archived intro</p>',
        'recap_editor_note' => 'Reset this note on duplicate.',
        'recap_cover_path' => 'worlds/recaps/pixel-week-2026-cover.jpg',
        'recap_published_at' => Carbon::parse('2026-07-11 10:00:00'),
    ]);

    $world->worldRelations()->create([
        'section_key' => 'featured_creators',
        'related_type' => 'user',
        'related_id' => $moderator->id,
        'context_label' => 'Lead pixel artist',
        'sort_order' => 0,
        'is_featured' => true,
    ]);

    $response = $this->actingAs($moderator)->post(route('studio.worlds.duplicate', ['world' => $world->id]));

    $duplicate = World::query()->where('slug', 'like', 'pixel-week-2026-copy%')->latest('id')->firstOrFail();

    $response->assertRedirect(route('studio.worlds.edit', ['world' => $duplicate->id]));

    expect($duplicate->status)->toBe(World::STATUS_DRAFT);
    expect($duplicate->is_featured)->toBeFalse();
    expect($duplicate->starts_at)->toBeNull();
    expect($duplicate->published_at)->toBeNull();
    expect($duplicate->recap_status)->toBe(World::RECAP_STATUS_DRAFT);
    expect($duplicate->recap_title)->toBeNull();
    expect($duplicate->recap_editor_note)->toBeNull();
    expect($duplicate->recap_cover_path)->toBeNull();
    expect($duplicate->recap_published_at)->toBeNull();
    expect($duplicate->section_visibility_json)->toMatchArray([
        'featured_artworks' => true,
        'featured_collections' => false,
        'events' => true,
    ]);
    expect($duplicate->worldRelations()->count())->toBe(1);
});

it('can duplicate a world as a structural shell without copying curated relations', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'structuralworldmod',
        'name' => 'Structural World Moderator',
    ]);

    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Spring Vibes 2026',
        'slug' => 'spring-vibes-2026',
        'is_recurring' => true,
        'recurrence_key' => 'spring-vibes',
        'edition_year' => 2026,
    ]);

    $world->worldRelations()->create([
        'section_key' => 'featured_creators',
        'related_type' => 'user',
        'related_id' => $moderator->id,
        'context_label' => 'Spring lead',
        'sort_order' => 0,
        'is_featured' => true,
    ]);

    $response = $this->actingAs($moderator)->post(route('studio.worlds.duplicate', ['world' => $world->id]), [
        'copy_mode' => 'structure_only',
    ]);

    $duplicate = World::query()->where('slug', 'like', 'spring-vibes-2026-copy%')->latest('id')->firstOrFail();

    $response->assertRedirect(route('studio.worlds.edit', ['world' => $duplicate->id]));
    expect($duplicate->is_recurring)->toBeFalse();
    expect($duplicate->recurrence_key)->toBeNull();
    expect($duplicate->edition_year)->toBeNull();
    expect($duplicate->worldRelations()->count())->toBe(0);
});

it('creates the next edition draft for recurring worlds', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'editionworldmod',
        'name' => 'Edition World Moderator',
    ]);

    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Halloween 2026',
        'slug' => 'halloween-2026',
        'is_recurring' => true,
        'recurrence_key' => 'halloween',
        'edition_year' => 2026,
        'submission_starts_at' => Carbon::parse('2026-09-15 00:00:00'),
        'submission_ends_at' => Carbon::parse('2026-10-30 00:00:00'),
        'cta_url' => 'https://skinbase.test/worlds/halloween-2026',
        'badge_url' => 'https://skinbase.test/badges/halloween-2026',
        'recap_status' => World::RECAP_STATUS_PUBLISHED,
        'recap_title' => 'Halloween 2026 recap',
        'recap_editor_note' => 'Clear this note for the next edition.',
        'recap_cover_path' => 'worlds/recaps/halloween-2026-cover.jpg',
        'recap_published_at' => Carbon::parse('2026-11-01 12:00:00'),
    ]);

    $response = $this->actingAs($moderator)->post(route('studio.worlds.new-edition', ['world' => $world->id]));

    $edition = World::query()->where('recurrence_key', 'halloween')->where('edition_year', 2027)->latest('id')->firstOrFail();

    $response->assertRedirect(route('studio.worlds.edit', ['world' => $edition->id]));
    expect($edition->parent_world_id)->toBe($world->id);
    expect($edition->status)->toBe(World::STATUS_DRAFT);
    expect($edition->is_recurring)->toBeTrue();
    expect($edition->slug)->toContain('2027');
    expect($edition->submission_starts_at)->toBeNull();
    expect($edition->submission_ends_at)->toBeNull();
    expect($edition->cta_url)->toBeNull();
    expect($edition->badge_url)->toBeNull();
    expect($edition->recap_status)->toBe(World::RECAP_STATUS_DRAFT);
    expect($edition->recap_title)->toBeNull();
    expect($edition->recap_editor_note)->toBeNull();
    expect($edition->recap_cover_path)->toBeNull();
    expect($edition->recap_published_at)->toBeNull();
});

it('shows recurring family context in the studio editor for recurring worlds', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'familycontextmod',
        'name' => 'Family Context Moderator',
    ]);

    studioWorld([
        'creator' => $moderator,
        'title' => 'Pixel Week 2025',
        'slug' => 'pixel-week-2025',
        'status' => World::STATUS_ARCHIVED,
        'is_recurring' => true,
        'recurrence_key' => 'pixel-week',
        'edition_year' => 2025,
        'starts_at' => Carbon::parse('2025-07-01 00:00:00'),
        'ends_at' => Carbon::parse('2025-07-10 00:00:00'),
        'published_at' => Carbon::parse('2025-06-28 18:00:00'),
    ]);

    $world = studioWorld([
        'creator' => $moderator,
        'title' => 'Pixel Week 2026',
        'slug' => 'pixel-week-2026',
        'status' => World::STATUS_PUBLISHED,
        'is_recurring' => true,
        'recurrence_key' => 'pixel-week',
        'edition_year' => 2026,
        'starts_at' => now()->subDay(),
        'ends_at' => now()->addDays(7),
        'published_at' => now()->subDays(2),
    ]);

    $this->actingAs($moderator)
        ->get(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioWorldEditor')
            ->where('world.family_title', 'Pixel Week')
            ->where('world.is_canonical_edition', true)
            ->where('world.family_edition_count', 2)
            ->where('world.previous_edition.title', 'Pixel Week 2025'));
});

it('surfaces editorial suggestions across the expected world content categories', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'suggestionsmod',
        'name' => 'Suggestions Moderator',
    ]);

    $fixture = studioWorldSuggestionFixture($moderator);
    $world = $fixture['world'];
    $communityArtwork = $fixture['community_artwork'];

    foreach (range(1, 4) as $index) {
        WorldAnalyticsEvent::query()->create([
            'world_id' => $world->id,
            'event_type' => WorldAnalyticsService::EVENT_ENTITY_CLICKED,
            'world_slug' => $world->slug,
            'world_type' => $world->type,
            'recurrence_key' => $world->recurrence_key,
            'edition_year' => $world->edition_year,
            'section_key' => 'community_submissions',
            'entity_type' => WorldRelation::TYPE_ARTWORK,
            'entity_id' => $communityArtwork->id,
            'entity_title' => $communityArtwork->title,
            'viewer_type' => 'user',
            'user_id' => $moderator->id,
            'visitor_key' => hash('sha256', 'suggestion-analytics-' . $index),
            'occurred_at' => now()->subMinutes($index),
        ]);
    }

    $response = $this->actingAs($moderator)
        ->get(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertOk();

    $suggestions = data_get($response->viewData('page'), 'props.suggestions');
    $groups = collect((array) data_get($suggestions, 'groups'))->keyBy('key');

    expect(data_get($suggestions, 'enabled'))->toBeTrue();
    expect(data_get($suggestions, 'summary.has_linked_challenge'))->toBeTrue();
    expect(data_get($suggestions, 'summary.world_is_recurring'))->toBeTrue();
    expect(data_get($suggestions, 'summary.family_signal_count'))->toBeGreaterThanOrEqual(1);
    expect(data_get($suggestions, 'summary.community_submission_count'))->toBe(1);
    expect(data_get($suggestions, 'summary.analytics_signal_count'))->toBeGreaterThanOrEqual(1);
    expect(collect((array) data_get($suggestions, 'filters.sort_options'))->pluck('value')->all())
        ->toContain('relevance', 'newest', 'performance');

    expect(data_get($groups->get('challenge'), 'items.0.title'))->toBe('Retro Circuit Finalist');
    expect(collect((array) data_get($groups->get('challenge'), 'items.0.reasons'))->pluck('label')->all())
        ->toContain('Challenge finalist');

    expect(data_get($groups->get('community'), 'items.0.title'))->toBe('Retro Skyline Community Entry');
    expect(collect((array) data_get($groups->get('community'), 'items.0.reasons'))->pluck('label')->all())
        ->toContain('Already a featured community submission', 'Top-clicked in this world');
    expect(data_get($groups->get('community'), 'items.0.signals.analytics_informed'))->toBeTrue();

    expect(data_get($groups->get('artworks'), 'items.0.title'))->toBe('Neon Drift Poster');
    expect(collect((array) data_get($groups->get('artworks'), 'items.0.reasons'))->pluck('label')->all())
        ->toContain('Matches world tags');

    expect(collect((array) data_get($groups->get('creators'), 'items'))->contains(function (array $item): bool {
        return (int) ($item['id'] ?? 0) > 0
            && (string) ($item['title'] ?? '') === 'Retro Captain'
            && collect((array) ($item['reasons'] ?? []))->pluck('label')->contains('Strong in this world family');
    }))->toBeTrue();

    expect(data_get($groups->get('collections'), 'items.0.title'))->toBe('Retro Signal Collection');
    expect(data_get($groups->get('groups'), 'items.0.title'))->toBe('Retro Signal Crew');
    expect(data_get($groups->get('news'), 'items.0.title'))->toBe('Retro Month results roundup');
});

it('stores suggestion feedback state and converts suggestions into world relations', function (): void {
    $moderator = User::factory()->create([
        'role' => 'moderator',
        'username' => 'suggestionactionsmod',
        'name' => 'Suggestion Actions Moderator',
    ]);

    $fixture = studioWorldSuggestionFixture($moderator);
    $world = $fixture['world'];
    $artwork = $fixture['artwork_suggestion'];
    $collection = $fixture['collection'];
    $group = $fixture['group'];

    $this->actingAs($moderator)
        ->postJson(route('studio.worlds.suggestions.pin', ['world' => $world->id]), [
            'related_type' => WorldRelation::TYPE_ARTWORK,
            'related_id' => $artwork->id,
            'section_key' => 'featured_artworks',
        ])
        ->assertOk()
        ->assertJsonPath('state.status', WorldEditorialSuggestionState::STATUS_PINNED)
        ->assertJsonPath('state.section_key', 'featured_artworks');

    $this->assertDatabaseHas('world_editorial_suggestion_states', [
        'world_id' => $world->id,
        'related_type' => WorldRelation::TYPE_ARTWORK,
        'related_id' => $artwork->id,
        'status' => WorldEditorialSuggestionState::STATUS_PINNED,
        'section_key' => 'featured_artworks',
    ]);

    $this->actingAs($moderator)
        ->postJson(route('studio.worlds.suggestions.dismiss', ['world' => $world->id]), [
            'related_type' => WorldRelation::TYPE_COLLECTION,
            'related_id' => $collection->id,
        ])
        ->assertOk()
        ->assertJsonPath('state.status', WorldEditorialSuggestionState::STATUS_DISMISSED);

    $this->assertDatabaseHas('world_editorial_suggestion_states', [
        'world_id' => $world->id,
        'related_type' => WorldRelation::TYPE_COLLECTION,
        'related_id' => $collection->id,
        'status' => WorldEditorialSuggestionState::STATUS_DISMISSED,
    ]);

    $this->actingAs($moderator)
        ->postJson(route('studio.worlds.suggestions.not-relevant', ['world' => $world->id]), [
            'related_type' => WorldRelation::TYPE_GROUP,
            'related_id' => $group->id,
        ])
        ->assertOk()
        ->assertJsonPath('state.status', WorldEditorialSuggestionState::STATUS_NOT_RELEVANT);

    $this->assertDatabaseHas('world_editorial_suggestion_states', [
        'world_id' => $world->id,
        'related_type' => WorldRelation::TYPE_GROUP,
        'related_id' => $group->id,
        'status' => WorldEditorialSuggestionState::STATUS_NOT_RELEVANT,
    ]);

    $response = $this->actingAs($moderator)
        ->get(route('studio.worlds.edit', ['world' => $world->id]))
        ->assertOk();

    $suggestions = data_get($response->viewData('page'), 'props.suggestions');

    expect(data_get($suggestions, 'summary.pinned_count'))->toBe(1);
    expect(data_get($suggestions, 'summary.suppressed_count'))->toBe(2);
    expect(collect((array) data_get($suggestions, 'pinned_items'))->pluck('key')->all())
        ->toContain('artwork:' . $artwork->id);

    $this->actingAs($moderator)
        ->postJson(route('studio.worlds.suggestions.restore', ['world' => $world->id]), [
            'related_type' => WorldRelation::TYPE_COLLECTION,
            'related_id' => $collection->id,
        ])
        ->assertOk();

    $this->assertDatabaseMissing('world_editorial_suggestion_states', [
        'world_id' => $world->id,
        'related_type' => WorldRelation::TYPE_COLLECTION,
        'related_id' => $collection->id,
    ]);

    $this->actingAs($moderator)
        ->postJson(route('studio.worlds.suggestions.add', ['world' => $world->id]), [
            'related_type' => WorldRelation::TYPE_ARTWORK,
            'related_id' => $artwork->id,
            'section_key' => 'featured_artworks',
            'is_featured' => true,
        ])
        ->assertOk()
        ->assertJsonPath('already_attached', false)
        ->assertJsonPath('relation.related_type', WorldRelation::TYPE_ARTWORK)
        ->assertJsonPath('relation.related_id', $artwork->id)
        ->assertJsonPath('relation.section_key', 'featured_artworks')
        ->assertJsonPath('relation.is_featured', true);

    $this->assertDatabaseHas('world_relations', [
        'world_id' => $world->id,
        'related_type' => WorldRelation::TYPE_ARTWORK,
        'related_id' => $artwork->id,
        'section_key' => 'featured_artworks',
        'is_featured' => 1,
    ]);

    $this->assertDatabaseMissing('world_editorial_suggestion_states', [
        'world_id' => $world->id,
        'related_type' => WorldRelation::TYPE_ARTWORK,
        'related_id' => $artwork->id,
    ]);
});