<?php

use App\Models\Category;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\CollectionHistory;
use App\Models\CollectionMergeAction;
use App\Models\CollectionSavedNote;
use App\Models\CollectionRecommendationSnapshot;
use App\Models\ContentType;
use App\Models\Story;
use App\Models\Tag;
use App\Models\User;
use App\Jobs\RefreshCollectionHealthJob;
use App\Jobs\RefreshCollectionQualityJob;
use App\Jobs\RefreshCollectionRecommendationJob;
use App\Jobs\ScanCollectionDuplicateCandidatesJob;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;

it('owner can view collection health while non owners cannot', function () {
    $owner = User::factory()->create(['username' => 'healthowner']);
    $viewer = User::factory()->create(['username' => 'healthviewer']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Health Check Set',
        'slug' => 'health-check-set',
        'workflow_state' => Collection::WORKFLOW_IN_REVIEW,
        'health_state' => Collection::HEALTH_NEEDS_REVIEW,
        'placement_eligibility' => false,
    ]);

    $this->actingAs($owner)
        ->getJson(route('settings.collections.health', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('collection.id', $collection->id)
        ->assertJsonPath('health.health_state', Collection::HEALTH_NEEDS_REVIEW)
        ->assertJsonPath('health.placement_eligibility', false);

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

it('staff can restore a supported collection history entry', function () {
    $admin = User::factory()->create(['username' => 'historyrestoreadmin', 'role' => 'admin']);
    $collection = Collection::factory()->for($admin)->create([
        'title' => 'Restore Workflow Collection',
        'slug' => 'restore-workflow-collection',
        'workflow_state' => Collection::WORKFLOW_DRAFT,
        'placement_eligibility' => false,
        'program_key' => null,
        'partner_key' => null,
        'experiment_key' => null,
    ]);

    $this->actingAs($admin)
        ->postJson(route('settings.collections.workflow', ['collection' => $collection->id]), [
            'workflow_state' => Collection::WORKFLOW_APPROVED,
            'program_key' => 'spring-homepage',
            'partner_key' => 'official-partner',
            'experiment_key' => 'discover-v5-a',
            'placement_eligibility' => true,
        ])
        ->assertOk()
        ->assertJsonPath('collection.workflow_state', Collection::WORKFLOW_APPROVED)
        ->assertJsonPath('collection.program_key', 'spring-homepage');

    $history = CollectionHistory::query()
        ->where('collection_id', $collection->id)
        ->where('action_type', 'workflow_updated')
        ->latest('id')
        ->firstOrFail();

    $this->actingAs($admin)
        ->postJson(route('settings.collections.history.restore', ['collection' => $collection->id, 'history' => $history->id]))
        ->assertOk()
        ->assertJsonPath('ok', true)
        ->assertJsonPath('collection.workflow_state', Collection::WORKFLOW_DRAFT)
        ->assertJsonPath('collection.program_key', null)
        ->assertJsonPath('collection.partner_key', null)
        ->assertJsonPath('collection.experiment_key', null)
        ->assertJsonPath('collection.placement_eligibility', false)
        ->assertJsonPath('restored_history_entry_id', $history->id);

    $collection->refresh();

    expect($collection->workflow_state)->toBe(Collection::WORKFLOW_DRAFT)
        ->and($collection->program_key)->toBeNull()
        ->and($collection->partner_key)->toBeNull()
        ->and($collection->experiment_key)->toBeNull()
        ->and((bool) $collection->placement_eligibility)->toBeFalse();
});

it('non-staff owners cannot restore collection history entries', function () {
    $owner = User::factory()->create(['username' => 'historyrestoreowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Owner Restore Blocked',
        'slug' => 'owner-restore-blocked',
    ]);

    $history = CollectionHistory::query()->create([
        'collection_id' => $collection->id,
        'actor_user_id' => $owner->id,
        'action_type' => 'updated',
        'summary' => 'Collection settings updated.',
        'before_json' => ['title' => 'Original title'],
        'after_json' => ['title' => 'Updated title'],
        'created_at' => now(),
    ]);

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

it('owner search only returns owned collections and staff fields stay protected', function () {
    $owner = User::factory()->create(['username' => 'searchowner']);
    $other = User::factory()->create(['username' => 'searchother']);

    $owned = Collection::factory()->for($owner)->create([
        'title' => 'Aurora Program Set',
        'slug' => 'aurora-program-set',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
        'program_key' => 'spring-homepage',
    ]);

    Collection::factory()->for($other)->create([
        'title' => 'Aurora Program Set External',
        'slug' => 'aurora-program-set-external',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
        'program_key' => 'spring-homepage',
    ]);

    $this->actingAs($owner)
        ->getJson(route('settings.collections.search', ['q' => 'Aurora', 'program_key' => 'spring-homepage']))
        ->assertOk()
        ->assertJsonCount(1, 'collections')
        ->assertJsonPath('collections.0.id', $owned->id);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.workflow', ['collection' => $owned->id]), [
            'workflow_state' => Collection::WORKFLOW_IN_REVIEW,
            'partner_key' => 'sponsored-partner',
            'sponsorship_state' => 'sponsored',
            'ownership_domain' => 'partner',
            'commercial_review_state' => 'approved',
            'legal_review_state' => 'approved',
            'placement_eligibility' => false,
        ])
        ->assertOk()
        ->assertJsonPath('collection.workflow_state', Collection::WORKFLOW_IN_REVIEW)
        ->assertJsonPath('collection.partner_key', null);

    $owned->refresh();

    expect($owned->workflow_state)->toBe(Collection::WORKFLOW_IN_REVIEW);
    expect($owned->partner_key)->toBeNull();
    expect($owned->sponsorship_state)->toBeNull();
    expect($owned->ownership_domain)->toBeNull();
    expect($owned->commercial_review_state)->toBeNull();
    expect($owned->legal_review_state)->toBeNull();
});

it('owner search validates filter values cleanly', function () {
    $owner = User::factory()->create(['username' => 'searchvalidationowner']);

    $this->actingAs($owner)
        ->getJson(route('settings.collections.search', [
            'mode' => 'invalid-mode',
            'placement_eligibility' => 'not-a-boolean',
        ]))
        ->assertStatus(422)
        ->assertJsonValidationErrors(['mode', 'placement_eligibility']);
});

it('owner can canonicalize and merge collections safely', function () {
    $owner = User::factory()->create(['username' => 'mergeowner']);
    $artworkA = Artwork::factory()->for($owner)->create();
    $artworkB = Artwork::factory()->for($owner)->create();

    $source = Collection::factory()->for($owner)->create([
        'title' => 'Winter Capsule Draft',
        'slug' => 'winter-capsule-draft',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $target = Collection::factory()->for($owner)->create([
        'title' => 'Winter Capsule Canonical',
        'slug' => 'winter-capsule-canonical',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    app(\App\Services\CollectionService::class)->attachArtworks($source, $owner, [$artworkA->id, $artworkB->id]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.canonicalize', ['collection' => $source->id]), [
            'target_collection_id' => $target->id,
        ])
        ->assertOk()
        ->assertJsonPath('collection.canonical_collection_id', $target->id);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.merge', ['collection' => $source->id]), [
            'target_collection_id' => $target->id,
        ])
        ->assertOk()
        ->assertJsonPath('source.lifecycle_state', Collection::LIFECYCLE_ARCHIVED)
        ->assertJsonPath('target.id', $target->id);

    $source->refresh();
    $target->refresh();

    expect($source->canonical_collection_id)->toBe($target->id);
    expect($source->lifecycle_state)->toBe(Collection::LIFECYCLE_ARCHIVED);
    expect($target->artworks()->count())->toBe(2);
});

it('public collection routes redirect to the canonical target after canonicalization', function () {
    $owner = User::factory()->create(['username' => 'canonredirector']);

    $source = Collection::factory()->for($owner)->create([
        'title' => 'Original Canonical Route',
        'slug' => 'original-canonical-route',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $target = Collection::factory()->for($owner)->create([
        'title' => 'Canonical Destination Route',
        'slug' => 'canonical-destination-route',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    app(\App\Services\CollectionCanonicalService::class)->designate($source, $target, $owner);

    $this->get(route('profile.collections.show', ['username' => $owner->username, 'slug' => $source->slug]))
        ->assertRedirect(route('profile.collections.show', ['username' => strtolower((string) $owner->username), 'slug' => $target->slug]));
});

it('public collection detail exposes normalized seo props', function () {
    $owner = User::factory()->create(['username' => 'seocollectionowner']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'SEO Collection Detail',
        'slug' => 'seo-collection-detail',
        'description' => 'A public collection used to verify normalized SEO props.',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $this->get(route('profile.collections.show', ['username' => strtolower((string) $owner->username), 'slug' => $collection->slug]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionShow')
            ->where('seo.title', fn ($value) => is_string($value) && str_contains($value, 'SEO Collection Detail'))
            ->where('seo.description', fn ($value) => is_string($value) && str_contains($value, 'verify normalized SEO props'))
            ->where('seo.robots', 'index,follow')
            ->where('seo.canonical', route('profile.collections.show', ['username' => strtolower((string) $owner->username), 'slug' => $collection->slug])));
});

it('owners can dismiss duplicate candidates from merge review', function () {
    $owner = User::factory()->create(['username' => 'mergedismissowner']);

    $source = Collection::factory()->for($owner)->create([
        'title' => 'Neon Futures',
        'slug' => 'neon-futures',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $target = Collection::factory()->for($owner)->create([
        'title' => 'Neon Futures',
        'slug' => 'neon-futures-alt',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $this->actingAs($owner)
        ->get(route('settings.collections.show', ['collection' => $source->id]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionManage')
            ->has('duplicateCandidates', 1)
            ->where('duplicateCandidates.0.collection.id', $target->id)
            ->where('endpoints.rejectDuplicate', route('settings.collections.merge.reject', ['collection' => $source->id])));

    $this->actingAs($owner)
        ->postJson(route('settings.collections.merge.reject', ['collection' => $source->id]), [
            'target_collection_id' => $target->id,
        ])
        ->assertOk()
        ->assertJsonCount(0, 'duplicate_candidates');

    expect(CollectionMergeAction::query()
        ->where('source_collection_id', $source->id)
        ->where('target_collection_id', $target->id)
        ->where('action_type', 'rejected')
        ->exists())->toBeTrue();

    $this->actingAs($owner)
        ->get(route('settings.collections.show', ['collection' => $source->id]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionManage')
            ->has('duplicateCandidates', 0));
});

it('staff can manage programming assignments and previews', function () {
    Queue::fake();

    $admin = User::factory()->create(['username' => 'programadmin', 'role' => 'admin']);
    $viewer = User::factory()->create(['username' => 'programviewer']);
    $owner = User::factory()->create(['username' => 'programowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Program Ready Set',
        'slug' => 'program-ready-set',
        'workflow_state' => Collection::WORKFLOW_PROGRAMMED,
        'program_key' => 'discover-spring',
        'placement_eligibility' => true,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
    ]);

    $mergeSource = Collection::factory()->for($owner)->create([
        'title' => 'Studio Merge Candidate',
        'slug' => 'studio-merge-candidate',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $mergeTarget = Collection::factory()->for($owner)->create([
        'title' => 'Studio Merge Candidate',
        'slug' => 'studio-merge-target',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    app(\App\Services\CollectionMergeService::class)->syncSuggestedCandidates($mergeSource, $admin);

    $this->actingAs($admin)
        ->get(route('staff.collections.programming'))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionStaffProgramming')
            ->where('collectionOptions', fn ($options): bool => collect($options)->contains(fn ($option): bool => (int) data_get($option, 'id') === (int) $collection->id))
            ->where('endpoints.preview', route('staff.collections.surfaces.preview'))
            ->where('observabilitySummary.counts.stale_health', fn ($count): bool => is_int($count))
            ->where('mergeQueue.summary.pending', fn ($pending): bool => (int) $pending >= 1)
            ->where('mergeQueue.pending', fn ($pending): bool => collect($pending)->contains(function ($item) use ($mergeSource, $mergeTarget): bool {
                return (int) data_get($item, 'source.id') === (int) $mergeSource->id
                    && (int) data_get($item, 'target.id') === (int) $mergeTarget->id;
            })));

    $this->actingAs($admin)
        ->postJson(route('staff.collections.merge-queue.reject'), [
            'source_collection_id' => $mergeSource->id,
            'target_collection_id' => $mergeTarget->id,
        ])
        ->assertOk()
        ->assertJsonPath('ok', true)
        ->assertJsonPath('mergeQueue.summary.rejected', 1);

    $canonicalSource = Collection::factory()->for($owner)->create([
        'title' => 'Studio Canonical Pair',
        'slug' => 'studio-canonical-pair',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $canonicalTarget = Collection::factory()->for($owner)->create([
        'title' => 'Studio Canonical Pair',
        'slug' => 'studio-canonical-target',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    app(\App\Services\CollectionMergeService::class)->syncSuggestedCandidates($canonicalSource, $admin);

    $this->actingAs($admin)
        ->postJson(route('staff.collections.merge-queue.canonicalize'), [
            'source_collection_id' => $canonicalSource->id,
            'target_collection_id' => $canonicalTarget->id,
        ])
        ->assertOk()
        ->assertJsonPath('ok', true)
        ->assertJsonPath('target.id', $canonicalTarget->id)
        ->assertJsonPath('mergeQueue.summary.approved', 1);

    expect($canonicalSource->fresh()->canonical_collection_id)->toBe($canonicalTarget->id);

    $mergeNowSource = Collection::factory()->for($owner)->create([
        'title' => 'Studio Merge Source Queue',
        'slug' => 'studio-merge-source-queue',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $mergeNowTarget = Collection::factory()->for($owner)->create([
        'title' => 'Studio Merge Target Queue',
        'slug' => 'studio-merge-target-queue',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

    $movedArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Merged Artwork',
        'slug' => 'merged-artwork',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subDay(),
    ]);

    $mergeNowSource->artworks()->attach($movedArtwork->id, ['order_num' => 1]);

    app(\App\Services\CollectionMergeService::class)->syncSuggestedCandidates($mergeNowSource, $admin);

    $this->actingAs($admin)
        ->postJson(route('staff.collections.merge-queue.merge'), [
            'source_collection_id' => $mergeNowSource->id,
            'target_collection_id' => $mergeNowTarget->id,
        ])
        ->assertOk()
        ->assertJsonPath('ok', true)
        ->assertJsonPath('target.id', $mergeNowTarget->id)
        ->assertJsonPath('attached_artwork_ids.0', $movedArtwork->id)
        ->assertJsonPath('mergeQueue.summary.completed', 1);

    expect($mergeNowSource->fresh()->canonical_collection_id)->toBe($mergeNowTarget->id)
        ->and($mergeNowSource->fresh()->lifecycle_state)->toBe(Collection::LIFECYCLE_ARCHIVED)
        ->and($mergeNowTarget->fresh()->artworks()->where('artworks.id', $movedArtwork->id)->exists())->toBeTrue();

    $this->actingAs($viewer)
        ->get(route('staff.collections.programming'))
        ->assertForbidden();

    $storeResponse = $this->actingAs($admin)
        ->postJson(route('staff.collections.programs.store'), [
            'collection_id' => $collection->id,
            'program_key' => 'discover-spring',
            'placement_scope' => 'homepage.hero',
            'priority' => 10,
        ])
        ->assertOk();

    $assignmentId = (int) $storeResponse->json('assignment.id');

    $this->actingAs($admin)
        ->patchJson(route('staff.collections.programs.update', ['program' => $assignmentId]), [
            'collection_id' => $collection->id,
            'program_key' => 'discover-spring',
            'placement_scope' => 'homepage.hero',
            'priority' => 12,
            'notes' => 'Promote on launch weekend.',
        ])
        ->assertOk()
        ->assertJsonPath('assignment.priority', 12);

    $this->actingAs($admin)
        ->postJson(route('staff.collections.surfaces.preview'), [
            'program_key' => 'discover-spring',
            'limit' => 6,
        ])
        ->assertOk()
        ->assertJsonPath('collections.0.id', $collection->id);

    $this->actingAs($admin)
        ->postJson(route('staff.collections.eligibility.refresh'), [
            'collection_id' => $collection->id,
        ])
        ->assertOk()
        ->assertJsonPath('queued', true)
        ->assertJsonPath('result.status', 'queued')
        ->assertJsonPath('result.count', 1);

    Queue::assertPushed(RefreshCollectionHealthJob::class, function (RefreshCollectionHealthJob $job) use ($admin, $collection): bool {
        return $job->actorUserId === $admin->id && $job->collectionId === $collection->id;
    });

    $this->actingAs($admin)
        ->postJson(route('staff.collections.duplicate-scan'), [
            'collection_id' => $collection->id,
        ])
        ->assertOk()
        ->assertJsonPath('queued', true)
        ->assertJsonPath('result.status', 'queued')
        ->assertJsonPath('result.count', 1);

    Queue::assertPushed(ScanCollectionDuplicateCandidatesJob::class, function (ScanCollectionDuplicateCandidatesJob $job) use ($admin, $collection): bool {
        return $job->actorUserId === $admin->id && $job->collectionId === $collection->id;
    });

    $this->actingAs($admin)
        ->postJson(route('staff.collections.recommendation-refresh'), [
            'collection_id' => $collection->id,
        ])
        ->assertOk()
        ->assertJsonPath('queued', true)
        ->assertJsonPath('result.status', 'queued')
        ->assertJsonPath('result.count', 1);

    Queue::assertPushed(RefreshCollectionRecommendationJob::class, function (RefreshCollectionRecommendationJob $job) use ($admin, $collection): bool {
        return $job->actorUserId === $admin->id && $job->collectionId === $collection->id;
    });

    $this->actingAs($admin)
        ->postJson(route('staff.collections.metadata.update'), [
            'collection_id' => $collection->id,
            'experiment_key' => 'discover-v5-a',
            'experiment_treatment' => 'treatment-b',
            'placement_variant' => 'homepage_dense',
            'ranking_mode_variant' => 'quality_first',
            'collection_pool_version' => '2026.03.26',
            'test_label' => 'Spring Program Rollout',
            'promotion_tier' => 'priority',
            'partner_key' => 'official-partner',
            'trust_tier' => 'trusted',
            'sponsorship_state' => 'approved',
            'ownership_domain' => 'partner_programs',
            'commercial_review_state' => 'approved',
            'legal_review_state' => 'cleared',
            'placement_eligibility' => true,
        ])
        ->assertOk()
        ->assertJsonPath('ok', true)
        ->assertJsonPath('collection.experiment_key', 'discover-v5-a')
        ->assertJsonPath('collection.experiment_treatment', 'treatment-b')
        ->assertJsonPath('collection.placement_variant', 'homepage_dense')
        ->assertJsonPath('collection.ranking_mode_variant', 'quality_first')
        ->assertJsonPath('collection.collection_pool_version', '2026.03.26')
        ->assertJsonPath('collection.test_label', 'Spring Program Rollout')
        ->assertJsonPath('collection.partner_key', 'official-partner')
        ->assertJsonPath('collection.trust_tier', 'trusted')
        ->assertJsonPath('collection.promotion_tier', 'priority')
        ->assertJsonPath('collection.sponsorship_state', 'approved')
        ->assertJsonPath('collection.ownership_domain', 'partner_programs')
        ->assertJsonPath('collection.commercial_review_state', 'approved')
        ->assertJsonPath('collection.legal_review_state', 'cleared')
        ->assertJsonPath('diagnostics.collection_id', $collection->id)
        ->assertJsonPath('diagnostics.placement_eligibility', true)
        ->assertJsonPath('diagnostics.experiment_treatment', 'treatment-b')
        ->assertJsonPath('diagnostics.placement_variant', 'homepage_dense')
        ->assertJsonPath('diagnostics.ranking_mode_variant', 'quality_first')
        ->assertJsonPath('diagnostics.collection_pool_version', '2026.03.26')
        ->assertJsonPath('diagnostics.test_label', 'Spring Program Rollout')
        ->assertJsonPath('diagnostics.sponsorship_state', 'approved')
        ->assertJsonPath('diagnostics.ownership_domain', 'partner_programs')
        ->assertJsonPath('diagnostics.commercial_review_state', 'approved')
        ->assertJsonPath('diagnostics.legal_review_state', 'cleared');

    $collection->refresh();

    expect($collection->experiment_key)->toBe('discover-v5-a')
        ->and($collection->experiment_treatment)->toBe('treatment-b')
        ->and($collection->placement_variant)->toBe('homepage_dense')
        ->and($collection->ranking_mode_variant)->toBe('quality_first')
        ->and($collection->collection_pool_version)->toBe('2026.03.26')
        ->and($collection->test_label)->toBe('Spring Program Rollout')
        ->and($collection->partner_key)->toBe('official-partner')
        ->and($collection->trust_tier)->toBe('trusted')
        ->and($collection->promotion_tier)->toBe('priority')
        ->and($collection->sponsorship_state)->toBe('approved')
        ->and($collection->ownership_domain)->toBe('partner_programs')
        ->and($collection->commercial_review_state)->toBe('approved')
        ->and($collection->legal_review_state)->toBe('cleared');
});

it('quality refresh queues background recomputation for owners', function () {
    Queue::fake();

    $owner = User::factory()->create(['username' => 'qualityqueueowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Queued Quality Collection',
        'slug' => 'queued-quality-collection',
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.quality-refresh', ['collection' => $collection->id]))
        ->assertOk()
        ->assertJsonPath('queued', true)
        ->assertJsonPath('result.status', 'queued')
        ->assertJsonPath('result.collection_ids.0', $collection->id);

    Queue::assertPushed(RefreshCollectionQualityJob::class, function (RefreshCollectionQualityJob $job) use ($owner, $collection): bool {
        return $job->actorUserId === $owner->id && $job->collectionId === $collection->id;
    });
});

it('recommendation snapshots stay idempotent for the same context and day', function () {
    $owner = User::factory()->create(['username' => 'recommendationidempotentowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Recommendation Snapshot Collection',
        'slug' => 'recommendation-snapshot-collection',
        'placement_eligibility' => true,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
    ]);

    $ranking = app(\App\Services\CollectionRankingService::class);

    $ranking->refresh($collection, 'default');
    $ranking->refresh($collection->fresh(), 'default');

    expect(CollectionRecommendationSnapshot::query()
        ->where('collection_id', $collection->id)
        ->where('context_key', 'default')
        ->count())->toBe(1);
});

it('public search only returns safe public collections', function () {
    $owner = User::factory()->create(['username' => 'publicsearchowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Visible Search Result',
        'slug' => 'visible-search-result',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Private Search Result',
        'slug' => 'private-search-result',
        'visibility' => Collection::VISIBILITY_PRIVATE,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Placement Blocked Search Result',
        'slug' => 'placement-blocked-search-result',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => false,
    ]);

    $this->get(route('collections.search', ['q' => 'Search Result']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->has('collections', 1)
            ->where('collections.0.title', 'Visible Search Result'));
});

it('public collection search exposes noindex seo props on filtered pages', function () {
    $owner = User::factory()->create(['username' => 'seosearchowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'SEO Search Target',
        'slug' => 'seo-search-target',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'workflow_state' => Collection::WORKFLOW_APPROVED,
        'placement_eligibility' => true,
    ]);

    $url = route('collections.search', ['q' => 'SEO Search Target']);

    $this->get($url)
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->where('seo.robots', 'noindex,follow')
            ->where('seo.canonical', $url)
            ->where('seo.title', 'Search Collections — Skinbase')
            ->where('seo.description', 'Search results for "SEO Search Target" across public Skinbase collections.'));
});

it('saved collections support private saved notes', function () {
    $user = User::factory()->create(['username' => 'savednoteowner']);
    $curator = User::factory()->create(['username' => 'savednotecurator']);
    $collection = Collection::factory()->for($curator)->create([
        'title' => 'Saved Note Candidate',
        'slug' => 'saved-note-candidate',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

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

    $this->actingAs($user)
        ->get(route('profile.collections.show', ['username' => strtolower((string) $curator->username), 'slug' => $collection->slug]))
        ->assertOk();

    $this->actingAs($user)
        ->patchJson(route('me.saved.collections.notes.update', ['collection' => $collection->id]), [
            'note' => 'Strong seasonal inspiration for homepage direction.',
        ])
        ->assertOk()
        ->assertJsonPath('note.collection_id', $collection->id)
        ->assertJsonPath('note.note', 'Strong seasonal inspiration for homepage direction.');

    $this->actingAs($user)
        ->get(route('me.saved.collections'))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/SavedCollections')
            ->where('collections.0.saved_note', 'Strong seasonal inspiration for homepage direction.')
            ->where('collections.0.saved_because', 'Saved from the collection page')
            ->has('recentlyRevisited', 1)
            ->where('recentlyRevisited.0.id', $collection->id));
});

it('browse surfaces expose save state and featured saves preserve surface context', function () {
    $user = User::factory()->create(['username' => 'featuredbrowseviewer']);
    $owner = User::factory()->create(['username' => 'featuredbrowseowner']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Featured Browse Save Search Target',
        'slug' => 'featured-browse-save-target',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($user)
        ->get(route('collections.search', ['q' => 'Featured Browse Save Search Target']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->where('collections.0.id', $collection->id)
            ->where('collections.0.saved', false)
            ->where('collections.0.save_url', route('collections.save', ['collection' => $collection->id])));

    $this->actingAs($user)
        ->postJson(route('collections.save', ['collection' => $collection->id]), [
            'context' => 'featured_landing',
            'context_meta' => [
                'surface_label' => 'featured collections',
            ],
        ])
        ->assertOk()
        ->assertJsonPath('saved', true);

    $this->actingAs($user)
        ->get(route('me.saved.collections'))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/SavedCollections')
            ->where('collections.0.saved_because', 'Saved from featured collections'));
});

it('saved collections support richer filtering search and sorting', function () {
    $user = User::factory()->create(['username' => 'savedlibraryfilteruser']);
    $owner = User::factory()->create(['username' => 'savedlibraryfilterowner']);

    $alpha = Collection::factory()->for($owner)->create([
        'title' => 'Alpha Personal Board',
        'slug' => 'alpha-personal-board',
        'type' => Collection::TYPE_PERSONAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $beta = Collection::factory()->for($owner)->create([
        'title' => 'Beta Community Capsule',
        'slug' => 'beta-community-capsule',
        'type' => Collection::TYPE_COMMUNITY,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $gamma = Collection::factory()->for($owner)->create([
        'title' => 'Gamma Editorial Campaign',
        'slug' => 'gamma-editorial-campaign',
        'type' => Collection::TYPE_EDITORIAL,
        'campaign_key' => 'spring-spotlight',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($user)->postJson(route('collections.save', ['collection' => $gamma->id]), [
        'context' => 'recommended_landing',
    ])->assertOk();

    $this->actingAs($user)->postJson(route('collections.save', ['collection' => $beta->id]), [
        'context' => 'community_landing',
    ])->assertOk();

    $this->actingAs($user)->postJson(route('collections.save', ['collection' => $alpha->id]), [
        'context' => 'collection_detail',
    ])->assertOk();

    CollectionSavedNote::query()->create([
        'user_id' => $user->id,
        'collection_id' => $alpha->id,
        'note' => 'Anchor reference for the homepage refresh.',
    ]);

    DB::table('collection_saves')
        ->where('user_id', $user->id)
        ->where('collection_id', $alpha->id)
        ->update([
            'created_at' => now()->subDays(3),
            'last_viewed_at' => now(),
        ]);

    DB::table('collection_saves')
        ->where('user_id', $user->id)
        ->whereIn('collection_id', [$beta->id, $gamma->id])
        ->update([
            'last_viewed_at' => DB::raw('created_at'),
        ]);

    $this->actingAs($user)
        ->get(route('me.saved.collections', ['filter' => 'noted', 'q' => 'alpha']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/SavedCollections')
            ->has('collections', 1)
            ->where('collections.0.id', $alpha->id)
            ->where('activeFilters.q', 'alpha')
            ->where('activeFilters.filter', 'noted'));

    $this->actingAs($user)
        ->get(route('me.saved.collections', ['filter' => 'revisited']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/SavedCollections')
            ->has('collections', 1)
            ->where('collections.0.id', $alpha->id)
            ->where('filterOptions.5.count', 1)
            ->where('filterOptions.6.count', 1));

    $this->actingAs($user)
        ->get(route('me.saved.collections', ['sort' => 'title_asc']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/SavedCollections')
            ->where('collections.0.id', $alpha->id)
            ->where('collections.1.id', $beta->id)
            ->where('collections.2.id', $gamma->id)
            ->where('sortOptions.5.key', 'title_asc'));
});

it('bulk actions validate campaign and lifecycle requirements', function () {
    $owner = User::factory()->create(['username' => 'bulkvalidationowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Bulk Validation Target',
        'slug' => 'bulk-validation-target',
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.bulk-actions'), [
            'action' => 'assign_campaign',
            'collection_ids' => [$collection->id],
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['campaign_key']);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.bulk-actions'), [
            'action' => 'update_lifecycle',
            'collection_ids' => [$collection->id],
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['lifecycle_state']);
});

it('owner collection target actions reject using the same collection as target', function () {
    $owner = User::factory()->create(['username' => 'selftargetowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Self Target Collection',
        'slug' => 'self-target-collection',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
    ]);

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

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

it('staff programming validates assignment schedule windows', function () {
    $admin = User::factory()->create(['username' => 'assignmentvalidator', 'role' => 'admin']);
    $owner = User::factory()->create(['username' => 'assignmenttargetowner']);
    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Assignment Validation Target',
        'slug' => 'assignment-validation-target',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($admin)
        ->postJson(route('staff.collections.programs.store'), [
            'collection_id' => $collection->id,
            'program_key' => 'discover-spring',
            'starts_at' => now()->addDay()->toISOString(),
            'ends_at' => now()->subDay()->toISOString(),
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['ends_at']);
});

it('collection entity links can be managed and render safely on public pages', function () {
    $owner = User::factory()->create(['username' => 'entitylinkowner']);
    $linkedCreator = User::factory()->create(['username' => 'linkedentitycreator']);

    $collection = Collection::factory()->for($owner)->create([
        'title' => 'Linked Context Collection',
        'slug' => 'linked-context-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'spring-launch',
        'campaign_label' => 'Spring Launch',
        'event_key' => 'launch-week',
        'event_label' => 'Launch Week',
    ]);

    $story = Story::query()->create([
        'creator_id' => $owner->id,
        'slug' => 'linked-story-brief',
        'title' => 'Linked Story Brief',
        'excerpt' => 'A companion story for this collection.',
        'status' => 'published',
        'story_type' => 'creator_story',
        'published_at' => now(),
    ]);

    $publicArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Linked Artwork Poster',
        'slug' => 'linked-artwork-poster',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $hiddenArtwork = Artwork::factory()->for($owner)->create([
        'title' => 'Hidden Artwork Poster',
        'slug' => 'hidden-artwork-poster',
        'is_public' => false,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $draftStory = Story::query()->create([
        'creator_id' => $owner->id,
        'slug' => 'hidden-story-brief',
        'title' => 'Hidden Story Brief',
        'excerpt' => 'This should not appear publicly.',
        'status' => 'draft',
        'story_type' => 'creator_story',
    ]);

    $contentType = ContentType::query()->create([
        'name' => 'Wallpapers',
        'slug' => 'wallpapers',
        'description' => 'Wallpaper categories',
    ]);

    $category = Category::query()->create([
        'content_type_id' => $contentType->id,
        'name' => 'Cyberpunk',
        'slug' => 'cyberpunk',
        'is_active' => true,
        'sort_order' => 1,
    ]);

    $tag = Tag::factory()->create([
        'name' => 'Neon',
        'slug' => 'neon',
        'usage_count' => 44,
        'is_active' => true,
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.entity-links.sync', ['collection' => $collection->id]), [
            'entity_links' => [
                ['linked_type' => 'creator', 'linked_id' => $linkedCreator->id, 'relationship_type' => 'featured creator'],
                ['linked_type' => 'artwork', 'linked_id' => $publicArtwork->id, 'relationship_type' => 'hero artwork'],
                ['linked_type' => 'story', 'linked_id' => $story->id, 'relationship_type' => 'behind the scenes'],
                ['linked_type' => 'category', 'linked_id' => $category->id, 'relationship_type' => 'genre fit'],
                ['linked_type' => 'campaign', 'linked_id' => (int) hexdec(substr(md5('campaign:spring-launch'), 0, 7)), 'relationship_type' => 'campaign hub'],
                ['linked_type' => 'event', 'linked_id' => (int) hexdec(substr(md5('event:launch-week'), 0, 7)), 'relationship_type' => 'event spotlight'],
                ['linked_type' => 'tag', 'linked_id' => $tag->id, 'relationship_type' => 'theme tag'],
                ['linked_type' => 'story', 'linked_id' => $draftStory->id, 'relationship_type' => 'internal draft'],
                ['linked_type' => 'artwork', 'linked_id' => $hiddenArtwork->id, 'relationship_type' => 'internal artwork'],
            ],
        ])
        ->assertOk()
        ->assertJsonCount(9, 'entityLinks')
        ->assertJsonPath('entityLinks.0.linked_type', 'creator')
        ->assertJsonPath('entityLinks.1.title', 'Linked Artwork Poster')
        ->assertJsonPath('entityLinks.2.title', 'Linked Story Brief')
        ->assertJsonPath('entityLinks.3.title', 'Cyberpunk')
        ->assertJsonPath('entityLinks.4.title', 'Spring Launch')
        ->assertJsonPath('entityLinks.5.title', 'Launch Week')
        ->assertJsonPath('entityLinks.6.title', 'Neon');

    $this->actingAs($owner)
        ->get(route('settings.collections.show', ['collection' => $collection->id]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionManage')
            ->has('entityLinks', 9)
            ->where('entityLinks.0.relationship_type', 'featured creator')
            ->where('entityLinks.1.relationship_type', 'hero artwork')
            ->has('entityLinkOptions.artwork')
            ->has('entityLinkOptions.campaign')
            ->has('entityLinkOptions.event')
            ->has('entityLinkOptions.tag')
            ->where('endpoints.syncEntityLinks', route('settings.collections.entity-links.sync', ['collection' => $collection->id])));

    $this->get(route('profile.collections.show', ['username' => strtolower((string) $owner->username), 'slug' => $collection->slug]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionShow')
            ->has('entityLinks', 7)
            ->where('entityLinks.0.title', $linkedCreator->name ?: $linkedCreator->username)
            ->where('entityLinks.1.title', 'Linked Artwork Poster')
            ->where('entityLinks.2.title', 'Linked Story Brief')
            ->where('entityLinks.3.title', 'Cyberpunk')
            ->where('entityLinks.4.title', 'Spring Launch')
            ->where('entityLinks.5.title', 'Launch Week')
            ->where('entityLinks.6.title', 'Neon'));
});

it('public program landing shows only placement-eligible public collections for a program key', function () {
    $owner = User::factory()->create(['username' => 'programlandingowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Program Hero Collection',
        'slug' => 'program-hero-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => true,
        'program_key' => 'discover-spring',
        'partner_label' => 'Official Partner',
        'sponsorship_label' => 'Sponsored Placement',
        'promotion_tier' => 'priority',
        'trust_tier' => 'trusted',
        'banner_text' => 'Discover Spring',
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Program Blocked Collection',
        'slug' => 'program-blocked-collection',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => false,
        'program_key' => 'discover-spring',
    ]);

    $this->get(route('collections.program.show', ['programKey' => 'discover-spring']))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->where('program.key', 'discover-spring')
            ->where('program.partner_labels.0', 'Official Partner')
            ->where('program.sponsorship_labels.0', 'Sponsored Placement')
            ->has('collections', 1)
            ->where('collections.0.title', 'Program Hero Collection'));
});

it('dashboard exposes v5 health warning summaries', function () {
    $owner = User::factory()->create(['username' => 'dashboardv5owner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Needs Review Collection',
        'slug' => 'needs-review-collection',
        'health_state' => Collection::HEALTH_NEEDS_REVIEW,
        'placement_eligibility' => false,
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Duplicate Risk Collection',
        'slug' => 'duplicate-risk-collection',
        'health_state' => Collection::HEALTH_DUPLICATE_RISK,
        'placement_eligibility' => false,
    ]);

    $this->actingAs($owner)
        ->get(route('settings.collections.dashboard'))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionDashboard')
            ->where('summary.needs_review', 1)
            ->where('summary.duplicate_risk', 1)
            ->where('summary.placement_blocked', 2)
            ->has('healthWarnings', 2)
            ->where('filterOptions.workflowStates.0', Collection::WORKFLOW_DRAFT)
            ->where('endpoints.search', route('settings.collections.search'))
            ->where('endpoints.bulkActions', route('settings.collections.bulk-actions')));
});

it('owner can apply safe bulk actions from the dashboard', function () {
    Queue::fake();

    $owner = User::factory()->create(['username' => 'bulkactionowner']);

    $first = Collection::factory()->for($owner)->create([
        'title' => 'Bulk Action First',
        'slug' => 'bulk-action-first',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $second = Collection::factory()->for($owner)->create([
        'title' => 'Bulk Action Second',
        'slug' => 'bulk-action-second',
        'workflow_state' => Collection::WORKFLOW_APPROVED,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'moderation_status' => Collection::MODERATION_ACTIVE,
    ]);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.bulk-actions'), [
            'action' => 'assign_campaign',
            'collection_ids' => [$first->id, $second->id],
            'campaign_key' => 'spring-launch',
            'campaign_label' => 'Spring Launch',
        ])
        ->assertOk()
        ->assertJsonPath('count', 2)
        ->assertJsonPath('collections.0.campaign_key', 'spring-launch');

    expect($first->fresh()->campaign_key)->toBe('spring-launch')
        ->and($second->fresh()->campaign_key)->toBe('spring-launch');

    $this->actingAs($owner)
        ->postJson(route('settings.collections.bulk-actions'), [
            'action' => 'mark_editorial_review',
            'collection_ids' => [$first->id, $second->id],
        ])
        ->assertOk()
        ->assertJsonPath('collections.0.workflow_state', Collection::WORKFLOW_IN_REVIEW);

    expect($first->fresh()->workflow_state)->toBe(Collection::WORKFLOW_IN_REVIEW)
        ->and($second->fresh()->workflow_state)->toBe(Collection::WORKFLOW_IN_REVIEW);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.bulk-actions'), [
            'action' => 'request_ai_review',
            'collection_ids' => [$first->id, $second->id],
        ])
        ->assertOk()
        ->assertJsonPath('count', 2);

    Queue::assertPushed(RefreshCollectionQualityJob::class, 2);

    $this->actingAs($owner)
        ->postJson(route('settings.collections.bulk-actions'), [
            'action' => 'archive',
            'collection_ids' => [$first->id, $second->id],
        ])
        ->assertOk()
        ->assertJsonPath('summary.archived', 2);

    expect($first->fresh()->lifecycle_state)->toBe(Collection::LIFECYCLE_ARCHIVED)
        ->and($second->fresh()->lifecycle_state)->toBe(Collection::LIFECYCLE_ARCHIVED);
});

it('public collection search returns expanded filter payloads for the search ui', function () {
    $owner = User::factory()->create(['username' => 'filtersearchowner']);

    Collection::factory()->for($owner)->create([
        'title' => 'Editorial Search Target',
        'slug' => 'editorial-search-target',
        'type' => Collection::TYPE_EDITORIAL,
        'mode' => Collection::MODE_MANUAL,
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'campaign_key' => 'winter-drop',
        'program_key' => 'homepage-hero',
        'health_state' => Collection::HEALTH_LOW_CONTENT,
    ]);

    $this->get(route('collections.search', [
        'q' => 'Search',
        'type' => Collection::TYPE_EDITORIAL,
        'mode' => Collection::MODE_MANUAL,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'health_state' => Collection::HEALTH_LOW_CONTENT,
        'campaign_key' => 'winter-drop',
        'program_key' => 'homepage-hero',
        'sort' => 'quality',
    ]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->where('search.filters.q', 'Search')
            ->where('search.filters.type', Collection::TYPE_EDITORIAL)
            ->where('search.filters.mode', Collection::MODE_MANUAL)
            ->where('search.filters.lifecycle_state', Collection::LIFECYCLE_PUBLISHED)
            ->where('search.filters.health_state', Collection::HEALTH_LOW_CONTENT)
            ->where('search.filters.campaign_key', 'winter-drop')
            ->where('search.filters.program_key', 'homepage-hero')
            ->where('search.filters.sort', 'quality')
            ->has('search.options.category')
            ->has('search.options.style')
            ->has('search.options.theme')
            ->has('search.options.color')
            ->has('search.options.quality_tier')
            ->has('collections', 1));
});

it('public collection search supports category style theme color and quality tier filters', function () {
    $owner = User::factory()->create(['username' => 'advsearchowner']);
    $contentType = ContentType::query()->create(['name' => 'Illustration', 'slug' => 'illustration']);
    $category = Category::query()->create([
        'name' => 'Landscape',
        'slug' => 'landscape',
        'content_type_id' => $contentType->id,
        'is_active' => true,
    ]);
    $styleTag = Tag::factory()->create(['name' => 'Watercolor', 'slug' => 'watercolor', 'is_active' => true]);
    $colorTag = Tag::factory()->create(['name' => 'Blue Tones', 'slug' => 'blue-tones', 'is_active' => true]);

    $matching = Collection::factory()->for($owner)->create([
        'title' => 'Advanced Filter Match',
        'slug' => 'advanced-filter-match',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => true,
        'health_state' => Collection::HEALTH_HEALTHY,
        'trust_tier' => 'high',
        'theme_token' => 'amber',
    ]);

    $artwork = Artwork::factory()->for($owner)->create([
        'title' => 'Watercolor Horizon',
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now()->subDay(),
    ]);

    $matching->artworks()->attach($artwork->id, ['order_num' => 1]);
    $artwork->categories()->attach($category->id);
    $artwork->tags()->attach([
        $styleTag->id => ['source' => 'system', 'confidence' => 1],
        $colorTag->id => ['source' => 'system', 'confidence' => 1],
    ]);

    DB::table('collection_entity_links')->insert([
        'collection_id' => $matching->id,
        'linked_type' => 'category',
        'linked_id' => $category->id,
        'relationship_type' => 'primary category',
        'metadata_json' => null,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    Collection::factory()->for($owner)->create([
        'title' => 'Advanced Filter Miss',
        'slug' => 'advanced-filter-miss',
        'visibility' => Collection::VISIBILITY_PUBLIC,
        'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
        'moderation_status' => Collection::MODERATION_ACTIVE,
        'placement_eligibility' => true,
        'health_state' => Collection::HEALTH_HEALTHY,
        'trust_tier' => 'limited',
        'theme_token' => 'violet',
    ]);

    $this->get(route('collections.search', [
        'category' => 'landscape',
        'style' => 'watercolor',
        'theme' => 'amber',
        'color' => 'blue tones',
        'quality_tier' => 'high',
    ]))
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('Collection/CollectionFeaturedIndex')
            ->where('search.filters.category', 'landscape')
            ->where('search.filters.style', 'watercolor')
            ->where('search.filters.theme', 'amber')
            ->where('search.filters.color', 'blue tones')
            ->where('search.filters.quality_tier', 'high')
            ->has('collections', 1)
            ->where('collections.0.id', $matching->id));
});

it('duplicate candidate scans assign a stable duplicate cluster key that health refresh preserves', function () {
    $owner = User::factory()->create(['username' => 'duplicateclusterowner']);

    $source = Collection::factory()->for($owner)->create([
        'title' => 'Duplicate Cluster Source',
        'slug' => 'duplicate-cluster-source',
    ]);

    $target = Collection::factory()->for($owner)->create([
        'title' => 'Duplicate Cluster Source',
        'slug' => 'duplicate-cluster-target',
    ]);

    $result = app(\App\Services\CollectionMergeService::class)->syncSuggestedCandidates($source->fresh(), $owner);

    $source->refresh();
    $target->refresh();

    expect($result['count'])->toBe(1)
        ->and($source->duplicate_cluster_key)->not->toBeNull()
        ->and($source->duplicate_cluster_key)->toBe($target->duplicate_cluster_key);

    app(\App\Services\CollectionHealthService::class)->refresh($source->fresh(), $owner, 'test-duplicate-cluster');

    expect($source->fresh()->duplicate_cluster_key)->toBe($target->fresh()->duplicate_cluster_key);
});