<?php

use App\Events\Collections\CollectionArtworkAttached;
use App\Events\Collections\CollectionArtworkRemoved;
use App\Events\Collections\CollectionCreated;
use App\Events\Collections\CollectionDeleted;
use App\Events\Collections\CollectionUpdated;
use App\Events\Collections\CollectionViewed;
use App\Models\Artwork;
use App\Models\Category;
use App\Models\Collection;
use App\Models\CollectionSurfaceDefinition;
use App\Models\CollectionSurfacePlacement;
use App\Models\Report;
use App\Models\ContentType;
use App\Models\Tag;
use App\Models\User;
use App\Services\CollectionService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;

it('owner can create collections and duplicate titles get unique per-user slugs', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Dark Fantasy Series',
            'slug' => '',
            'description' => 'First curated set.',
            'visibility' => 'public',
            'sort_mode' => 'manual',
        ])
        ->assertOk();

    $this->actingAs($user)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Dark Fantasy Series',
            'slug' => '',
            'description' => 'Second curated set.',
            'visibility' => 'public',
            'sort_mode' => 'manual',
        ])
        ->assertOk();

    expect(Collection::query()->where('user_id', $user->id)->orderBy('id')->pluck('slug')->all())
        ->toBe(['dark-fantasy-series', 'dark-fantasy-series-2']);
});

it('dispatches collection lifecycle events across owner actions and public views', function () {
    Event::fake([
        CollectionCreated::class,
        CollectionUpdated::class,
        CollectionDeleted::class,
        CollectionArtworkAttached::class,
        CollectionArtworkRemoved::class,
        CollectionViewed::class,
    ]);

    $user = User::factory()->create(['username' => 'eventowner']);
    $artwork = Artwork::factory()->for($user)->create();

    $createResponse = $this->actingAs($user)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Event Driven Showcase',
            'slug' => '',
            'description' => 'Tracking collection lifecycle events.',
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
        ])
        ->assertOk();

    $collectionId = (int) $createResponse->json('collection.id');
    $collection = Collection::query()->findOrFail($collectionId);

    Event::assertDispatched(CollectionCreated::class, fn (CollectionCreated $event) => (int) $event->collection->id === $collectionId);

    $this->actingAs($user)
        ->patchJson(route('settings.collections.update', ['collection' => $collectionId]), [
            'title' => 'Event Driven Showcase Updated',
            'slug' => 'event-driven-showcase-updated',
            'description' => 'Updated description.',
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
            'cover_artwork_id' => null,
        ])
        ->assertOk();

    Event::assertDispatched(CollectionUpdated::class, fn (CollectionUpdated $event) => (int) $event->collection->id === $collectionId);

    $this->actingAs($user)
        ->postJson(route('settings.collections.artworks.attach', ['collection' => $collectionId]), [
            'artwork_ids' => [$artwork->id],
        ])
        ->assertOk();

    Event::assertDispatched(CollectionArtworkAttached::class, fn (CollectionArtworkAttached $event) => (int) $event->collection->id === $collectionId && $event->artworkIds === [$artwork->id]);

    $this->actingAs($user)
        ->deleteJson(route('settings.collections.artworks.remove', ['collection' => $collectionId, 'artwork' => $artwork->id]))
        ->assertOk();

    Event::assertDispatched(CollectionArtworkRemoved::class, fn (CollectionArtworkRemoved $event) => (int) $event->collection->id === $collectionId && (int) $event->artworkId === (int) $artwork->id);

    $this->get(route('profile.collections.show', ['username' => $user->username, 'slug' => 'event-driven-showcase-updated']))
        ->assertOk();

    Event::assertDispatched(CollectionViewed::class, fn (CollectionViewed $event) => (int) $event->collection->id === $collectionId && (int) $event->viewerId === (int) $user->id);

    $this->actingAs($user)
        ->deleteJson(route('settings.collections.destroy', ['collection' => $collectionId]))
        ->assertOk();

    Event::assertDispatched(CollectionDeleted::class, fn (CollectionDeleted $event) => (int) $event->collection->id === $collectionId);
});

it('owner can edit a collection', function () {
    $user = User::factory()->create();
    $collection = Collection::factory()->for($user)->create([
        'title' => 'Original Title',
        'slug' => 'original-title',
    ]);

    $this->actingAs($user)
        ->patchJson(route('settings.collections.update', ['collection' => $collection->id]), [
            'title' => 'Featured 2026 Works',
            'slug' => 'featured-2026-works',
            'description' => 'Updated description.',
            'visibility' => 'unlisted',
            'sort_mode' => 'manual',
            'cover_artwork_id' => null,
        ])
        ->assertOk()
        ->assertJsonPath('collection.title', 'Featured 2026 Works');

    $collection->refresh();

    expect($collection->title)->toBe('Featured 2026 Works');
    expect($collection->slug)->toBe('featured-2026-works');
    expect($collection->visibility)->toBe('unlisted');
});

it('owner can persist campaign banner metadata and public page renders it', function () {
    $user = User::factory()->create(['username' => 'campaigncurator']);

    $response = $this->actingAs($user)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Spring Frontpage Spotlight',
            'slug' => '',
            'description' => 'A seasonal campaign collection.',
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
            'event_key' => 'spring-2026',
            'event_label' => 'Spring 2026',
            'season_key' => 'spring',
            'banner_text' => 'Fresh editorial picks for the new season.',
            'badge_label' => 'Seasonal Spotlight',
            'spotlight_style' => Collection::SPOTLIGHT_STYLE_SEASONAL,
        ])
        ->assertOk()
        ->assertJsonPath('collection.event_key', 'spring-2026')
        ->assertJsonPath('collection.season_key', 'spring')
        ->assertJsonPath('collection.banner_text', 'Fresh editorial picks for the new season.')
        ->assertJsonPath('collection.spotlight_style', Collection::SPOTLIGHT_STYLE_SEASONAL);

    $collectionId = (int) $response->json('collection.id');
    $collection = Collection::query()->findOrFail($collectionId);

    expect($collection->event_key)->toBe('spring-2026');
    expect($collection->season_key)->toBe('spring');
    expect($collection->banner_text)->toBe('Fresh editorial picks for the new season.');
    expect($collection->spotlight_style)->toBe(Collection::SPOTLIGHT_STYLE_SEASONAL);

    $this->get(route('profile.collections.show', ['username' => $user->username, 'slug' => $collection->slug]))
        ->assertOk()
        ->assertSee('Fresh editorial picks for the new season.', false)
        ->assertSee('Spring 2026', false)
        ->assertSee('Seasonal Spotlight', false);
});

it('staff can create a collection surface placement with v4 placement fields', function () {
    $admin = User::factory()->create(['role' => 'admin']);
    $owner = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Homepage Editorial Placement',
        'slug' => 'homepage-editorial-placement',
    ]);

    $this->actingAs($admin)
        ->postJson(route('settings.collections.surfaces.placements.store'), [
            'surface_key' => 'homepage-editorial',
            'collection_id' => $collection->id,
            'placement_type' => 'campaign',
            'priority' => 8,
            'campaign_key' => 'spring-homepage',
            'notes' => 'Pinned during the spring editorial campaign.',
            'is_active' => true,
        ])
        ->assertOk()
        ->assertJsonPath('placement.surface_key', 'homepage-editorial')
        ->assertJsonPath('placement.placement_type', 'campaign')
        ->assertJsonPath('placement.campaign_key', 'spring-homepage');

    $this->assertDatabaseHas('collection_surface_placements', [
        'surface_key' => 'homepage-editorial',
        'collection_id' => $collection->id,
        'placement_type' => 'campaign',
        'campaign_key' => 'spring-homepage',
        'created_by_user_id' => $admin->id,
    ]);

    $this->assertDatabaseHas('collection_history', [
        'collection_id' => $collection->id,
        'actor_user_id' => $admin->id,
        'action_type' => 'placement_assigned',
    ]);
});

it('staff can update collection surface definitions and placements', function () {
    $admin = User::factory()->create(['role' => 'admin']);
    $owner = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Campaign Primary Collection',
        'slug' => 'campaign-primary-collection',
    ]);

    $definition = CollectionSurfaceDefinition::query()->create([
        'surface_key' => 'discover-hero',
        'title' => 'Discover Hero',
        'description' => 'Original hero surface.',
        'mode' => 'manual',
        'rules_json' => ['campaign_key' => 'winter'],
        'ranking_mode' => 'ranking_score',
        'max_items' => 6,
        'is_active' => true,
    ]);

    $placement = CollectionSurfacePlacement::query()->create([
        'collection_id' => $collection->id,
        'surface_key' => 'discover-hero',
        'placement_type' => 'manual',
        'priority' => 2,
        'starts_at' => now()->subDay(),
        'ends_at' => now()->addDay(),
        'is_active' => true,
        'campaign_key' => 'winter',
        'notes' => 'Original placement note.',
        'created_by_user_id' => $admin->id,
    ]);

    $this->actingAs($admin)
        ->patchJson(route('settings.collections.surfaces.definitions.update', ['definition' => $definition->id]), [
            'surface_key' => 'ignored-by-server',
            'title' => 'Discover Hero Updated',
            'description' => 'Updated hero surface.',
            'mode' => 'automatic',
            'ranking_mode' => 'recent_activity',
            'max_items' => 10,
            'is_active' => false,
            'starts_at' => now()->subDay()->toISOString(),
            'ends_at' => now()->addDays(2)->toISOString(),
            'fallback_surface_key' => 'homepage.featured_collections',
            'rules_json' => [
                'campaign_key' => 'spring',
                'featured_only' => true,
            ],
        ])
        ->assertOk()
        ->assertJsonPath('definition.surface_key', 'discover-hero')
        ->assertJsonPath('definition.title', 'Discover Hero Updated')
        ->assertJsonPath('definition.mode', 'automatic')
        ->assertJsonPath('definition.ranking_mode', 'recent_activity')
        ->assertJsonPath('definition.max_items', 10)
        ->assertJsonPath('definition.is_active', false)
        ->assertJsonPath('definition.fallback_surface_key', 'homepage.featured_collections')
        ->assertJsonPath('definition.rules_json.campaign_key', 'spring');

    $this->actingAs($admin)
        ->patchJson(route('settings.collections.surfaces.placements.update', ['placement' => $placement->id]), [
            'surface_key' => 'discover-hero',
            'collection_id' => $collection->id,
            'placement_type' => 'scheduled_override',
            'priority' => 11,
            'starts_at' => now()->toISOString(),
            'ends_at' => now()->addDays(3)->toISOString(),
            'is_active' => false,
            'campaign_key' => 'spring',
            'notes' => 'Updated placement note.',
        ])
        ->assertOk()
        ->assertJsonPath('placement.id', $placement->id)
        ->assertJsonPath('placement.placement_type', 'scheduled_override')
        ->assertJsonPath('placement.priority', 11)
        ->assertJsonPath('placement.is_active', false)
        ->assertJsonPath('placement.campaign_key', 'spring')
        ->assertJsonPath('placement.notes', 'Updated placement note.');

    $this->assertDatabaseHas('collection_surface_definitions', [
        'id' => $definition->id,
        'surface_key' => 'discover-hero',
        'title' => 'Discover Hero Updated',
        'mode' => 'automatic',
        'ranking_mode' => 'recent_activity',
        'max_items' => 10,
        'is_active' => 0,
        'fallback_surface_key' => 'homepage.featured_collections',
    ]);

    $this->assertDatabaseHas('collection_surface_placements', [
        'id' => $placement->id,
        'surface_key' => 'discover-hero',
        'collection_id' => $collection->id,
        'placement_type' => 'scheduled_override',
        'priority' => 11,
        'campaign_key' => 'spring',
        'notes' => 'Updated placement note.',
        'is_active' => 0,
        'created_by_user_id' => $admin->id,
    ]);

    $this->assertDatabaseHas('collection_history', [
        'collection_id' => $collection->id,
        'actor_user_id' => $admin->id,
        'action_type' => 'placement_updated',
    ]);
});

it('staff can preview batch editorial campaign and placement changes without persisting them', function () {
    $admin = User::factory()->create(['role' => 'admin']);
    $owner = User::factory()->create();
    $eligible = Collection::factory()->for($owner)->create([
        'title' => 'Batch Preview Eligible',
        'slug' => 'batch-preview-eligible',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);
    $ineligible = Collection::factory()->for($owner)->create([
        'title' => 'Batch Preview Ineligible',
        'slug' => 'batch-preview-ineligible',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($admin)
        ->postJson(route('settings.collections.surfaces.batch-editorial'), [
            'collection_ids' => [$eligible->id, $ineligible->id],
            'campaign_key' => 'summer-edit',
            'campaign_label' => 'Summer Edit',
            'editorial_notes' => 'Preview run for homepage campaign planning.',
            'surface_key' => 'homepage-editorial',
            'placement_type' => 'campaign',
            'priority' => 7,
            'apply' => false,
        ])
        ->assertOk()
        ->assertJsonPath('mode', 'preview')
        ->assertJsonPath('plan.collections_count', 2)
        ->assertJsonPath('plan.placement_eligible_count', 1)
        ->assertJsonPath('plan.items.0.campaign_updates.campaign_key', 'summer-edit');

    expect(Collection::query()->findOrFail($eligible->id)->campaign_key)->toBeNull();
    expect(CollectionSurfacePlacement::query()->where('collection_id', $eligible->id)->exists())->toBeFalse();
});

it('staff can apply batch editorial campaign and placement changes', function () {
    $admin = User::factory()->create(['role' => 'admin']);
    $owner = User::factory()->create();
    $eligible = Collection::factory()->for($owner)->create([
        'title' => 'Batch Apply Eligible',
        'slug' => 'batch-apply-eligible',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);
    $ineligible = Collection::factory()->for($owner)->create([
        'title' => 'Batch Apply Ineligible',
        'slug' => 'batch-apply-ineligible',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($admin)
        ->postJson(route('settings.collections.surfaces.batch-editorial'), [
            'collection_ids' => [$eligible->id, $ineligible->id],
            'campaign_key' => 'spring-editorial-push',
            'campaign_label' => 'Spring Editorial Push',
            'editorial_notes' => 'Apply homepage editorial batch.',
            'surface_key' => 'homepage-editorial',
            'placement_type' => 'campaign',
            'priority' => 9,
            'notes' => 'Pinned by batch tooling.',
            'apply' => true,
        ])
        ->assertOk()
        ->assertJsonPath('mode', 'apply')
        ->assertJsonPath('plan.collections_count', 2)
        ->assertJsonPath('plan.placement_eligible_count', 1);

    $this->assertDatabaseHas('collections', [
        'id' => $eligible->id,
        'campaign_key' => 'spring-editorial-push',
        'campaign_label' => 'Spring Editorial Push',
        'editorial_notes' => 'Apply homepage editorial batch.',
    ]);

    $this->assertDatabaseHas('collections', [
        'id' => $ineligible->id,
        'campaign_key' => 'spring-editorial-push',
        'campaign_label' => 'Spring Editorial Push',
        'editorial_notes' => 'Apply homepage editorial batch.',
    ]);

    $this->assertDatabaseHas('collection_surface_placements', [
        'collection_id' => $eligible->id,
        'surface_key' => 'homepage-editorial',
        'placement_type' => 'campaign',
        'priority' => 9,
        'campaign_key' => 'spring-editorial-push',
        'notes' => 'Pinned by batch tooling.',
        'created_by_user_id' => $admin->id,
    ]);

    $this->assertDatabaseMissing('collection_surface_placements', [
        'collection_id' => $ineligible->id,
        'surface_key' => 'homepage-editorial',
    ]);

    $this->assertDatabaseHas('collection_history', [
        'collection_id' => $eligible->id,
        'actor_user_id' => $admin->id,
        'action_type' => 'batch_editorial_updated',
    ]);

    $this->assertDatabaseHas('collection_history', [
        'collection_id' => $eligible->id,
        'actor_user_id' => $admin->id,
        'action_type' => 'placement_assigned',
    ]);
});

it('automatic collection surfaces only resolve eligible public collections from rules', function () {
    $owner = User::factory()->create();

    $eligible = Collection::factory()->for($owner)->create([
        'title' => 'Spring Automatic Surface Winner',
        'slug' => 'spring-automatic-surface-winner',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'spring-launch',
        'ranking_score' => 92,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Restricted Spring Collection',
        'slug' => 'restricted-spring-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_RESTRICTED,
        'moderation_status' => Collection::MODERATION_RESTRICTED,
        'campaign_key' => 'spring-launch',
        'ranking_score' => 100,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Private Spring Collection',
        'slug' => 'private-spring-collection',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'spring-launch',
        'ranking_score' => 110,
    ]);

    CollectionSurfaceDefinition::query()->create([
        'surface_key' => 'discover.featured_collections',
        'title' => 'Automatic Featured Collections',
        'description' => 'Rule-driven seasonal spotlight.',
        'mode' => 'automatic',
        'rules_json' => [
            'campaign_key' => 'spring-launch',
        ],
        'ranking_mode' => 'ranking_score',
        'max_items' => 8,
        'is_active' => true,
    ]);

    $response = $this->get(route('collections.featured'))
        ->assertOk()
        ->assertSee('Featured collections', false)
        ->assertSee('Spring Automatic Surface Winner', false)
        ->assertDontSee('Restricted Spring Collection', false)
        ->assertDontSee('Private Spring Collection', false);

    $response->assertSee('Spring Automatic Surface Winner', false);
    expect(app(\App\Services\CollectionSurfaceService::class)->resolveSurfaceItems('discover.featured_collections', 8)->pluck('id')->all())
        ->toBe([$eligible->id]);
});

it('automatic surfaces support broader explainable rules and quality-score ordering', function () {
    $owner = User::factory()->create(['username' => 'signalowner']);
    $otherOwner = User::factory()->create(['username' => 'outsider']);

    $highQuality = Collection::factory()->for($owner)->create([
        'title' => 'Aurora Signature One',
        'slug' => 'aurora-signature-one',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'presentation_style' => Collection::PRESENTATION_HERO_GRID,
        'theme_token' => 'aurora',
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
        'commercial_eligibility' => true,
        'analytics_enabled' => true,
        'quality_score' => 97,
        'ranking_score' => 40,
    ]);

    $lowerQuality = Collection::factory()->for($owner)->create([
        'title' => 'Aurora Signature Two',
        'slug' => 'aurora-signature-two',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'presentation_style' => Collection::PRESENTATION_HERO_GRID,
        'theme_token' => 'aurora',
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
        'commercial_eligibility' => true,
        'analytics_enabled' => true,
        'quality_score' => 88,
        'ranking_score' => 85,
    ]);

    $excluded = Collection::factory()->for($owner)->create([
        'title' => 'Aurora Excluded',
        'slug' => 'aurora-excluded',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'presentation_style' => Collection::PRESENTATION_HERO_GRID,
        'theme_token' => 'aurora',
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
        'commercial_eligibility' => true,
        'analytics_enabled' => true,
        'quality_score' => 99,
        'ranking_score' => 99,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Aurora Closed Collaboration',
        'slug' => 'aurora-closed-collaboration',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'presentation_style' => Collection::PRESENTATION_HERO_GRID,
        'theme_token' => 'aurora',
        'collaboration_mode' => Collection::COLLABORATION_CLOSED,
        'commercial_eligibility' => true,
        'analytics_enabled' => true,
        'quality_score' => 96,
    ]);

    Collection::factory()->for($otherOwner)->create([
        'title' => 'Other Owner Aurora',
        'slug' => 'other-owner-aurora',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'presentation_style' => Collection::PRESENTATION_HERO_GRID,
        'theme_token' => 'aurora',
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
        'commercial_eligibility' => true,
        'analytics_enabled' => true,
        'quality_score' => 95,
    ]);

    CollectionSurfaceDefinition::query()->create([
        'surface_key' => 'homepage.aurora_signatures',
        'title' => 'Aurora Signatures',
        'description' => 'Rule-driven creator curation.',
        'mode' => 'automatic',
        'rules_json' => [
            'owner_username' => $owner->username,
            'presentation_style' => Collection::PRESENTATION_HERO_GRID,
            'theme_token' => 'aurora',
            'collaboration_mode' => Collection::COLLABORATION_OPEN,
            'commercial_eligible_only' => true,
            'analytics_enabled_only' => true,
            'min_quality_score' => 85,
            'exclude_collection_ids' => [$excluded->id],
        ],
        'ranking_mode' => 'quality_score',
        'max_items' => 6,
        'is_active' => true,
    ]);

    expect(app(\App\Services\CollectionSurfaceService::class)->resolveSurfaceItems('homepage.aurora_signatures', 6)->pluck('id')->all())
        ->toBe([$highQuality->id, $lowerQuality->id]);
});

it('surface definitions can fall back to another surface when they are inactive or empty', function () {
    $owner = User::factory()->create();

    $fallbackCollection = Collection::factory()->for($owner)->create([
        'title' => 'Fallback Surface Winner',
        'slug' => 'fallback-surface-winner',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'fallback-campaign',
        'ranking_score' => 77,
    ]);

    CollectionSurfaceDefinition::query()->create([
        'surface_key' => 'homepage.featured_collections',
        'title' => 'Fallback Surface',
        'description' => 'Always-on fallback surface.',
        'mode' => 'automatic',
        'rules_json' => [
            'campaign_key' => 'fallback-campaign',
        ],
        'ranking_mode' => 'ranking_score',
        'max_items' => 6,
        'is_active' => true,
    ]);

    CollectionSurfaceDefinition::query()->create([
        'surface_key' => 'homepage.editorial_collections',
        'title' => 'Scheduled Primary Surface',
        'description' => 'Primary campaign window.',
        'mode' => 'automatic',
        'rules_json' => [
            'campaign_key' => 'missing-window',
        ],
        'ranking_mode' => 'ranking_score',
        'max_items' => 6,
        'is_active' => true,
        'starts_at' => now()->addDay(),
        'fallback_surface_key' => 'homepage.featured_collections',
    ]);

    expect(app(\App\Services\CollectionSurfaceService::class)->resolveSurfaceItems('homepage.editorial_collections', 6)->pluck('id')->all())
        ->toBe([$fallbackCollection->id]);
});

it('staff can delete placements and must clear placements before deleting a surface definition', function () {
    $admin = User::factory()->create(['role' => 'admin']);
    $owner = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Placement Delete Candidate',
        'slug' => 'placement-delete-candidate',
    ]);

    $definition = CollectionSurfaceDefinition::query()->create([
        'surface_key' => 'homepage.cleanup_test',
        'title' => 'Cleanup Test',
        'description' => 'Surface cleanup coverage.',
        'mode' => 'manual',
        'rules_json' => null,
        'ranking_mode' => 'ranking_score',
        'max_items' => 4,
        'is_active' => true,
    ]);

    $placement = CollectionSurfacePlacement::query()->create([
        'collection_id' => $collection->id,
        'surface_key' => 'homepage.cleanup_test',
        'placement_type' => 'manual',
        'priority' => 5,
        'starts_at' => now()->subHour(),
        'ends_at' => now()->addHour(),
        'is_active' => true,
        'campaign_key' => null,
        'notes' => 'Temporary slot.',
        'created_by_user_id' => $admin->id,
    ]);

    $this->actingAs($admin)
        ->deleteJson(route('settings.collections.surfaces.definitions.destroy', ['definition' => $definition->id]))
        ->assertStatus(422);

    $this->actingAs($admin)
        ->deleteJson(route('settings.collections.surfaces.placements.destroy', ['placement' => $placement->id]))
        ->assertOk()
        ->assertJsonPath('deleted_placement_id', $placement->id);

    $this->assertDatabaseMissing('collection_surface_placements', [
        'id' => $placement->id,
    ]);

    $this->assertDatabaseHas('collection_history', [
        'collection_id' => $collection->id,
        'actor_user_id' => $admin->id,
        'action_type' => 'placement_removed',
    ]);

    $this->actingAs($admin)
        ->deleteJson(route('settings.collections.surfaces.definitions.destroy', ['definition' => $definition->id]))
        ->assertOk()
        ->assertJsonPath('deleted_definition_id', $definition->id);

    $this->assertDatabaseMissing('collection_surface_definitions', [
        'id' => $definition->id,
    ]);
});

it('surface conflict summaries flag overlapping active placements on the same surface', function () {
    $owner = User::factory()->create();

    $first = Collection::factory()->for($owner)->create([
        'title' => 'Conflict One',
        'slug' => 'conflict-one',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $second = Collection::factory()->for($owner)->create([
        'title' => 'Conflict Two',
        'slug' => 'conflict-two',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    CollectionSurfacePlacement::query()->create([
        'collection_id' => $first->id,
        'surface_key' => 'homepage.editorial_collections',
        'placement_type' => 'manual',
        'priority' => 10,
        'starts_at' => now()->addHour(),
        'ends_at' => now()->addHours(5),
        'is_active' => true,
        'created_by_user_id' => $owner->id,
    ]);

    CollectionSurfacePlacement::query()->create([
        'collection_id' => $second->id,
        'surface_key' => 'homepage.editorial_collections',
        'placement_type' => 'manual',
        'priority' => 9,
        'starts_at' => now()->addHours(2),
        'ends_at' => now()->addHours(6),
        'is_active' => true,
        'created_by_user_id' => $owner->id,
    ]);

    $conflicts = app(\App\Services\CollectionSurfaceService::class)->placementConflicts('homepage.editorial_collections');

    expect($conflicts)->toHaveCount(1);
    expect($conflicts->first()['surface_key'])->toBe('homepage.editorial_collections');
    expect($conflicts->first()['collection_titles'])->toBe(['Conflict One', 'Conflict Two']);
    expect($conflicts->first()['placement_ids'])->toHaveCount(2);
});

it('user can create saved lists and add a saved collection into a list', function () {
    $user = User::factory()->create();
    $owner = User::factory()->create(['username' => 'savedowner']);
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Reference Shelf Collection',
        'slug' => 'reference-shelf-collection',
    ]);

    DB::table('collection_saves')->insert([
        'user_id' => $user->id,
        'collection_id' => $collection->id,
        'created_at' => now(),
    ]);

    $listResponse = $this->actingAs($user)
        ->postJson(route('me.saved.collections.lists.store'), [
            'title' => 'Spring references',
        ])
        ->assertOk()
        ->assertJsonPath('list.title', 'Spring references');

    $listId = (int) $listResponse->json('list.id');

    $this->actingAs($user)
        ->postJson(route('me.saved.collections.lists.items.store', ['collection' => $collection->id]), [
            'saved_list_id' => $listId,
        ])
        ->assertOk()
        ->assertJsonPath('item.saved_list_id', $listId)
        ->assertJsonPath('item.collection_id', $collection->id);

    $this->assertDatabaseHas('collection_saved_lists', [
        'id' => $listId,
        'user_id' => $user->id,
        'title' => 'Spring references',
    ]);

    $this->assertDatabaseHas('collection_saved_list_items', [
        'saved_list_id' => $listId,
        'collection_id' => $collection->id,
    ]);
});

it('user can browse a saved list route and remove a collection from that list', function () {
    $user = User::factory()->create();
    $owner = User::factory()->create(['username' => 'listowner']);
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'List Removal Collection',
        'slug' => 'list-removal-collection',
    ]);

    DB::table('collection_saves')->insert([
        'user_id' => $user->id,
        'collection_id' => $collection->id,
        'created_at' => now(),
    ]);

    $listId = DB::table('collection_saved_lists')->insertGetId([
        'user_id' => $user->id,
        'title' => 'Campaign watchlist',
        'slug' => 'campaign-watchlist',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    DB::table('collection_saved_list_items')->insert([
        'saved_list_id' => $listId,
        'collection_id' => $collection->id,
        'order_num' => 0,
        'created_at' => now(),
    ]);

    $this->actingAs($user)
        ->get(route('me.saved.collections.lists.show', ['listSlug' => 'campaign-watchlist']))
        ->assertOk();

    $this->actingAs($user)
        ->deleteJson(route('me.saved.collections.lists.items.destroy', ['list' => $listId, 'collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('removed', true)
        ->assertJsonPath('list.id', $listId)
        ->assertJsonPath('list.items_count', 0);

    $this->assertDatabaseMissing('collection_saved_list_items', [
        'saved_list_id' => $listId,
        'collection_id' => $collection->id,
    ]);
});

it('saved list route preserves saved list order and user can reorder items', function () {
    $user = User::factory()->create();
    $owner = User::factory()->create(['username' => 'reorderowner']);
    $collectionA = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'First Ordered Collection',
        'slug' => 'first-ordered-collection',
    ]);
    $collectionB = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Second Ordered Collection',
        'slug' => 'second-ordered-collection',
    ]);
    $collectionC = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Third Ordered Collection',
        'slug' => 'third-ordered-collection',
    ]);

    DB::table('collection_saves')->insert([
        [
            'user_id' => $user->id,
            'collection_id' => $collectionA->id,
            'created_at' => now(),
        ],
        [
            'user_id' => $user->id,
            'collection_id' => $collectionB->id,
            'created_at' => now()->subMinute(),
        ],
        [
            'user_id' => $user->id,
            'collection_id' => $collectionC->id,
            'created_at' => now()->subMinutes(2),
        ],
    ]);

    $listId = DB::table('collection_saved_lists')->insertGetId([
        'user_id' => $user->id,
        'title' => 'Editorial stack',
        'slug' => 'editorial-stack',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    DB::table('collection_saved_list_items')->insert([
        [
            'saved_list_id' => $listId,
            'collection_id' => $collectionB->id,
            'order_num' => 0,
            'created_at' => now(),
        ],
        [
            'saved_list_id' => $listId,
            'collection_id' => $collectionC->id,
            'order_num' => 1,
            'created_at' => now(),
        ],
        [
            'saved_list_id' => $listId,
            'collection_id' => $collectionA->id,
            'order_num' => 2,
            'created_at' => now(),
        ],
    ]);

    $response = $this->actingAs($user)
        ->get(route('me.saved.collections.lists.show', ['listSlug' => 'editorial-stack']))
        ->assertOk();

    $page = $response->viewData('page');

    expect(collect(data_get($page, 'props.collections', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toBe([$collectionB->id, $collectionC->id, $collectionA->id]);

    $this->actingAs($user)
        ->postJson(route('me.saved.collections.lists.items.reorder', ['list' => $listId]), [
            'collection_ids' => [$collectionA->id, $collectionB->id, $collectionC->id],
        ])
        ->assertOk()
        ->assertJsonPath('ordered_collection_ids.0', $collectionA->id)
        ->assertJsonPath('ordered_collection_ids.1', $collectionB->id)
        ->assertJsonPath('ordered_collection_ids.2', $collectionC->id);

    expect(DB::table('collection_saved_list_items')
        ->where('saved_list_id', $listId)
        ->orderBy('order_num')
        ->pluck('collection_id')
        ->map(fn ($id) => (int) $id)
        ->all())
        ->toBe([$collectionA->id, $collectionB->id, $collectionC->id]);
});

it('unsaving a collection clears that users saved list items for it', function () {
    $user = User::factory()->create();
    $owner = User::factory()->create(['username' => 'unsaveowner']);
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'title' => 'Unsave Cleanup Collection',
        'slug' => 'unsave-cleanup-collection',
    ]);

    DB::table('collection_saves')->insert([
        'user_id' => $user->id,
        'collection_id' => $collection->id,
        'created_at' => now(),
    ]);

    $listId = DB::table('collection_saved_lists')->insertGetId([
        'user_id' => $user->id,
        'title' => 'Reference board',
        'slug' => 'reference-board',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    DB::table('collection_saved_list_items')->insert([
        'saved_list_id' => $listId,
        'collection_id' => $collection->id,
        'order_num' => 0,
        'created_at' => now(),
    ]);

    $this->actingAs($user)
        ->deleteJson(route('collections.unsave', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('saved', false);

    $this->assertDatabaseMissing('collection_saves', [
        'user_id' => $user->id,
        'collection_id' => $collection->id,
    ]);

    $this->assertDatabaseMissing('collection_saved_list_items', [
        'saved_list_id' => $listId,
        'collection_id' => $collection->id,
    ]);
});

it('owner can delete a collection without deleting artworks', function () {
    $user = User::factory()->create();
    $collection = Collection::factory()->for($user)->create();
    $artwork = Artwork::factory()->for($user)->create();

    DB::table('collection_artwork')->insert([
        'collection_id' => $collection->id,
        'artwork_id' => $artwork->id,
        'order_num' => 0,
        'created_at' => now(),
        'updated_at' => now(),
    ]);
    $collection->update(['artworks_count' => 1]);

    $this->actingAs($user)
        ->deleteJson(route('settings.collections.destroy', ['collection' => $collection->id]))
        ->assertOk();

    $this->assertSoftDeleted('collections', ['id' => $collection->id]);
    $this->assertDatabaseMissing('collection_artwork', [
        'collection_id' => $collection->id,
        'artwork_id' => $artwork->id,
    ]);
    expect(Artwork::query()->find($artwork->id))->not->toBeNull();
});

it('owner can attach only their own artworks', function () {
    $user = User::factory()->create();
    $collection = Collection::factory()->for($user)->create();
    $artwork = Artwork::factory()->for($user)->create();

    $this->actingAs($user)
        ->postJson(route('settings.collections.artworks.attach', ['collection' => $collection->id]), [
            'artwork_ids' => [$artwork->id],
        ])
        ->assertOk()
        ->assertJsonPath('attachedArtworks.0.id', $artwork->id);

    $this->assertDatabaseHas('collection_artwork', [
        'collection_id' => $collection->id,
        'artwork_id' => $artwork->id,
    ]);
});

it('owner can load collection options for an owned artwork card action', function () {
    $user = User::factory()->create();
    $artwork = Artwork::factory()->for($user)->create();
    $attachedCollection = Collection::factory()->for($user)->create([
        'title' => 'Attached Showcase',
        'slug' => 'attached-showcase',
        'artworks_count' => 1,
    ]);
    $freeCollection = Collection::factory()->for($user)->create([
        'title' => 'Fresh Showcase',
        'slug' => 'fresh-showcase',
        'artworks_count' => 0,
    ]);

    DB::table('collection_artwork')->insert([
        'collection_id' => $attachedCollection->id,
        'artwork_id' => $artwork->id,
        'order_num' => 0,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $response = $this->actingAs($user)
        ->getJson(route('settings.collections.artworks.options', ['artwork' => $artwork->id]))
        ->assertOk()
        ->json();

    expect($response['meta']['artwork_id'])->toBe($artwork->id);
    expect($response['meta']['create_url'])->toBe(route('settings.collections.create'));
    expect(collect($response['data'])->pluck('title')->sort()->values()->all())
        ->toBe(collect([$freeCollection->title, $attachedCollection->title])->sort()->values()->all());
    expect(collect($response['data'])->firstWhere('id', $attachedCollection->id)['already_attached'])->toBeTrue();
    expect(collect($response['data'])->firstWhere('id', $freeCollection->id)['already_attached'])->toBeFalse();
});

it('owner cannot load collection options for another users artwork', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $artwork = Artwork::factory()->for($otherUser)->create();

    $this->actingAs($user)
        ->getJson(route('settings.collections.artworks.options', ['artwork' => $artwork->id]))
        ->assertNotFound();
});

it('owner cannot attach another users artwork', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $collection = Collection::factory()->for($user)->create();
    $foreignArtwork = Artwork::factory()->for($otherUser)->create();

    $this->actingAs($user)
        ->postJson(route('settings.collections.artworks.attach', ['collection' => $collection->id]), [
            'artwork_ids' => [$foreignArtwork->id],
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors('artwork_ids');

    $this->assertDatabaseMissing('collection_artwork', [
        'collection_id' => $collection->id,
        'artwork_id' => $foreignArtwork->id,
    ]);
});

it('owner can reorder artworks inside a collection', function () {
    $user = User::factory()->create();
    $collection = Collection::factory()->for($user)->create();
    $artworkA = Artwork::factory()->for($user)->create();
    $artworkB = Artwork::factory()->for($user)->create();
    $artworkC = Artwork::factory()->for($user)->create();

    app(CollectionService::class)->attachArtworks($collection, $user, [$artworkA->id, $artworkB->id, $artworkC->id]);

    $this->actingAs($user)
        ->postJson(route('settings.collections.artworks.reorder', ['collection' => $collection->id]), [
            'ordered_artwork_ids' => [$artworkC->id, $artworkA->id, $artworkB->id],
        ])
        ->assertOk();

    expect(DB::table('collection_artwork')
        ->where('collection_id', $collection->id)
        ->orderBy('order_num')
        ->pluck('artwork_id')
        ->map(fn ($id) => (int) $id)
        ->all())
        ->toBe([$artworkC->id, $artworkA->id, $artworkB->id]);
});

it('public can view a public collection', function () {
    $user = User::factory()->create(['username' => 'collector']);
    $collection = Collection::factory()->for($user)->create([
        'title' => 'Neon Blue Experiments',
        'slug' => 'neon-blue-experiments',
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    $this->get(route('profile.collections.show', ['username' => $user->username, 'slug' => $collection->slug]))
        ->assertOk()
        ->assertSee('Neon Blue Experiments', false);
});

it('public series landing page lists public series collections in order and excludes hidden entries', function () {
    $owner = User::factory()->create(['username' => 'seriesowner']);

    $first = Collection::factory()->for($owner)->create([
        'title' => 'Series Start',
        'slug' => 'series-start',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'series_key' => 'winter-wallpaper-run',
        'series_title' => 'Winter Wallpaper Run',
        'series_description' => 'A seasonal progression of desktop-ready winter showcases.',
        'series_order' => 1,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Series Hidden Private',
        'slug' => 'series-hidden-private',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'series_key' => 'winter-wallpaper-run',
        'series_order' => 2,
    ]);

    $second = Collection::factory()->for($owner)->create([
        'title' => 'Series Finale',
        'slug' => 'series-finale',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'series_key' => 'winter-wallpaper-run',
        'series_order' => 3,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Series Restricted Entry',
        'slug' => 'series-restricted-entry',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_RESTRICTED,
        'moderation_status' => Collection::MODERATION_RESTRICTED,
        'series_key' => 'winter-wallpaper-run',
        'series_order' => 4,
    ]);

    $this->get(route('collections.series.show', ['seriesKey' => 'winter-wallpaper-run']))
        ->assertOk()
        ->assertSee('Winter Wallpaper Run', false)
        ->assertSee('A seasonal progression of desktop-ready winter showcases.', false)
        ->assertSeeInOrder([$first->title, $second->title], false)
        ->assertDontSee('Series Hidden Private', false)
        ->assertDontSee('Series Restricted Entry', false);
});

it('collection pages link to the full public series landing page when the collection belongs to a series', function () {
    $owner = User::factory()->create(['username' => 'serieslinkowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Series Link Entry',
        'slug' => 'series-link-entry',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'series_key' => 'editorial-story-run',
        'series_title' => 'Editorial Story Run',
        'series_description' => 'A connected editorial sequence across multiple collection chapters.',
        'series_order' => 2,
    ]);

    $seriesUrl = str_replace('/', '\\/', route('collections.series.show', ['seriesKey' => 'editorial-story-run']));

    $this->get(route('profile.collections.show', ['username' => $owner->username, 'slug' => $collection->slug]))
        ->assertOk()
        ->assertSee($seriesUrl, false)
        ->assertSee('Editorial Story Run', false)
        ->assertSee('A connected editorial sequence across multiple collection chapters.', false);
});

it('public cannot view a private collection', function () {
    $user = User::factory()->create(['username' => 'privatecollector']);
    $collection = Collection::factory()->for($user)->create([
        'slug' => 'hidden-showcase',
        'visibility' => Collection::VISIBILITY_PRIVATE,
    ]);

    $this->get(route('profile.collections.show', ['username' => $user->username, 'slug' => $collection->slug]))
        ->assertNotFound();
});

it('unlisted collections are hidden from public profile listings but visible to the owner', function () {
    $user = User::factory()->create(['username' => 'curator']);

    Collection::factory()->for($user)->create([
        'title' => 'Public Collection',
        'slug' => 'public-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    Collection::factory()->for($user)->create([
        'title' => 'Secret Drop',
        'slug' => 'secret-drop',
        'visibility' => Collection::VISIBILITY_UNLISTED,
    ]);

    $this->get(route('profile.tab', ['username' => $user->username, 'tab' => 'collections']))
        ->assertOk()
        ->assertSee('Public Collection', false)
        ->assertDontSee('Secret Drop', false);

    $this->actingAs($user)
        ->get(route('profile.tab', ['username' => $user->username, 'tab' => 'collections']))
        ->assertOk()
        ->assertSee('Public Collection', false)
        ->assertSee('Secret Drop', false);
});

it('smart preview can resolve artworks by ai style tags for the owner only', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $ownedArtwork = Artwork::factory()->for($user)->create();
    $foreignArtwork = Artwork::factory()->for($otherUser)->create();
    $styleTag = Tag::query()->create([
        'name' => 'Digital Painting',
        'slug' => 'digital-painting',
        'usage_count' => 0,
        'is_active' => true,
    ]);

    DB::table('artwork_tag')->insert([
        [
            'artwork_id' => $ownedArtwork->id,
            'tag_id' => $styleTag->id,
            'source' => 'ai',
            'confidence' => 0.97,
            'created_at' => now(),
        ],
        [
            'artwork_id' => $foreignArtwork->id,
            'tag_id' => $styleTag->id,
            'source' => 'ai',
            'confidence' => 0.92,
            'created_at' => now(),
        ],
    ]);

    $response = $this->actingAs($user)
        ->postJson(route('settings.collections.smart.preview'), [
            'smart_rules_json' => [
                'match' => 'all',
                'sort' => 'newest',
                'rules' => [
                    [
                        'field' => 'style',
                        'operator' => 'equals',
                        'value' => 'digital-painting',
                    ],
                ],
            ],
        ])
        ->assertOk();

    expect(collect($response->json('preview.artworks.data'))->pluck('id')->all())
        ->toBe([$ownedArtwork->id]);
});

it('smart preview can resolve artworks by ai color tags for the owner only', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $ownedArtwork = Artwork::factory()->for($user)->create();
    $foreignArtwork = Artwork::factory()->for($otherUser)->create();
    $colorTag = Tag::query()->create([
        'name' => 'Blue Tones',
        'slug' => 'blue-tones',
        'usage_count' => 0,
        'is_active' => true,
    ]);

    DB::table('artwork_tag')->insert([
        [
            'artwork_id' => $ownedArtwork->id,
            'tag_id' => $colorTag->id,
            'source' => 'ai',
            'confidence' => 0.95,
            'created_at' => now(),
        ],
        [
            'artwork_id' => $foreignArtwork->id,
            'tag_id' => $colorTag->id,
            'source' => 'ai',
            'confidence' => 0.88,
            'created_at' => now(),
        ],
    ]);

    $response = $this->actingAs($user)
        ->postJson(route('settings.collections.smart.preview'), [
            'smart_rules_json' => [
                'match' => 'all',
                'sort' => 'newest',
                'rules' => [
                    [
                        'field' => 'color',
                        'operator' => 'equals',
                        'value' => 'blue-tones',
                    ],
                ],
            ],
        ])
        ->assertOk();

    expect(collect($response->json('preview.artworks.data'))->pluck('id')->all())
        ->toBe([$ownedArtwork->id]);
});

it('smart preview can resolve artworks by mature flag for the owner only', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $ownedMatureArtwork = Artwork::factory()->for($user)->create(['is_mature' => true]);
    Artwork::factory()->for($user)->create(['is_mature' => false]);
    Artwork::factory()->for($otherUser)->create(['is_mature' => true]);

    $response = $this->actingAs($user)
        ->postJson(route('settings.collections.smart.preview'), [
            'smart_rules_json' => [
                'match' => 'all',
                'sort' => 'newest',
                'rules' => [
                    [
                        'field' => 'is_mature',
                        'operator' => 'equals',
                        'value' => true,
                    ],
                ],
            ],
        ])
        ->assertOk();

    expect(collect($response->json('preview.artworks.data'))->pluck('id')->all())
        ->toBe([$ownedMatureArtwork->id]);
});

it('cover fallback resolves to the first attached artwork when manual cover becomes unavailable', function () {
    $user = User::factory()->create();
    $collection = Collection::factory()->for($user)->create();
    $firstArtwork = Artwork::factory()->for($user)->create();
    $secondArtwork = Artwork::factory()->for($user)->create();
    $service = app(CollectionService::class);

    $service->attachArtworks($collection, $user, [$firstArtwork->id, $secondArtwork->id]);
    $collection->refresh();

    expect($collection->resolvedCoverArtwork()?->id)->toBe($firstArtwork->id);

    $service->updateCollection($collection->loadMissing('user'), [
        'title' => $collection->title,
        'slug' => $collection->slug,
        'description' => $collection->description,
        'visibility' => $collection->visibility,
        'sort_mode' => $collection->sort_mode,
        'cover_artwork_id' => $secondArtwork->id,
    ]);

    $collection->refresh();
    expect($collection->resolvedCoverArtwork()?->id)->toBe($secondArtwork->id);

    $service->removeArtwork($collection->loadMissing('user'), $secondArtwork);
    $collection->refresh();

    expect($collection->cover_artwork_id)->toBeNull();
    expect($collection->resolvedCoverArtwork()?->id)->toBe($firstArtwork->id);
});

it('owner can feature a public collection up to the configured limit', function () {
    $user = User::factory()->create();
    $limit = (int) config('collections.featured_limit', 3);

    $existingFeatured = Collection::factory()->count(max($limit - 1, 0))->for($user)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'is_featured' => true,
        'featured_at' => now()->subDay(),
    ]);

    $collection = Collection::factory()->for($user)->create([
        'title' => 'Pin Me',
        'slug' => 'pin-me',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'is_featured' => false,
        'featured_at' => null,
    ]);

    $this->actingAs($user)
        ->postJson(route('settings.collections.feature', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('collection.is_featured', true);

    $collection->refresh();

    expect($collection->is_featured)->toBeTrue();
    expect($collection->featured_at)->not->toBeNull();
    expect(Collection::query()->ownedBy($user->id)->where('is_featured', true)->count())
        ->toBe($existingFeatured->count() + 1);
});

it('owner cannot feature more than the configured limit', function () {
    $user = User::factory()->create();
    $limit = (int) config('collections.featured_limit', 3);

    Collection::factory()->count($limit)->for($user)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'is_featured' => true,
        'featured_at' => now()->subHour(),
    ]);

    $overflow = Collection::factory()->for($user)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'is_featured' => false,
        'featured_at' => null,
    ]);

    $this->actingAs($user)
        ->postJson(route('settings.collections.feature', ['collection' => $overflow->id]))
        ->assertStatus(422)
        ->assertJsonValidationErrors('collection');

    $overflow->refresh();
    expect($overflow->is_featured)->toBeFalse();
});

it('featured discovery only shows public featured collections', function () {
    $owner = User::factory()->create(['username' => 'featureowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Public Featured Collection',
        'slug' => 'public-featured-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'is_featured' => true,
        'featured_at' => now(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Private Featured Collection',
        'slug' => 'private-featured-collection',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'is_featured' => true,
        'featured_at' => now(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Unlisted Featured Collection',
        'slug' => 'unlisted-featured-collection',
        'visibility' => Collection::VISIBILITY_UNLISTED,
        'is_featured' => true,
        'featured_at' => now(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Placement Blocked Featured Collection',
        'slug' => 'placement-blocked-featured-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'placement_eligibility' => false,
        'is_featured' => true,
        'featured_at' => now(),
    ]);

    $this->get(route('collections.featured'))
        ->assertOk()
        ->assertSee('Public Featured Collection', false)
        ->assertDontSee('Private Featured Collection', false)
        ->assertDontSee('Unlisted Featured Collection', false)
        ->assertDontSee('Placement Blocked Featured Collection', false);
});

it('authenticated users can like and follow public collections', function () {
    $owner = User::factory()->create(['username' => 'showcaseowner']);
    $viewer = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'likes_count' => 0,
        'followers_count' => 0,
    ]);

    $this->actingAs($viewer)
        ->postJson(route('collections.like', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('liked', true)
        ->assertJsonPath('likes_count', 1);

    $this->actingAs($viewer)
        ->postJson(route('collections.follow', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('following', true)
        ->assertJsonPath('followers_count', 1);

    $this->assertDatabaseHas('collection_likes', [
        'collection_id' => $collection->id,
        'user_id' => $viewer->id,
    ]);

    $this->assertDatabaseHas('collection_follows', [
        'collection_id' => $collection->id,
        'user_id' => $viewer->id,
    ]);

    $this->actingAs($viewer)
        ->deleteJson(route('collections.unlike', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('liked', false)
        ->assertJsonPath('likes_count', 0);

    $this->actingAs($viewer)
        ->deleteJson(route('collections.unfollow', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('following', false)
        ->assertJsonPath('followers_count', 0);
});

it('private collections cannot be liked or followed publicly', function () {
    $owner = User::factory()->create();
    $viewer = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PRIVATE,
    ]);

    $this->actingAs($viewer)
        ->postJson(route('collections.like', ['collection' => $collection->id]))
        ->assertNotFound();

    $this->actingAs($viewer)
        ->postJson(route('collections.follow', ['collection' => $collection->id]))
        ->assertNotFound();
});

it('owners cannot like or follow their own public collections', function () {
    $owner = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    $this->actingAs($owner)
        ->postJson(route('collections.like', ['collection' => $collection->id]))
        ->assertStatus(422)
        ->assertJsonValidationErrors('collection');

    $this->actingAs($owner)
        ->postJson(route('collections.follow', ['collection' => $collection->id]))
        ->assertStatus(422)
        ->assertJsonValidationErrors('collection');
});

it('collection creation creates an active owner membership and allows collaborator invites', function () {
    $owner = User::factory()->create(['username' => 'curatorowner']);
    $invitee = User::factory()->create(['username' => 'guesteditor']);

    $collectionId = (int) $this->actingAs($owner)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Community Showcase',
            'slug' => 'community-showcase',
            'type' => Collection::TYPE_COMMUNITY,
            'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
            'allow_comments' => true,
            'allow_saves' => true,
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
        ])
        ->assertOk()
        ->json('collection.id');

    $collection = Collection::query()->findOrFail($collectionId);

    $this->assertDatabaseHas('collection_members', [
        'collection_id' => $collection->id,
        'user_id' => $owner->id,
        'role' => Collection::MEMBER_ROLE_OWNER,
        'status' => Collection::MEMBER_STATUS_ACTIVE,
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.store', ['collection' => $collection->id]), [
            'username' => $invitee->username,
            'role' => Collection::MEMBER_ROLE_EDITOR,
        ])
        ->assertOk();

    $member = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $invitee->id)
        ->firstOrFail();

    expect($member->status)->toBe(Collection::MEMBER_STATUS_PENDING);

    $this->actingAs($invitee)
        ->postJson(route('settings.collections.members.accept', ['member' => $member->id]))
        ->assertOk();

    $member->refresh();
    expect($member->status)->toBe(Collection::MEMBER_STATUS_ACTIVE);
});

it('invited user can reject a collaborator invite', function () {
    $owner = User::factory()->create(['username' => 'inviteowner']);
    $invitee = User::factory()->create(['username' => 'declineguest']);

    $collectionId = (int) $this->actingAs($owner)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Invite Workflow Collection',
            'slug' => 'invite-workflow-collection',
            'type' => Collection::TYPE_COMMUNITY,
            'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
        ])
        ->assertOk()
        ->json('collection.id');

    $collection = Collection::query()->findOrFail($collectionId);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.store', ['collection' => $collection->id]), [
            'username' => $invitee->username,
            'role' => Collection::MEMBER_ROLE_CONTRIBUTOR,
        ])
        ->assertOk();

    $member = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $invitee->id)
        ->firstOrFail();

    $this->actingAs($invitee)
        ->postJson(route('settings.collections.members.decline', ['member' => $member->id]))
        ->assertOk();

    $member->refresh();

    expect($member->status)->toBe(Collection::MEMBER_STATUS_REVOKED);
    expect($member->accepted_at)->toBeNull();
    expect($member->revoked_at)->not->toBeNull();
});

it('scheduled collections only become public inside their publish window', function () {
    $owner = User::factory()->create(['username' => 'scheduleowner']);
    $publishAt = now()->addHour();
    $unpublishAt = now()->addHours(2);

    $collectionId = (int) $this->actingAs($owner)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Scheduled Launch Collection',
            'slug' => 'scheduled-launch-collection',
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
            'published_at' => $publishAt->toISOString(),
            'unpublished_at' => $unpublishAt->toISOString(),
        ])
        ->assertOk()
        ->json('collection.id');

    $collection = Collection::query()->findOrFail($collectionId);
    $showRoute = route('profile.collections.show', ['username' => $owner->username, 'slug' => $collection->slug]);

    auth()->logout();

    $this->get($showRoute)->assertNotFound();

    $this->travelTo($publishAt->copy()->addMinute());
    $this->get($showRoute)->assertOk();

    $this->travelTo($unpublishAt->copy()->addMinute());
    $this->get($showRoute)->assertNotFound();

    $this->travelBack();
});

it('collection lifecycle sync expires stale invites and unfeatures unpublished collections', function () {
    $owner = User::factory()->create(['username' => 'lifecycleowner']);
    $invitee = User::factory()->create(['username' => 'staleinvitee']);

    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'is_featured' => true,
        'featured_at' => now()->subDay(),
        'unpublished_at' => now()->subMinute(),
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.store', ['collection' => $collection->id]), [
            'username' => $invitee->username,
            'role' => Collection::MEMBER_ROLE_VIEWER,
        ])
        ->assertOk();

    $member = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $invitee->id)
        ->firstOrFail();

    $member->forceFill([
        'expires_at' => now()->subMinute(),
    ])->save();

    $this->artisan('collections:sync-lifecycle')->assertExitCode(0);

    $member->refresh();
    $collection->refresh();

    expect($member->status)->toBe(Collection::MEMBER_STATUS_REVOKED);
    expect((bool) $collection->is_featured)->toBeFalse();
    expect($collection->featured_at)->toBeNull();
});

it('expired invites cannot be accepted and ownership can be transferred to an active collaborator', function () {
    $owner = User::factory()->create(['username' => 'transferowner']);
    $invitee = User::factory()->create(['username' => 'expiredinvitee']);
    $newOwner = User::factory()->create(['username' => 'nextowner']);

    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    $collaborators = app(\App\Services\CollectionCollaborationService::class);
    $collaborators->ensureOwnerMembership($collection);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.store', ['collection' => $collection->id]), [
            'username' => $invitee->username,
            'role' => Collection::MEMBER_ROLE_CONTRIBUTOR,
        ])
        ->assertOk();

    $expiredMember = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $invitee->id)
        ->firstOrFail();

    $expiredMember->forceFill([
        'expires_at' => now()->subMinute(),
    ])->save();

    $this->actingAs($invitee)
        ->postJson(route('settings.collections.members.accept', ['member' => $expiredMember->id]))
        ->assertStatus(422);

    $expiredMember->refresh();
    expect($expiredMember->status)->toBe(Collection::MEMBER_STATUS_REVOKED);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.store', ['collection' => $collection->id]), [
            'username' => $newOwner->username,
            'role' => Collection::MEMBER_ROLE_EDITOR,
        ])
        ->assertOk();

    $transferMember = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $newOwner->id)
        ->firstOrFail();

    $this->actingAs($newOwner)
        ->postJson(route('settings.collections.members.accept', ['member' => $transferMember->id]))
        ->assertOk();

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.transfer', ['collection' => $collection->id, 'member' => $transferMember->id]))
        ->assertOk();

    $collection->refresh();
    $transferMember->refresh();

    expect((int) $collection->user_id)->toBe((int) $newOwner->id);
    expect($transferMember->role)->toBe(Collection::MEMBER_ROLE_OWNER);

    $this->assertDatabaseHas('collection_members', [
        'collection_id' => $collection->id,
        'user_id' => $owner->id,
        'role' => Collection::MEMBER_ROLE_EDITOR,
    ]);
});

it('collection owners can override invite expiry per collaborator invite', function () {
    $owner = User::factory()->create(['username' => 'custominviteowner']);
    $invitee = User::factory()->create(['username' => 'custominvitee']);

    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $beforeInvite = now();

    $this->actingAs($owner)
        ->postJson(route('settings.collections.members.store', ['collection' => $collection->id]), [
            'username' => $invitee->username,
            'role' => Collection::MEMBER_ROLE_VIEWER,
            'expires_in_days' => 14,
        ])
        ->assertOk();

    $member = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $invitee->id)
        ->firstOrFail();

    expect($member->expires_at)->not->toBeNull();
    expect($member->expires_at->betweenIncluded($beforeInvite->copy()->addDays(13)->startOfMinute(), $beforeInvite->copy()->addDays(14)->addMinute()))->toBeTrue();
});

it('submission anti spam guard rate limits repeated collection submissions', function () {
    config()->set('collections.submissions.max_per_hour', 2);

    $owner = User::factory()->create(['username' => 'ratelimitowner']);
    $submitter = User::factory()->create(['username' => 'ratelimitsubmitter']);
    $artworkA = Artwork::factory()->for($submitter)->create();
    $artworkB = Artwork::factory()->for($submitter)->create();
    $artworkC = Artwork::factory()->for($submitter)->create();

    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'mode' => Collection::MODE_MANUAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_submissions' => true,
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $this->actingAs($submitter)
        ->postJson(route('collections.submissions.store', ['collection' => $collection->id]), [
            'artwork_id' => $artworkA->id,
        ])
        ->assertOk();

    $this->actingAs($submitter)
        ->postJson(route('collections.submissions.store', ['collection' => $collection->id]), [
            'artwork_id' => $artworkB->id,
        ])
        ->assertOk();

    $this->actingAs($submitter)
        ->postJson(route('collections.submissions.store', ['collection' => $collection->id]), [
            'artwork_id' => $artworkC->id,
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors('collection');
});

it('authenticated viewers can save public collections', function () {
    $owner = User::factory()->create();
    $viewer = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_saves' => true,
        'saves_count' => 0,
    ]);

    $this->actingAs($viewer)
        ->postJson(route('collections.save', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('saved', true)
        ->assertJsonPath('saves_count', 1);

    $this->assertDatabaseHas('collection_saves', [
        'collection_id' => $collection->id,
        'user_id' => $viewer->id,
    ]);
});

it('community collections can receive submissions and managers can approve them', function () {
    $owner = User::factory()->create(['username' => 'collectionowner']);
    $submitter = User::factory()->create(['username' => 'submitter']);
    $artwork = Artwork::factory()->for($submitter)->create();
    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'mode' => Collection::MODE_MANUAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_submissions' => true,
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $response = $this->actingAs($submitter)
        ->postJson(route('collections.submissions.store', ['collection' => $collection->id]), [
            'artwork_id' => $artwork->id,
        ])
        ->assertOk();

    $submissionId = (int) $response->json('submission.id');

    $this->actingAs($owner)
        ->postJson(route('collections.submissions.approve', ['submission' => $submissionId]))
        ->assertOk();

    $this->assertDatabaseHas('collection_artwork', [
        'collection_id' => $collection->id,
        'artwork_id' => $artwork->id,
    ]);

    $this->assertDatabaseHas('collection_submissions', [
        'id' => $submissionId,
        'status' => Collection::SUBMISSION_APPROVED,
    ]);
});

it('authenticated viewers can comment on collections when comments are enabled', function () {
    $owner = User::factory()->create(['username' => 'commentowner']);
    $viewer = User::factory()->create(['username' => 'commentviewer']);
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_comments' => true,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $this->actingAs($viewer)
        ->postJson(route('collections.comments.store', ['collection' => $collection->id]), [
            'body' => 'Love the visual direction here.',
        ])
        ->assertOk()
        ->assertJsonPath('comments.0.user.username', $viewer->username);

    $this->assertDatabaseHas('collection_comments', [
        'collection_id' => $collection->id,
        'user_id' => $viewer->id,
        'status' => Collection::COMMENT_VISIBLE,
    ]);
});

it('smart preview only resolves artworks owned by the requesting user', function () {
    $owner = User::factory()->create();
    $otherUser = User::factory()->create();
    $tag = \App\Models\Tag::factory()->create([
        'name' => 'Cyberpunk',
        'slug' => 'cyberpunk',
    ]);

    $ownedArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Owned Neon City',
        'slug' => 'owned-neon-city',
    ]);
    $foreignArtwork = Artwork::factory()->for($otherUser)->create([
        'title' => 'Foreign Neon City',
        'slug' => 'foreign-neon-city',
    ]);

    DB::table('artwork_tag')->insert([
        [
            'artwork_id' => $ownedArtwork->id,
            'tag_id' => $tag->id,
            'source' => 'user',
            'confidence' => null,
        ],
        [
            'artwork_id' => $foreignArtwork->id,
            'tag_id' => $tag->id,
            'source' => 'user',
            'confidence' => null,
        ],
    ]);

    $response = $this->actingAs($owner)
        ->postJson(route('settings.collections.smart.preview'), [
            'smart_rules_json' => [
                'match' => 'all',
                'sort' => 'newest',
                'rules' => [
                    [
                        'field' => 'tags',
                        'operator' => 'contains',
                        'value' => 'cyberpunk',
                    ],
                ],
            ],
        ])
        ->assertOk()
        ->json('preview');

    expect($response['count'])->toBe(1);
    expect(collect($response['artworks']['data'])->pluck('id')->all())->toBe([$ownedArtwork->id]);
    expect(collect($response['artworks']['data'])->pluck('title')->all())->toContain('Owned Neon City');
    expect(collect($response['artworks']['data'])->pluck('title')->all())->not->toContain('Foreign Neon City');
});

it('smart preview can resolve artworks by medium using owned content types', function () {
    $owner = User::factory()->create();
    $contentType = ContentType::query()->create([
        'name' => 'Wallpapers',
        'slug' => 'wallpapers',
        'description' => 'Wallpaper uploads',
    ]);
    $rootCategory = Category::query()->create([
        'content_type_id' => $contentType->id,
        'parent_id' => null,
        'name' => 'Abstract',
        'slug' => 'abstract',
        'description' => 'Abstract wallpapers',
        'is_active' => true,
        'sort_order' => 1,
    ]);

    $matchingArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Wallpaper Study',
        'slug' => 'wallpaper-study',
    ]);
    $otherArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Uncategorized Study',
        'slug' => 'uncategorized-study',
    ]);

    DB::table('artwork_category')->insert([
        'artwork_id' => $matchingArtwork->id,
        'category_id' => $rootCategory->id,
    ]);

    $response = $this->actingAs($owner)
        ->postJson(route('settings.collections.smart.preview'), [
            'smart_rules_json' => [
                'match' => 'all',
                'sort' => 'newest',
                'rules' => [
                    [
                        'field' => 'medium',
                        'operator' => 'equals',
                        'value' => 'wallpapers',
                    ],
                ],
            ],
        ])
        ->assertOk()
        ->json('preview');

    expect($response['count'])->toBe(1);
    expect(collect($response['artworks']['data'])->pluck('id')->all())->toBe([$matchingArtwork->id]);
    expect(collect($response['artworks']['data'])->pluck('title')->all())->toContain('Wallpaper Study');
    expect(collect($response['artworks']['data'])->pluck('title')->all())->not->toContain('Uncategorized Study');
});

it('smart collections do not accept manual artwork attachments', function () {
    $owner = User::factory()->create();
    $artwork = Artwork::factory()->for($owner)->create();
    $collection = Collection::factory()->for($owner)->create([
        'mode' => Collection::MODE_SMART,
        'sort_mode' => Collection::SORT_NEWEST,
        'smart_rules_json' => [
            'match' => 'all',
            'sort' => 'newest',
            'rules' => [
                [
                    'field' => 'tags',
                    'operator' => 'contains',
                    'value' => 'cyberpunk',
                ],
            ],
        ],
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.artworks.attach', ['collection' => $collection->id]), [
            'artwork_ids' => [$artwork->id],
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors('collection');

    $this->assertDatabaseMissing('collection_artwork', [
        'collection_id' => $collection->id,
        'artwork_id' => $artwork->id,
    ]);
});

it('collection managers can request AI curation suggestions', function () {
    $owner = User::factory()->create();
    $artwork = Artwork::factory()->for($owner)->create([
        'title' => 'Neon Horizon',
        'slug' => 'neon-horizon',
    ]);
    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'allow_submissions' => true,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'title' => 'Untitled Community Set',
        'slug' => 'untitled-community-set',
    ]);
    $tag = Tag::query()->create([
        'name' => 'Cyberpunk',
        'slug' => 'cyberpunk',
        'usage_count' => 0,
        'is_active' => true,
    ]);

    DB::table('artwork_tag')->insert([
        'artwork_id' => $artwork->id,
        'tag_id' => $tag->id,
        'source' => 'ai',
        'confidence' => 0.98,
        'created_at' => now(),
    ]);

    app(CollectionService::class)->attachArtworks($collection, $owner, [$artwork->id]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-title', ['collection' => $collection->id]), [
            'draft' => ['title' => $collection->title],
        ])
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-summary', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-cover', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.artwork.id', $artwork->id);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-grouping', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');
});

it('collection AI suggestion endpoints are permission protected', function () {
    $owner = User::factory()->create();
    $outsider = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create();

    $this->actingAs($outsider)
        ->postJson(route('settings.collections.ai.suggest-title', ['collection' => $collection->id]))
        ->assertForbidden();
});

it('collection managers can request extended AI curation suggestions', function () {
    $owner = User::factory()->create();
    $tag = Tag::query()->create([
        'name' => 'Minimal',
        'slug' => 'minimal',
        'usage_count' => 0,
        'is_active' => true,
    ]);
    $attachedArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Minimal Study One',
        'slug' => 'minimal-study-one',
    ]);
    $suggestedArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Minimal Study Two',
        'slug' => 'minimal-study-two',
    ]);

    foreach ([$attachedArtwork, $suggestedArtwork] as $artwork) {
        DB::table('artwork_tag')->insert([
            'artwork_id' => $artwork->id,
            'tag_id' => $tag->id,
            'source' => 'user',
            'confidence' => null,
            'created_at' => now(),
        ]);
    }

    $collection = Collection::factory()->for($owner)->create([
        'type' => Collection::TYPE_COMMUNITY,
        'mode' => Collection::MODE_SMART,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'smart_rules_json' => [
            'match' => 'all',
            'sort' => 'newest',
            'rules' => [
                [
                    'field' => 'tags',
                    'operator' => 'contains',
                    'value' => 'minimal',
                ],
            ],
        ],
    ]);

    app(CollectionService::class)->attachArtworks($collection->forceFill(['mode' => Collection::MODE_MANUAL]), $owner, [$attachedArtwork->id]);
    $collection->forceFill([
        'mode' => Collection::MODE_SMART,
        'smart_rules_json' => [
            'match' => 'all',
            'sort' => 'newest',
            'rules' => [[
                'field' => 'tags',
                'operator' => 'contains',
                'value' => 'minimal',
            ]],
        ],
    ])->save();

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-related-artworks', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai')
        ->assertJsonPath('suggestion.artworks.0.id', $suggestedArtwork->id);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-tags', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-seo-description', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.explain-smart-rules', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-split-themes', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-merge-idea', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai');
});

it('collection managers can request AI metadata diagnostics and stale refresh suggestions', function () {
    $owner = User::factory()->create();
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Untitled Collection',
        'slug' => 'untitled-collection',
        'summary' => null,
        'description' => 'Short mood note.',
        'cover_artwork_id' => null,
        'metadata_completeness_score' => 42.0,
        'freshness_score' => 18.0,
        'updated_at' => now()->subDays(90),
        'last_activity_at' => now()->subDays(76),
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.detect-weak-metadata', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.status', 'needs_work')
        ->assertJsonPath('suggestion.source', 'heuristic-ai')
        ->assertJson(fn ($json) => $json
            ->where('suggestion.issues', fn ($issues): bool => collect($issues)
                ->pluck('key')
                ->intersect(['title', 'summary', 'description', 'cover', 'theme', 'metadata_score'])
                ->count() >= 5));

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-stale-refresh', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.stale', true)
        ->assertJsonPath('suggestion.source', 'heuristic-ai')
        ->assertJson(fn ($json) => $json
            ->where('suggestion.actions', fn ($actions): bool => collect($actions)
                ->pluck('key')
                ->intersect(['refresh_summary', 'set_cover', 'add_recent_artworks'])
                ->count() === 3));
});

it('healthy collections return a clean AI metadata diagnostic', function () {
    $owner = User::factory()->create();
    $artwork = Artwork::factory()->for($owner)->create([
        'title' => 'Ocean Light Study',
        'slug' => 'ocean-light-study',
    ]);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Ocean Light Editorial',
        'slug' => 'ocean-light-editorial',
        'summary' => 'A focused selection of ocean-lit studies with a clean editorial rhythm.',
        'description' => 'A longer curatorial description that explains the atmosphere, pacing, and recurring light studies across the featured works for discovery surfaces.',
        'cover_artwork_id' => $artwork->id,
        'metadata_completeness_score' => 88.0,
        'freshness_score' => 82.0,
        'updated_at' => now()->subDays(7),
        'last_activity_at' => now()->subDays(3),
    ]);
    $tag = Tag::query()->create([
        'name' => 'Oceanic',
        'slug' => 'oceanic',
        'usage_count' => 0,
        'is_active' => true,
    ]);

    DB::table('artwork_tag')->insert([
        'artwork_id' => $artwork->id,
        'tag_id' => $tag->id,
        'source' => 'user',
        'confidence' => null,
        'created_at' => now(),
    ]);

    app(CollectionService::class)->attachArtworks($collection, $owner, [$artwork->id]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.detect-weak-metadata', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.status', 'healthy')
        ->assertJsonPath('suggestion.issues', []);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-stale-refresh', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.stale', false)
        ->assertJsonPath('suggestion.actions', []);
});

it('collection managers can request AI campaign fit and related collection link suggestions', function () {
    $owner = User::factory()->create(['username' => 'campaignfitowner']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Spring Night Editorial',
        'slug' => 'spring-night-editorial',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'season_key' => 'spring',
        'event_key' => 'spring-lights-2026',
        'event_label' => 'Spring Lights 2026',
        'ranking_score' => 81,
    ]);

    $campaignCandidate = Collection::factory()->for($owner)->create([
        'title' => 'Spring Lights Campaign Hub',
        'slug' => 'spring-lights-campaign-hub',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'spring-lights',
        'campaign_label' => 'Spring Lights',
        'season_key' => 'spring',
        'event_key' => 'spring-lights-2026',
        'event_label' => 'Spring Lights 2026',
        'ranking_score' => 88,
    ]);

    $relatedLinkCandidate = Collection::factory()->for($owner)->create([
        'title' => 'Spring Night Community Picks',
        'slug' => 'spring-night-community-picks',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'season_key' => 'spring',
        'event_key' => 'spring-lights-2026',
        'ranking_score' => 77,
    ]);

    $alreadyLinked = Collection::factory()->for($owner)->create([
        'title' => 'Already Linked Spring Feature',
        'slug' => 'already-linked-spring-feature',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'season_key' => 'spring',
        'event_key' => 'spring-lights-2026',
        'ranking_score' => 79,
    ]);

    DB::table('collection_related_links')->insert([
        'collection_id' => $collection->id,
        'related_collection_id' => $alreadyLinked->id,
        'sort_order' => 0,
        'created_by_user_id' => $owner->id,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-campaign-fit', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai')
        ->assertJsonPath('suggestion.eligibility.is_campaign_ready', true)
        ->assertJson(fn ($json) => $json
            ->where('suggestion.fits.0.campaign_key', 'spring-lights')
            ->where('suggestion.fits.0.campaign_label', 'Spring Lights')
            ->has('suggestion.recommended_surfaces'));

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.suggest-related-collections-to-link', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('suggestion.source', 'heuristic-ai')
        ->assertJson(fn ($json) => $json
            ->where('suggestion.linked_collection_ids.0', $alreadyLinked->id)
            ->where('suggestion.suggestions.0.id', $campaignCandidate->id)
            ->where('suggestion.suggestions.0.link_type', 'same_curator')
            ->where('suggestion.suggestions', fn ($suggestions): bool => collect($suggestions)->pluck('id')->contains($relatedLinkCandidate->id) && ! collect($suggestions)->pluck('id')->contains($alreadyLinked->id)));
});

it('admins can moderate collection visibility interactions and collaborators', function () {
    $admin = User::factory()->create(['role' => 'admin']);
    $owner = User::factory()->create(['username' => 'moderatedowner']);
    $contributor = User::factory()->create(['username' => 'modcollaborator']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Moderated Collection',
        'slug' => 'moderated-collection',
        'type' => Collection::TYPE_COMMUNITY,
        'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_comments' => true,
        'allow_submissions' => true,
        'allow_saves' => true,
        'is_featured' => true,
        'featured_at' => now(),
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);
    app(\App\Services\CollectionCollaborationService::class)->inviteMember($collection, $owner, $contributor, Collection::MEMBER_ROLE_CONTRIBUTOR);

    $member = \App\Models\CollectionMember::query()
        ->where('collection_id', $collection->id)
        ->where('user_id', $contributor->id)
        ->firstOrFail();

    $this->actingAs($admin)
        ->patchJson(route('api.admin.collections.moderation.update', ['collection' => $collection->id]), [
            'moderation_status' => Collection::MODERATION_RESTRICTED,
        ])
        ->assertOk()
        ->assertJsonPath('collection.moderation_status', Collection::MODERATION_RESTRICTED);

    $this->actingAs($admin)
        ->patchJson(route('api.admin.collections.interactions.update', ['collection' => $collection->id]), [
            'allow_comments' => false,
            'allow_submissions' => false,
            'allow_saves' => false,
        ])
        ->assertOk()
        ->assertJsonPath('collection.allow_comments', false)
        ->assertJsonPath('collection.allow_submissions', false)
        ->assertJsonPath('collection.allow_saves', false);

    $this->actingAs($admin)
        ->postJson(route('api.admin.collections.unfeature', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('collection.is_featured', false);

    $this->actingAs($admin)
        ->deleteJson(route('api.admin.collections.members.destroy', ['collection' => $collection->id, 'member' => $member->id]))
        ->assertOk();

    $collection->refresh();
    $member->refresh();

    expect($collection->moderation_status)->toBe(Collection::MODERATION_RESTRICTED);
    expect($collection->allow_comments)->toBeFalse();
    expect($collection->allow_submissions)->toBeFalse();
    expect($collection->allow_saves)->toBeFalse();
    expect($collection->is_featured)->toBeFalse();
    expect($member->status)->toBe(Collection::MEMBER_STATUS_REVOKED);

    auth()->logout();

    $this->get(route('profile.collections.show', ['username' => $owner->username, 'slug' => $collection->slug]))
        ->assertNotFound();
});

it('saved collections page lists the viewers saved public collections only', function () {
    $owner = User::factory()->create(['username' => 'savedowner']);
    $viewer = User::factory()->create();
    $publicCollection = Collection::factory()->for($owner)->create([
        'title' => 'Saved Public Collection',
        'slug' => 'saved-public-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_saves' => true,
    ]);
    $privateCollection = Collection::factory()->for($owner)->create([
        'title' => 'Hidden Saved Collection',
        'slug' => 'hidden-saved-collection',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'allow_saves' => true,
    ]);

    $this->actingAs($viewer)
        ->postJson(route('collections.save', ['collection' => $publicCollection->id]))
        ->assertOk();

    DB::table('collection_saves')->insert([
        'collection_id' => $privateCollection->id,
        'user_id' => $viewer->id,
        'created_at' => now(),
    ]);

    $this->actingAs($viewer)
        ->get(route('me.saved.collections'))
        ->assertOk()
        ->assertSee('Saved Public Collection', false)
        ->assertDontSee('Hidden Saved Collection', false);
});

it('admin can create an editorial collection under a staff account with persisted layout modules', function () {
    $admin = User::factory()->create([
        'username' => 'collectionsadmin',
        'role' => 'admin',
    ]);
    $staffOwner = User::factory()->create([
        'username' => 'staffcurator',
        'role' => 'moderator',
    ]);

    $response = $this->actingAs($admin)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Staff Picks Cyberpunk',
            'slug' => '',
            'type' => Collection::TYPE_EDITORIAL,
            'editorial_owner_mode' => Collection::EDITORIAL_OWNER_STAFF_ACCOUNT,
            'editorial_owner_username' => $staffOwner->username,
            'description' => 'Premium curation for the homepage feature strip.',
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
            'layout_modules_json' => [
                ['key' => 'editorial_note', 'enabled' => true, 'slot' => 'full'],
                ['key' => 'artwork_grid', 'enabled' => true, 'slot' => 'main'],
                ['key' => 'discussion', 'enabled' => false, 'slot' => 'sidebar'],
            ],
        ])
        ->assertOk()
        ->assertJsonPath('collection.owner.username', $staffOwner->username)
        ->assertJsonPath('collection.owner.mode', Collection::EDITORIAL_OWNER_STAFF_ACCOUNT)
        ->assertJsonPath('collection.layout_modules.0.key', 'editorial_note')
        ->assertJsonPath('collection.layout_modules.1.key', 'artwork_grid');

    $collectionId = (int) $response->json('collection.id');
    $collection = Collection::query()->findOrFail($collectionId);

    expect((int) $collection->user_id)->toBe((int) $staffOwner->id);
    expect((int) $collection->managed_by_user_id)->toBe((int) $admin->id);
    expect((int) $collection->editorial_owner_user_id)->toBe((int) $staffOwner->id);
    expect($collection->editorial_owner_mode)->toBe(Collection::EDITORIAL_OWNER_STAFF_ACCOUNT);
    expect(collect($collection->layout_modules_json)->pluck('key')->take(2)->values()->all())
        ->toBe(['editorial_note', 'artwork_grid']);
    expect((bool) collect($collection->layout_modules_json)->firstWhere('key', 'discussion')['enabled'])
        ->toBeFalse();

    $this->get(route('profile.collections.show', ['username' => $staffOwner->username, 'slug' => $collection->slug]))
        ->assertOk()
        ->assertSee('Staff Picks Cyberpunk', false);
});

it('admin can create a system-owned editorial collection with a dedicated public label', function () {
    $admin = User::factory()->create([
        'username' => 'systemeditoradmin',
        'role' => 'admin',
    ]);
    $systemOwner = User::factory()->create([
        'username' => 'skinbaseeditorial',
        'role' => 'admin',
    ]);

    config()->set('collections.editorial.system_owner_username', $systemOwner->username);
    config()->set('collections.editorial.system_owner_label', 'Skinbase Editorial');

    $response = $this->actingAs($admin)
        ->postJson(route('settings.collections.store'), [
            'title' => 'Nova Launch Highlights',
            'slug' => '',
            'type' => Collection::TYPE_EDITORIAL,
            'editorial_owner_mode' => Collection::EDITORIAL_OWNER_SYSTEM,
            'editorial_owner_label' => 'Nova Editorial Desk',
            'description' => 'Collections supporting launch-week editorial coverage.',
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'sort_mode' => Collection::SORT_MANUAL,
        ])
        ->assertOk()
        ->assertJsonPath('collection.owner.is_system', true)
        ->assertJsonPath('collection.owner.name', 'Nova Editorial Desk');

    $collectionId = (int) $response->json('collection.id');
    $collection = Collection::query()->findOrFail($collectionId);

    expect((int) $collection->user_id)->toBe((int) $systemOwner->id);
    expect((int) $collection->managed_by_user_id)->toBe((int) $admin->id);
    expect((int) $collection->editorial_owner_user_id)->toBe((int) $systemOwner->id);
    expect($collection->editorial_owner_label)->toBe('Nova Editorial Desk');

    $this->get(route('profile.collections.show', ['username' => $systemOwner->username, 'slug' => $collection->slug]))
        ->assertOk()
        ->assertSee('Nova Editorial Desk', false);
});

it('related collection recommendations do not leak private collections', function () {
    $owner = User::factory()->create(['username' => 'relatedowner']);
    $tag = Tag::query()->create([
        'name' => 'Minimal',
        'slug' => 'minimal',
        'usage_count' => 0,
        'is_active' => true,
    ]);

    $baseCollection = Collection::factory()->for($owner)->create([
        'title' => 'Base Collection',
        'slug' => 'base-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'type' => Collection::TYPE_COMMUNITY,
    ]);
    $publicRelated = Collection::factory()->for($owner)->create([
        'title' => 'Visible Related Collection',
        'slug' => 'visible-related-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'type' => Collection::TYPE_COMMUNITY,
    ]);
    $privateRelated = Collection::factory()->for($owner)->create([
        'title' => 'Private Related Collection',
        'slug' => 'private-related-collection',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'type' => Collection::TYPE_COMMUNITY,
    ]);

    $baseArtwork = Artwork::factory()->for($owner)->create();
    $publicArtwork = Artwork::factory()->for($owner)->create();
    $privateArtwork = Artwork::factory()->for($owner)->create();

    foreach ([$baseArtwork, $publicArtwork, $privateArtwork] as $artwork) {
        DB::table('artwork_tag')->insert([
            'artwork_id' => $artwork->id,
            'tag_id' => $tag->id,
            'source' => 'user',
            'confidence' => null,
            'created_at' => now(),
        ]);
    }

    app(CollectionService::class)->attachArtworks($baseCollection, $owner, [$baseArtwork->id]);
    app(CollectionService::class)->attachArtworks($publicRelated, $owner, [$publicArtwork->id]);
    app(CollectionService::class)->attachArtworks($privateRelated, $owner, [$privateArtwork->id]);

    $this->get(route('profile.collections.show', ['username' => $owner->username, 'slug' => $baseCollection->slug]))
        ->assertOk()
        ->assertSee('Visible Related Collection', false)
        ->assertDontSee('Private Related Collection', false);
});

it('trending discovery route only shows public collections ordered by engagement-friendly signals', function () {
    $owner = User::factory()->create(['username' => 'trendowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Quiet Public Collection',
        'slug' => 'quiet-public-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'likes_count' => 1,
        'followers_count' => 0,
        'saves_count' => 0,
        'comments_count' => 0,
        'views_count' => 1,
        'last_activity_at' => now()->subDay(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Hot Public Collection',
        'slug' => 'hot-public-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'likes_count' => 20,
        'followers_count' => 8,
        'saves_count' => 7,
        'comments_count' => 5,
        'views_count' => 400,
        'last_activity_at' => now(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Private Trending Collection',
        'slug' => 'private-trending-collection',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'likes_count' => 999,
        'followers_count' => 999,
        'saves_count' => 999,
        'comments_count' => 999,
        'views_count' => 9999,
        'last_activity_at' => now(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Placement Blocked Trending Collection',
        'slug' => 'placement-blocked-trending-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'placement_eligibility' => false,
        'likes_count' => 500,
        'followers_count' => 300,
        'saves_count' => 200,
        'comments_count' => 100,
        'views_count' => 9000,
        'last_activity_at' => now(),
    ]);

    $this->get(route('collections.trending'))
        ->assertOk()
        ->assertSee('Trending collections', false)
        ->assertSeeInOrder(['Hot Public Collection', 'Quiet Public Collection'])
        ->assertDontSee('Private Trending Collection', false)
        ->assertDontSee('Placement Blocked Trending Collection', false);
});

it('editorial discovery route only shows editorial public collections', function () {
    $owner = User::factory()->create(['username' => 'editorialowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Staff Picks Spring',
        'slug' => 'staff-picks-spring',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Community Picks Spring',
        'slug' => 'community-picks-spring',
        'type' => Collection::TYPE_COMMUNITY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    $this->get(route('collections.editorial'))
        ->assertOk()
        ->assertSee('Editorial collections', false)
        ->assertSee('Staff Picks Spring', false)
        ->assertDontSee('Community Picks Spring', false);
});

it('community discovery route only shows community public collections', function () {
    $owner = User::factory()->create(['username' => 'communityowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Open Theme Jam',
        'slug' => 'open-theme-jam',
        'type' => Collection::TYPE_COMMUNITY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Curator Portfolio',
        'slug' => 'curator-portfolio',
        'type' => Collection::TYPE_PERSONAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    $this->get(route('collections.community'))
        ->assertOk()
        ->assertSee('Community collections', false)
        ->assertSee('Open Theme Jam', false)
        ->assertDontSee('Curator Portfolio', false);
});

it('seasonal discovery route only shows public collections tagged for events or seasons', function () {
    $owner = User::factory()->create(['username' => 'seasonowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Winter Wallpapers 2026',
        'slug' => 'winter-wallpapers-2026',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'season_key' => 'winter-2026',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Evergreen Studio Set',
        'slug' => 'evergreen-studio-set',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'season_key' => null,
        'event_key' => null,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Private Holiday Vault',
        'slug' => 'private-holiday-vault',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'event_key' => 'holiday-2026',
    ]);

    $this->get(route('collections.seasonal'))
        ->assertOk()
        ->assertSee('Seasonal and event collections', false)
        ->assertSee('Winter Wallpapers 2026', false)
        ->assertDontSee('Evergreen Studio Set', false)
        ->assertDontSee('Private Holiday Vault', false);
});

it('campaign landing page shows public campaign collections and excludes restricted entries', function () {
    $owner = User::factory()->create(['username' => 'campaignlandingowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Winter Launch Editorial',
        'slug' => 'winter-launch-editorial',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'winter-launch',
        'campaign_label' => 'Winter Launch',
        'event_label' => 'Winter Spotlight 2026',
        'badge_label' => 'Campaign Live',
        'banner_text' => 'A campaign landing page for winter launch collections.',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Winter Launch Community',
        'slug' => 'winter-launch-community',
        'type' => Collection::TYPE_COMMUNITY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'winter-launch',
        'campaign_label' => 'Winter Launch',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Placement Blocked Winter Launch',
        'slug' => 'placement-blocked-winter-launch',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => false,
        'campaign_key' => 'winter-launch',
        'campaign_label' => 'Winter Launch',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Private Winter Launch Vault',
        'slug' => 'private-winter-launch-vault',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'winter-launch',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Restricted Winter Launch Set',
        'slug' => 'restricted-winter-launch-set',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_RESTRICTED,
        'moderation_status' => Collection::MODERATION_RESTRICTED,
        'campaign_key' => 'winter-launch',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Different Campaign Collection',
        'slug' => 'different-campaign-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'spring-launch',
    ]);

    $this->get(route('collections.campaign.show', ['campaignKey' => 'winter-launch']))
        ->assertOk()
        ->assertSee('Winter Launch', false)
        ->assertSee('Winter Launch Editorial', false)
        ->assertSee('Winter Launch Community', false)
        ->assertDontSee('Placement Blocked Winter Launch', false)
        ->assertDontSee('Private Winter Launch Vault', false)
        ->assertDontSee('Restricted Winter Launch Set', false)
        ->assertDontSee('Different Campaign Collection', false)
        ->assertSee('Campaign Live', false)
        ->assertSee('Winter Spotlight 2026', false);
});

it('program landing page shows partner and sponsor metadata for public eligible collections', function () {
    $owner = User::factory()->create(['username' => 'programlandingfeatureowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Program Landing Hero',
        'slug' => 'program-landing-hero',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => true,
        'program_key' => 'editorial-spring',
        'partner_label' => 'Editorial Board',
        'sponsorship_label' => 'Brand Partner',
        'promotion_tier' => 'priority',
        'trust_tier' => 'trusted',
        'banner_text' => 'Editorial Spring Program',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Program Landing Hidden',
        'slug' => 'program-landing-hidden',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => true,
        'program_key' => 'editorial-spring',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Program Landing Ineligible',
        'slug' => 'program-landing-ineligible',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => false,
        'program_key' => 'editorial-spring',
    ]);

    $this->get(route('collections.program.show', ['programKey' => 'editorial-spring']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->where('program.key', 'editorial-spring')
            ->where('program.label', 'Editorial Spring Program')
            ->where('program.partner_labels.0', 'Editorial Board')
            ->where('program.sponsorship_labels.0', 'Brand Partner')
            ->has('collections', 1)
            ->where('collections.0.title', 'Program Landing Hero'));
});

it('authenticated viewers can report visible collections comments and submissions', function () {
    $owner = User::factory()->create(['username' => 'reportowner']);
    $viewer = User::factory()->create(['username' => 'reportviewer']);
    $submitter = User::factory()->create(['username' => 'reportsubmitter']);
    $artwork = Artwork::factory()->for($submitter)->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'allow_comments' => true,
        'allow_submissions' => true,
        'type' => Collection::TYPE_COMMUNITY,
        'mode' => Collection::MODE_MANUAL,
        'collaboration_mode' => Collection::COLLABORATION_OPEN,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $comment = app(\App\Services\CollectionCommentService::class)->create($collection, $submitter, 'Reportable comment');
    $submission = app(\App\Services\CollectionSubmissionService::class)->submit($collection, $submitter, $artwork);

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'collection',
            'target_id' => $collection->id,
            'reason' => 'Misleading curation',
        ])
        ->assertCreated();

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'collection_comment',
            'target_id' => $comment->id,
            'reason' => 'Harassment',
        ])
        ->assertCreated();

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'collection_submission',
            'target_id' => $submission->id,
            'reason' => 'Abusive submission',
        ])
        ->assertCreated();

    expect(Report::query()->where('reporter_id', $viewer->id)->count())->toBe(3);
});

it('users cannot report private collection entities they cannot view', function () {
    $owner = User::factory()->create();
    $viewer = User::factory()->create();
    $submitter = User::factory()->create();
    $artwork = Artwork::factory()->for($submitter)->create();
    $collection = Collection::factory()->for($owner)->create([
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'allow_comments' => true,
        'allow_submissions' => true,
        'type' => Collection::TYPE_COMMUNITY,
        'mode' => Collection::MODE_MANUAL,
        'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $comment = app(\App\Services\CollectionCommentService::class)->create($collection, $owner, 'Private comment');
    $submission = \App\Models\CollectionSubmission::query()->create([
        'collection_id' => $collection->id,
        'artwork_id' => $artwork->id,
        'user_id' => $submitter->id,
        'message' => 'Private submission',
        'status' => Collection::SUBMISSION_PENDING,
    ]);

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'collection',
            'target_id' => $collection->id,
            'reason' => 'Should fail',
        ])
        ->assertForbidden();

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'collection_comment',
            'target_id' => $comment->id,
            'reason' => 'Should fail',
        ])
        ->assertForbidden();

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'collection_submission',
            'target_id' => $submission->id,
            'reason' => 'Should fail',
        ])
        ->assertForbidden();
});

it('dashboard summarises only the owners collections and highlights attention queues', function () {
    $owner = User::factory()->create(['username' => 'dashboardowner']);
    $otherOwner = User::factory()->create(['username' => 'dashboardother']);
    $submitter = User::factory()->create(['username' => 'dashsubmitter']);

    $draft = Collection::factory()->for($owner)->create([
        'title' => 'Dashboard Draft',
        'slug' => 'dashboard-draft',
        'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
        'ranking_score' => 10,
    ]);
    $scheduled = Collection::factory()->for($owner)->create([
        'title' => 'Dashboard Scheduled',
        'slug' => 'dashboard-scheduled',
        'lifecycle_state' => Collection::LIFECYCLE_SCHEDULED,
        'ranking_score' => 40,
    ]);
    $publishedHealthy = Collection::factory()->for($owner)->create([
        'title' => 'Dashboard Healthy',
        'slug' => 'dashboard-healthy',
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'ranking_score' => 99,
        'quality_score' => 93,
    ]);
    $publishedLowQuality = Collection::factory()->for($owner)->create([
        'title' => 'Dashboard Low Quality',
        'slug' => 'dashboard-low-quality',
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'ranking_score' => 70,
        'quality_score' => 31,
    ]);
    $archivedRestricted = Collection::factory()->for($owner)->create([
        'title' => 'Dashboard Restricted',
        'slug' => 'dashboard-restricted',
        'lifecycle_state' => Collection::LIFECYCLE_ARCHIVED,
        'ranking_score' => 80,
        'moderation_status' => Collection::MODERATION_RESTRICTED,
    ]);
    $expiringCampaign = Collection::factory()->for($owner)->create([
        'title' => 'Dashboard Expiring',
        'slug' => 'dashboard-expiring',
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'ranking_score' => 85,
        'unpublished_at' => now()->addDays(7),
    ]);

    Collection::factory()->for($otherOwner)->create([
        'title' => 'Other Owner Collection',
        'slug' => 'other-owner-collection',
        'ranking_score' => 500,
        'quality_score' => 99,
    ]);

    $firstArtwork = Artwork::factory()->for($submitter)->create();
    $secondArtwork = Artwork::factory()->for($submitter)->create();
    $approvedArtwork = Artwork::factory()->for($submitter)->create();

    DB::table('collection_submissions')->insert([
        [
            'collection_id' => $scheduled->id,
            'artwork_id' => $firstArtwork->id,
            'user_id' => $submitter->id,
            'message' => 'Pending submission one',
            'status' => Collection::SUBMISSION_PENDING,
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'collection_id' => $publishedLowQuality->id,
            'artwork_id' => $secondArtwork->id,
            'user_id' => $submitter->id,
            'message' => 'Pending submission two',
            'status' => Collection::SUBMISSION_PENDING,
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'collection_id' => $publishedLowQuality->id,
            'artwork_id' => $approvedArtwork->id,
            'user_id' => $owner->id,
            'message' => 'Approved submission',
            'status' => Collection::SUBMISSION_APPROVED,
            'created_at' => now(),
            'updated_at' => now(),
        ],
    ]);

    $response = $this->actingAs($owner)
        ->get(route('settings.collections.dashboard'))
        ->assertOk();

    $page = $response->viewData('page');
    expect(data_get($page, 'component'))->toBe('Collection/CollectionDashboard');
    expect(data_get($page, 'props.summary'))->toMatchArray([
        'total' => 6,
        'drafts' => 1,
        'scheduled' => 1,
        'published' => 3,
        'archived' => 1,
        'pending_submissions' => 2,
    ]);
    expect(collect(data_get($page, 'props.topPerforming', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toBe([
            $publishedHealthy->id,
            $expiringCampaign->id,
            $archivedRestricted->id,
            $publishedLowQuality->id,
            $scheduled->id,
            $draft->id,
        ]);
    expect(collect(data_get($page, 'props.needsAttention', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toBe([
            $archivedRestricted->id,
            $publishedLowQuality->id,
            $draft->id,
        ]);
    expect(collect(data_get($page, 'props.expiringCampaigns', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toBe([$expiringCampaign->id]);
});

it('owners can view collection analytics with deltas timeline and top artworks', function () {
    $owner = User::factory()->create(['username' => 'analyticsowner']);
    $submitterA = User::factory()->create(['username' => 'analyticssubmittera']);
    $submitterB = User::factory()->create(['username' => 'analyticssubmitterb']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Analytics Collection',
        'slug' => 'analytics-collection',
        'views_count' => 120,
        'likes_count' => 30,
        'followers_count' => 10,
        'saves_count' => 7,
        'comments_count' => 5,
        'shares_count' => 4,
    ]);

    $artworkA = Artwork::factory()->for($owner)->create([
        'title' => 'Analytics Artwork A',
        'slug' => 'analytics-artwork-a',
    ]);
    $artworkB = Artwork::factory()->for($owner)->create([
        'title' => 'Analytics Artwork B',
        'slug' => 'analytics-artwork-b',
    ]);

    DB::table('collection_artwork')->insert([
        [
            'collection_id' => $collection->id,
            'artwork_id' => $artworkA->id,
            'order_num' => 0,
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'collection_id' => $collection->id,
            'artwork_id' => $artworkB->id,
            'order_num' => 1,
            'created_at' => now(),
            'updated_at' => now(),
        ],
    ]);

    DB::table('artwork_stats')->insert([
        'artwork_id' => $artworkA->id,
        'views' => 500,
        'downloads' => 0,
        'favorites' => 40,
        'comments_count' => 0,
        'shares_count' => 12,
        'rating_avg' => 0,
        'rating_count' => 0,
        'views_24h' => 0,
        'views_7d' => 0,
        'downloads_24h' => 0,
        'downloads_7d' => 0,
        'shares_24h' => 0,
        'comments_24h' => 0,
        'favourites_24h' => 0,
        'ranking_score' => 88.5,
        'engagement_velocity' => 0,
        'heat_score' => 0,
        'heat_score_updated_at' => null,
        'views_1h' => 0,
        'favourites_1h' => 0,
        'comments_1h' => 0,
        'shares_1h' => 0,
        'downloads_1h' => 0,
    ]);
    DB::table('artwork_stats')->insert([
        'artwork_id' => $artworkB->id,
        'views' => 150,
        'downloads' => 0,
        'favorites' => 9,
        'comments_count' => 0,
        'shares_count' => 3,
        'rating_avg' => 0,
        'rating_count' => 0,
        'views_24h' => 0,
        'views_7d' => 0,
        'downloads_24h' => 0,
        'downloads_7d' => 0,
        'shares_24h' => 0,
        'comments_24h' => 0,
        'favourites_24h' => 0,
        'ranking_score' => 44.2,
        'engagement_velocity' => 0,
        'heat_score' => 0,
        'heat_score_updated_at' => null,
        'views_1h' => 0,
        'favourites_1h' => 0,
        'comments_1h' => 0,
        'shares_1h' => 0,
        'downloads_1h' => 0,
    ]);

    DB::table('collection_submissions')->insert([
        [
            'collection_id' => $collection->id,
            'artwork_id' => $artworkA->id,
            'user_id' => $submitterA->id,
            'message' => 'Submission A',
            'status' => Collection::SUBMISSION_PENDING,
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'collection_id' => $collection->id,
            'artwork_id' => $artworkB->id,
            'user_id' => $submitterB->id,
            'message' => 'Submission B',
            'status' => Collection::SUBMISSION_APPROVED,
            'created_at' => now(),
            'updated_at' => now(),
        ],
    ]);

    DB::table('collection_daily_stats')->insert([
        [
            'collection_id' => $collection->id,
            'stat_date' => now()->subDays(2)->toDateString(),
            'views_count' => 20,
            'likes_count' => 5,
            'follows_count' => 2,
            'saves_count' => 1,
            'comments_count' => 1,
            'shares_count' => 0,
            'submissions_count' => 1,
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'collection_id' => $collection->id,
            'stat_date' => now()->subDay()->toDateString(),
            'views_count' => 75,
            'likes_count' => 15,
            'follows_count' => 6,
            'saves_count' => 4,
            'comments_count' => 3,
            'shares_count' => 2,
            'submissions_count' => 2,
            'created_at' => now(),
            'updated_at' => now(),
        ],
        [
            'collection_id' => $collection->id,
            'stat_date' => now()->toDateString(),
            'views_count' => 120,
            'likes_count' => 30,
            'follows_count' => 10,
            'saves_count' => 7,
            'comments_count' => 5,
            'shares_count' => 4,
            'submissions_count' => 2,
            'created_at' => now(),
            'updated_at' => now(),
        ],
    ]);

    $response = $this->actingAs($owner)
        ->get(route('settings.collections.analytics', ['collection' => $collection->id, 'days' => 14]))
        ->assertOk();

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

    expect(data_get($page, 'component'))->toBe('Collection/CollectionAnalytics');
    expect(data_get($analytics, 'totals'))->toMatchArray([
        'views' => 120,
        'likes' => 30,
        'follows' => 10,
        'saves' => 7,
        'comments' => 5,
        'shares' => 4,
        'submissions' => 2,
    ]);
    expect(data_get($analytics, 'range'))->toMatchArray([
        'days' => 14,
        'views_delta' => 100,
        'likes_delta' => 25,
        'follows_delta' => 8,
        'saves_delta' => 6,
        'comments_delta' => 4,
    ]);
    expect(collect(data_get($analytics, 'timeline', []))->pluck('date')->all())
        ->toBe([
            now()->subDays(2)->toDateString(),
            now()->subDay()->toDateString(),
            now()->toDateString(),
        ]);
    expect(collect(data_get($analytics, 'top_artworks', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toBe([$artworkA->id, $artworkB->id]);
    expect(data_get($analytics, 'top_artworks.0.ranking_score'))->toBe(88.5);
    expect(data_get($page, 'props.historyUrl'))->toBe(route('settings.collections.history', ['collection' => $collection->id]));
});

it('active collection editors can access collection analytics and history', function () {
    $owner = User::factory()->create(['username' => 'insightowner']);
    $editor = User::factory()->create(['username' => 'insighteditor']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Collaborative Insights Collection',
        'slug' => 'collaborative-insights-collection',
        'type' => Collection::TYPE_COMMUNITY,
        'collaboration_mode' => Collection::COLLABORATION_INVITE_ONLY,
    ]);

    app(\App\Services\CollectionCollaborationService::class)->ensureOwnerMembership($collection);

    $member = app(\App\Services\CollectionCollaborationService::class)
        ->inviteMember($collection, $owner, $editor, Collection::MEMBER_ROLE_EDITOR);

    app(\App\Services\CollectionCollaborationService::class)->acceptInvite($member, $editor);

    $this->actingAs($editor)
        ->get(route('settings.collections.analytics', ['collection' => $collection->id]))
        ->assertOk();

    $this->actingAs($editor)
        ->get(route('settings.collections.history', ['collection' => $collection->id]))
        ->assertOk();
});

it('non collaborators cannot access collection analytics or history', function () {
    $owner = User::factory()->create(['username' => 'analyticsprotectedowner']);
    $viewer = User::factory()->create(['username' => 'analyticsblockedviewer']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Protected Insights Collection',
        'slug' => 'protected-insights-collection',
    ]);

    $this->actingAs($viewer)
        ->get(route('settings.collections.analytics', ['collection' => $collection->id]))
        ->assertForbidden();

    $this->actingAs($viewer)
        ->get(route('settings.collections.history', ['collection' => $collection->id]))
        ->assertForbidden();
});

it('owner can update presentation settings through the dedicated presentation workflow route', function () {
    $owner = User::factory()->create(['username' => 'presentationowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Presentation Workflow Collection',
        'slug' => 'presentation-workflow-collection',
        'subtitle' => null,
        'summary' => null,
        'presentation_style' => Collection::PRESENTATION_STANDARD,
        'emphasis_mode' => Collection::EMPHASIS_BALANCED,
        'theme_token' => null,
        'layout_modules_json' => null,
    ]);

    $response = $this->actingAs($owner)
        ->postJson(route('settings.collections.presentation', ['collection' => $collection->id]), [
            'subtitle' => 'Premium showcase layout',
            'summary' => 'Tuned for hero-grid presentation.',
            'presentation_style' => Collection::PRESENTATION_HERO_GRID,
            'emphasis_mode' => Collection::EMPHASIS_COVER_HEAVY,
            'theme_token' => 'amber',
            'layout_modules_json' => [
                ['key' => 'editorial_note', 'enabled' => true, 'slot' => 'full'],
                ['key' => 'artwork_grid', 'enabled' => true, 'slot' => 'main'],
            ],
        ])
        ->assertOk()
        ->assertJsonPath('collection.subtitle', 'Premium showcase layout')
        ->assertJsonPath('collection.presentation_style', Collection::PRESENTATION_HERO_GRID)
        ->assertJsonPath('collection.emphasis_mode', Collection::EMPHASIS_COVER_HEAVY)
        ->assertJsonPath('collection.theme_token', 'amber');

    $responseKeys = collect($response->json('collection.layout_modules', []))
        ->pluck('key')
        ->all();

    $collection->refresh();

    expect($collection->subtitle)->toBe('Premium showcase layout');
    expect($collection->summary)->toBe('Tuned for hero-grid presentation.');
    expect($collection->presentation_style)->toBe(Collection::PRESENTATION_HERO_GRID);
    expect($collection->emphasis_mode)->toBe(Collection::EMPHASIS_COVER_HEAVY);
    expect($collection->theme_token)->toBe('amber');
    expect($responseKeys)->toContain('editorial_note', 'artwork_grid');
    expect(collect($collection->layout_modules_json)->pluck('key')->all())
        ->toContain('editorial_note', 'artwork_grid');
});

it('public collection payload exposes premium intro and featured artwork layout modules', function () {
    $owner = User::factory()->create(['username' => 'presentationmodulesowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Presentation Modules Collection',
        'slug' => 'presentation-modules-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'published_at' => now()->subMinute(),
        'subtitle' => 'Hidden intro subtitle',
        'description' => 'This intro copy should disappear when the intro block is disabled.',
        'allow_comments' => true,
        'layout_modules_json' => null,
    ]);

    $artworks = Artwork::factory()->count(3)->for($owner)->create([
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subMinute(),
    ]);

    foreach ($artworks as $index => $artwork) {
        $collection->artworks()->attach($artwork->id, ['order_num' => $index + 1]);
    }

    $collection->forceFill([
        'artworks_count' => $artworks->count(),
    ])->save();

    $response = $this->actingAs($owner)
        ->postJson(route('settings.collections.presentation', ['collection' => $collection->id]), [
            'layout_modules_json' => [
                ['key' => 'intro_block', 'enabled' => false, 'slot' => 'full'],
                ['key' => 'featured_artworks', 'enabled' => true, 'slot' => 'main'],
                ['key' => 'artwork_grid', 'enabled' => true, 'slot' => 'full'],
                ['key' => 'collaborators', 'enabled' => false, 'slot' => 'sidebar'],
                ['key' => 'discussion', 'enabled' => false, 'slot' => 'main'],
            ],
        ])
        ->assertOk()
        ->assertJsonPath('collection.layout_modules.0.key', 'intro_block');

    $request = \Illuminate\Http\Request::create((string) $response->json('collection.public_url'), 'GET', [], [], [], [
        'HTTP_X_INERTIA' => 'true',
        'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
    ]);
    $request->setUserResolver(fn () => $owner);

    $httpResponse = app(\App\Http\Controllers\User\ProfileCollectionController::class)
        ->show($request, strtolower((string) $owner->username), (string) $collection->fresh()->slug)
        ->toResponse($request);

    expect($httpResponse->getStatusCode())->toBe(200);

    $page = json_decode((string) $httpResponse->getContent(), true, 512, JSON_THROW_ON_ERROR);
    $layoutModules = collect(data_get($page, 'props.collection.layout_modules', []));

    expect((string) data_get($page, 'component'))->toBe('Collection/CollectionShow');
    expect((bool) $layoutModules->firstWhere('key', 'intro_block')['enabled'])->toBeFalse();
    expect((bool) $layoutModules->firstWhere('key', 'featured_artworks')['enabled'])->toBeTrue();
    expect((bool) $layoutModules->firstWhere('key', 'discussion')['enabled'])->toBeFalse();
    expect((bool) $layoutModules->firstWhere('key', 'collaborators')['enabled'])->toBeFalse();
});

it('owner can update campaign metadata through the dedicated campaign workflow route', function () {
    $owner = User::factory()->create(['username' => 'campaignworkflowowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Campaign Workflow Collection',
        'slug' => 'campaign-workflow-collection',
        'campaign_key' => null,
        'campaign_label' => null,
        'event_key' => null,
        'banner_text' => null,
        'commercial_eligibility' => false,
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.campaign', ['collection' => $collection->id]), [
            'event_key' => 'winter-spotlight-2026',
            'event_label' => 'Winter Spotlight 2026',
            'season_key' => 'winter',
            'banner_text' => 'A seasonal editorial push for desktop showcases.',
            'badge_label' => 'Campaign Live',
            'spotlight_style' => Collection::SPOTLIGHT_STYLE_SEASONAL,
            'campaign_key' => 'winter-homepage',
            'campaign_label' => 'Winter Homepage',
            'commercial_eligibility' => true,
            'promotion_tier' => 'hero',
            'sponsorship_label' => 'Presented with Nova Partners',
            'partner_label' => 'Nova Partners',
            'monetization_ready_status' => 'reviewed',
            'brand_safe_status' => 'safe',
            'editorial_notes' => 'Primary winter homepage candidate with strong hero potential.',
        ])
        ->assertOk()
        ->assertJsonPath('collection.event_key', 'winter-spotlight-2026')
        ->assertJsonPath('collection.campaign_key', 'winter-homepage')
        ->assertJsonPath('campaign.campaign_key', 'winter-homepage')
        ->assertJsonPath('campaign.schedule.is_scheduled', false)
        ->assertJsonPath('campaign.editorial_notes', 'Primary winter homepage candidate with strong hero potential.')
        ->assertJsonPath('collection.commercial_eligibility', true)
        ->assertJsonPath('collection.partner_label', 'Nova Partners');

    $collection->refresh();

    expect($collection->event_label)->toBe('Winter Spotlight 2026');
    expect($collection->banner_text)->toBe('A seasonal editorial push for desktop showcases.');
    expect($collection->spotlight_style)->toBe(Collection::SPOTLIGHT_STYLE_SEASONAL);
    expect($collection->campaign_label)->toBe('Winter Homepage');
    expect($collection->commercial_eligibility)->toBeTrue();
    expect($collection->promotion_tier)->toBe('hero');
    expect($collection->editorial_notes)->toBe('Primary winter homepage candidate with strong hero potential.');
});

it('admin can persist staff commercial notes through the campaign workflow route', function () {
    $admin = User::factory()->create([
        'username' => 'campaignadmin',
        'role' => 'admin',
    ]);

    $collection = Collection::factory()->for($admin)->create([
        'title' => 'Commercial Review Collection',
        'slug' => 'commercial-review-collection',
        'staff_commercial_notes' => null,
    ]);

    $this->actingAs($admin)
        ->postJson(route('settings.collections.campaign', ['collection' => $collection->id]), [
            'staff_commercial_notes' => 'Brand-safe, partner-approved, waiting on sponsor creative lock.',
        ])
        ->assertOk()
        ->assertJsonPath('campaign.staff_commercial_notes', 'Brand-safe, partner-approved, waiting on sponsor creative lock.');

    $collection->refresh();

    expect($collection->staff_commercial_notes)->toBe('Brand-safe, partner-approved, waiting on sponsor creative lock.');
});

it('non admins cannot persist staff commercial notes through the campaign workflow route', function () {
    $owner = User::factory()->create(['username' => 'campaignnonadmin']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Owner Campaign Collection',
        'slug' => 'owner-campaign-collection',
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.campaign', ['collection' => $collection->id]), [
            'staff_commercial_notes' => 'Should be rejected for non-admin owners.',
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['staff_commercial_notes']);
});

it('quality review exposes editorial automation campaign context and surface suggestions', function () {
    $owner = User::factory()->create(['username' => 'editorialautomationowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Winter Editorial Picks',
        'slug' => 'winter-editorial-picks',
        'type' => Collection::TYPE_EDITORIAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'winter-homepage',
        'campaign_label' => 'Winter Homepage',
        'event_key' => 'winter-spotlight-2026',
        'event_label' => 'Winter Spotlight 2026',
        'season_key' => 'winter',
        'ranking_score' => 82,
        'artworks_count' => 6,
        'summary' => 'A tight editorial pass across winter-themed desktop showcases.',
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.ai.quality-review', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('review.campaign_summary.campaign_key', 'winter-homepage')
        ->assertJsonPath('review.campaign_summary.eligibility.is_campaign_ready', true)
        ->assertJsonPath('review.suggested_surface_assignments.0.surface_key', 'homepage.featured_collections');
});

it('owner can update lifecycle scheduling through the dedicated lifecycle workflow route', function () {
    $owner = User::factory()->create(['username' => 'lifecycleworkflowowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Lifecycle Workflow Collection',
        'slug' => 'lifecycle-workflow-collection',
        'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'published_at' => null,
        'unpublished_at' => null,
        'archived_at' => null,
        'expired_at' => null,
    ]);

    $publishAt = now()->addDay()->startOfHour();
    $unpublishAt = now()->addDays(10)->startOfHour();
    $this->actingAs($owner)
        ->postJson(route('settings.collections.lifecycle', ['collection' => $collection->id]), [
            'lifecycle_state' => Collection::LIFECYCLE_SCHEDULED,
            'visibility' => Collection::VISIBILITY_PUBLIC,
            'published_at' => $publishAt->toIso8601String(),
            'unpublished_at' => $unpublishAt->toIso8601String(),
        ])
        ->assertOk()
        ->assertJsonPath('collection.lifecycle_state', Collection::LIFECYCLE_SCHEDULED)
        ->assertJsonPath('collection.visibility', Collection::VISIBILITY_PUBLIC);

    $collection->refresh();

    expect($collection->lifecycle_state)->toBe(Collection::LIFECYCLE_SCHEDULED);
    expect($collection->visibility)->toBe(Collection::VISIBILITY_PUBLIC);
    expect($collection->published_at?->toISOString())->toBe($publishAt->toISOString());
    expect($collection->unpublished_at?->toISOString())->toBe($unpublishAt->toISOString());
    expect($collection->archived_at)->toBeNull();
});

it('owner can update series metadata through the dedicated series workflow route', function () {
    $owner = User::factory()->create(['username' => 'seriesworkflowowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Series Workflow Collection',
        'slug' => 'series-workflow-collection',
        'series_key' => null,
        'series_title' => null,
        'series_description' => null,
        'series_order' => null,
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.series', ['collection' => $collection->id]), [
            'series_key' => 'winter-story-arc',
            'series_title' => 'Winter Story Arc',
            'series_description' => 'A multi-part sequence of winter collection chapters.',
            'series_order' => 2,
        ])
        ->assertOk()
        ->assertJsonPath('collection.series_key', 'winter-story-arc')
        ->assertJsonPath('series.key', 'winter-story-arc')
        ->assertJsonPath('series.title', 'Winter Story Arc')
        ->assertJsonPath('series.order', 2);

    $collection->refresh();

    expect($collection->series_key)->toBe('winter-story-arc');
    expect($collection->series_title)->toBe('Winter Story Arc');
    expect($collection->series_description)->toBe('A multi-part sequence of winter collection chapters.');
    expect($collection->series_order)->toBe(2);
});

it('non owners cannot use dedicated collection workflow routes', function () {
    $owner = User::factory()->create(['username' => 'workflowowner']);
    $viewer = User::factory()->create(['username' => 'workflowviewer']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Protected Workflow Collection',
        'slug' => 'protected-workflow-collection',
    ]);

    $this->actingAs($viewer)
        ->postJson(route('settings.collections.presentation', ['collection' => $collection->id]), [
            'presentation_style' => Collection::PRESENTATION_MASONRY,
        ])
        ->assertForbidden();

    $this->actingAs($viewer)
        ->postJson(route('settings.collections.campaign', ['collection' => $collection->id]), [
            'campaign_key' => 'blocked-campaign',
        ])
        ->assertForbidden();

    $this->actingAs($viewer)
        ->postJson(route('settings.collections.lifecycle', ['collection' => $collection->id]), [
            'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        ])
        ->assertForbidden();

    $this->actingAs($viewer)
        ->postJson(route('settings.collections.series', ['collection' => $collection->id]), [
            'series_key' => 'blocked-series',
        ])
        ->assertForbidden();
});

it('owner can sync manual linked collections and the manage studio hydrates them in order', function () {
    $owner = User::factory()->create(['username' => 'linkedowner']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Base Linked Collection',
        'slug' => 'base-linked-collection',
    ]);
    $linkedA = Collection::factory()->for($owner)->create([
        'title' => 'Linked Collection A',
        'slug' => 'linked-collection-a',
    ]);
    $linkedB = Collection::factory()->for($owner)->create([
        'title' => 'Linked Collection B',
        'slug' => 'linked-collection-b',
    ]);
    $unlinkedOption = Collection::factory()->for($owner)->create([
        'title' => 'Unlinked Candidate Collection',
        'slug' => 'unlinked-candidate-collection',
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.linked.sync', ['collection' => $collection->id]), [
            'related_collection_ids' => [$linkedB->id, $linkedA->id],
        ])
        ->assertOk()
        ->assertJsonPath('linkedCollections.0.id', $linkedB->id)
        ->assertJsonPath('linkedCollections.1.id', $linkedA->id);

    expect(DB::table('collection_related_links')
        ->where('collection_id', $collection->id)
        ->orderBy('sort_order')
        ->pluck('related_collection_id')
        ->map(fn ($id) => (int) $id)
        ->all())
        ->toBe([$linkedB->id, $linkedA->id]);

    $response = $this->actingAs($owner)
        ->get(route('settings.collections.show', ['collection' => $collection->id]))
        ->assertOk();

    $page = $response->viewData('page');

    expect(collect(data_get($page, 'props.linkedCollections', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toBe([$linkedB->id, $linkedA->id]);
    expect(collect(data_get($page, 'props.linkedCollectionOptions', []))->pluck('id')->map(fn ($id) => (int) $id)->all())
        ->toContain($unlinkedOption->id);
});

it('manual linked collections appear on public pages without leaking private linked targets', function () {
    $owner = User::factory()->create(['username' => 'publiclinkedowner']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Public Linked Hub',
        'slug' => 'public-linked-hub',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);
    $publicLinked = Collection::factory()->for($owner)->create([
        'title' => 'Visible Manual Link',
        'slug' => 'visible-manual-link',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);
    $privateLinked = Collection::factory()->for($owner)->create([
        'title' => 'Hidden Manual Link',
        'slug' => 'hidden-manual-link',
        'visibility' => Collection::VISIBILITY_PRIVATE,
        'lifecycle_state' => Collection::LIFECYCLE_DRAFT,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.linked.sync', ['collection' => $collection->id]), [
            'related_collection_ids' => [$publicLinked->id, $privateLinked->id],
        ])
        ->assertOk();

    $this->get(route('profile.collections.show', ['username' => $owner->username, 'slug' => $collection->slug]))
        ->assertOk()
        ->assertSee('Visible Manual Link', false)
        ->assertDontSee('Hidden Manual Link', false);
});

it('non owners cannot sync manual linked collections', function () {
    $owner = User::factory()->create(['username' => 'protectedlinkedowner']);
    $viewer = User::factory()->create(['username' => 'protectedlinkedviewer']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Protected Linked Collection',
        'slug' => 'protected-linked-collection',
    ]);
    $otherCollection = Collection::factory()->for($owner)->create([
        'title' => 'Other Managed Collection',
        'slug' => 'other-managed-collection',
    ]);

    $this->actingAs($viewer)
        ->postJson(route('settings.collections.linked.sync', ['collection' => $collection->id]), [
            'related_collection_ids' => [$otherCollection->id],
        ])
        ->assertForbidden();
});
