<?php

use App\Models\Artwork;
use App\Models\Group;
use App\Models\GroupInvitation;
use App\Models\GroupJoinRequest;
use App\Models\GroupMember;
use App\Models\GroupPost;
use App\Models\GroupRecruitmentProfile;
use App\Models\GroupActivityItem;
use App\Models\GroupAsset;
use App\Models\GroupChallenge;
use App\Models\GroupEvent;
use App\Models\GroupContributorStat;
use App\Models\GroupProject;
use App\Models\GroupProjectMember;
use App\Models\GroupRelease;
use App\Models\GroupReleaseContributor;
use App\Models\GroupReleaseMilestone;
use App\Models\Notification;
use App\Models\Report;
use App\Models\User;
use App\Models\Collection;
use App\Services\GroupMembershipService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;

uses(RefreshDatabase::class);

function createCategoryForGroupsFeatureTests(): int
{
    $contentTypeId = DB::table('content_types')->insertGetId([
        'name' => 'Digital Art',
        'slug' => 'digital-art-' . Str::lower(Str::random(6)),
        'description' => null,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    return DB::table('categories')->insertGetId([
        'content_type_id' => $contentTypeId,
        'parent_id' => null,
        'name' => 'Concept',
        'slug' => 'concept-' . Str::lower(Str::random(6)),
        'description' => null,
        'image' => null,
        'is_active' => true,
        'sort_order' => 0,
        'created_at' => now(),
        'updated_at' => now(),
    ]);
}

it('publishes an artwork under a group with credited authors', function () {
    $owner = User::factory()->create();
    $editor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $editor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->unpublished()->for($editor, 'user')->create([
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
    ]);

    $response = $this->actingAs($editor)->postJson("/api/uploads/{$artwork->id}/publish", [
        'group' => $group->slug,
        'primary_author_user_id' => $owner->id,
        'contributor_user_ids' => [$editor->id],
        'contributor_credits' => [
            [
                'user_id' => $editor->id,
                'credit_role' => 'Color assist',
                'is_primary' => true,
            ],
        ],
        'visibility' => 'public',
    ]);

    $response->assertOk()->assertJson([
        'success' => true,
        'artwork_id' => $artwork->id,
        'status' => 'published',
    ]);

    $artwork->refresh()->load(['contributors', 'group', 'primaryAuthor', 'uploadedBy']);
    $group->refresh();

    expect($artwork->group_id)->toBe($group->id)
        ->and($artwork->published_as_type)->toBe(Artwork::PUBLISHED_AS_GROUP)
        ->and($artwork->published_as_id)->toBe($group->id)
        ->and($artwork->uploaded_by_user_id)->toBe($editor->id)
        ->and($artwork->primary_author_user_id)->toBe($owner->id)
        ->and($artwork->contributors)->toHaveCount(1)
        ->and($artwork->contributors->first()->user_id)->toBe($editor->id)
        ->and($artwork->contributors->first()->credit_role)->toBe('Color assist')
        ->and($artwork->contributors->first()->is_primary)->toBeTrue()
        ->and($artwork->artwork_status)->toBe('published')
        ->and($group->artworks_count)->toBe(1);
});

it('renders the enriched studio group dashboard payload', function () {
    $owner = User::factory()->create();
    $editor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $editor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    Artwork::factory()->unpublished()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'draft',
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_public' => false,
        'is_approved' => false,
    ]);

    Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
    ]);

    Collection::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    $this->actingAs($owner)
        ->get(route('studio.groups.show', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupDashboard')
            ->where('studioGroup.slug', $group->slug)
            ->where('dashboard.draft_artworks_count', 1)
            ->where('dashboard.active_members_count', 2)
            ->where('draftsPendingAction.0.status', 'draft')
            ->where('recentArtworks.0.status', 'published')
            ->has('recentCollections', 1)
        );
});

it('renders public group pages and accepts group reports', function () {
    $viewer = User::factory()->create();
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);
    $admin = User::factory()->create();
    $featuredArtwork = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'primary_author_user_id' => $owner->id,
        'uploaded_by_user_id' => $owner->id,
    ]);
    $featuredCollection = Collection::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'is_featured' => true,
        'visibility' => Collection::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);
    $group->forceFill(['featured_artwork_id' => $featuredArtwork->id])->save();

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $admin->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_ADMIN,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $this->get(route('groups.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupIndex')
            ->where('title', 'Groups')
        );

    $this->actingAs($viewer)
        ->get(route('groups.show', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('group.slug', $group->slug)
            ->where('section', 'overview')
            ->where('seo.canonical', route('groups.show', ['group' => $group]))
            ->where('featuredArtworks.0.id', $featuredArtwork->id)
            ->where('featuredCollections.0.id', $featuredCollection->id)
            ->where('leadership.0.role', Group::ROLE_OWNER)
        );

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

    expect(Report::query()->where('target_type', 'group')->where('target_id', $group->id)->count())->toBe(1);
});

it('adds canonical metadata to public group artwork sections', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
        'status' => Group::LIFECYCLE_ACTIVE,
        'slug' => 'pixelart',
        'name' => 'Pixelart',
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->get(route('groups.section', ['group' => $group, 'section' => 'artworks']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('section', 'artworks')
            ->where('seo.canonical', route('groups.section', ['group' => $group, 'section' => 'artworks']))
            ->where('seo.og_url', route('groups.section', ['group' => $group, 'section' => 'artworks']))
        );
});

it('lets owners manage releases, contributors, milestones, and publishing through studio endpoints', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.store', ['group' => $group]), [
            'title' => 'Spring Systems Drop',
            'summary' => 'A shared release pipeline for the group.',
            'description' => 'Coordinated release covering artworks and public milestones.',
            'visibility' => GroupRelease::VISIBILITY_PUBLIC,
            'status' => GroupRelease::STATUS_PLANNED,
            'current_stage' => GroupRelease::STAGE_CONCEPT,
            'lead_user_id' => $owner->id,
            'featured_artwork_id' => $artwork->id,
            'is_featured' => true,
        ])
        ->assertRedirect();

    $release = GroupRelease::query()->where('group_id', $group->id)->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.attach-artwork', ['group' => $group, 'release' => $release]), [
            'artwork_id' => $artwork->id,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.attach-contributor', ['group' => $group, 'release' => $release]), [
            'user_id' => $contributor->id,
            'role_label' => 'Texture artist',
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.milestones.store', ['group' => $group, 'release' => $release]), [
            'title' => 'Final approval',
            'summary' => 'Wrap quality checks before launch.',
            'status' => 'active',
            'owner_user_id' => $owner->id,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.stage', ['group' => $group, 'release' => $release]), [
            'current_stage' => GroupRelease::STAGE_APPROVAL,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.publish', ['group' => $group, 'release' => $release]))
        ->assertRedirect();

    $release->refresh();

    expect($release->status)->toBe(GroupRelease::STATUS_RELEASED)
        ->and($release->current_stage)->toBe(GroupRelease::STAGE_RELEASED)
        ->and($release->released_at)->not->toBeNull()
        ->and($release->published_at)->not->toBeNull()
        ->and($release->artworks()->count())->toBe(1)
        ->and(GroupReleaseContributor::query()->where('group_release_id', $release->id)->where('user_id', $contributor->id)->exists())->toBeTrue()
        ->and(GroupReleaseMilestone::query()->where('group_release_id', $release->id)->count())->toBe(1);
});

it('lets owners manage challenge outcomes and exposes them on public challenge pages', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $winner = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);
    $finalist = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $this->actingAs($owner)
        ->post(route('studio.groups.challenges.store', ['group' => $group]), [
            'title' => 'Pixel Week Results',
            'summary' => 'Challenge summary',
            'description' => 'Challenge description',
            'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
            'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
            'status' => GroupChallenge::STATUS_ACTIVE,
            'start_at' => now()->subDay()->toDateTimeString(),
            'end_at' => now()->addDay()->toDateTimeString(),
            'judging_mode' => 'curated',
        ])
        ->assertRedirect();

    $challenge = GroupChallenge::query()->where('group_id', $group->id)->latest('id')->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.challenges.attach-artwork', ['group' => $group, 'challenge' => $challenge]), [
            'artwork_id' => $winner->id,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.challenges.attach-artwork', ['group' => $group, 'challenge' => $challenge]), [
            'artwork_id' => $finalist->id,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->patch(route('studio.groups.challenges.update', ['group' => $group, 'challenge' => $challenge]), [
            'title' => 'Pixel Week Results',
            'summary' => 'Challenge summary',
            'description' => 'Challenge description',
            'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
            'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
            'status' => GroupChallenge::STATUS_ACTIVE,
            'start_at' => now()->subDay()->toDateTimeString(),
            'end_at' => now()->addDay()->toDateTimeString(),
            'judging_mode' => 'curated',
            'outcomes' => [
                [
                    'artwork_id' => $winner->id,
                    'outcome_type' => 'winner',
                    'position' => 1,
                    'sort_order' => 0,
                    'title_override' => 'Grand Winner',
                    'note' => 'Top overall result.',
                ],
                [
                    'artwork_id' => $finalist->id,
                    'outcome_type' => 'finalist',
                    'sort_order' => 1,
                    'note' => 'Strong finalist showing.',
                ],
            ],
        ])
        ->assertRedirect();

    $challenge->refresh();

    expect($challenge->featured_artwork_id)->toBe($winner->id);

    $this->assertDatabaseHas('group_challenge_outcomes', [
        'group_challenge_id' => $challenge->id,
        'artwork_id' => $winner->id,
        'outcome_type' => 'winner',
    ]);
    $this->assertDatabaseHas('group_challenge_outcomes', [
        'group_challenge_id' => $challenge->id,
        'artwork_id' => $finalist->id,
        'outcome_type' => 'finalist',
    ]);

    $this->get(route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupChallengeShow')
            ->where('challenge.outcome_sections.winner.items.0.title', $winner->title)
            ->where('challenge.outcome_sections.finalist.items.0.title', $finalist->title));
});

it('renders public release pages and the studio reputation dashboard with v4 payloads', function () {
    $viewer = User::factory()->create();
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
        'is_verified' => true,
        'last_activity_at' => now(),
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'group_review_status' => 'approved',
        'published_at' => now(),
    ]);

    $release = GroupRelease::query()->create([
        'group_id' => $group->id,
        'title' => 'Public Drop',
        'slug' => 'public-drop',
        'summary' => 'Release summary',
        'description' => 'Release detail body',
        'status' => GroupRelease::STATUS_RELEASED,
        'current_stage' => GroupRelease::STAGE_RELEASED,
        'visibility' => GroupRelease::VISIBILITY_PUBLIC,
        'lead_user_id' => $owner->id,
        'featured_artwork_id' => $artwork->id,
        'created_by_user_id' => $owner->id,
        'released_at' => now(),
        'published_at' => now(),
        'is_featured' => true,
    ]);

    $release->artworks()->attach($artwork->id, ['sort_order' => 1]);

    GroupReleaseContributor::query()->create([
        'group_release_id' => $release->id,
        'user_id' => $contributor->id,
        'role_label' => 'Editor',
        'sort_order' => 1,
    ]);

    GroupReleaseMilestone::query()->create([
        'group_release_id' => $release->id,
        'title' => 'Published',
        'summary' => 'Release has shipped.',
        'status' => 'completed',
        'owner_user_id' => $owner->id,
        'sort_order' => 1,
    ]);

    $this->actingAs($viewer)
        ->get(route('groups.releases.show', ['group' => $group, 'release' => $release]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupReleaseShow')
            ->where('group.slug', $group->slug)
            ->where('release.title', 'Public Drop')
            ->where('seo.canonical', route('groups.releases.show', ['group' => $group, 'release' => $release]))
            ->where('seo.og_type', 'article')
            ->has('release.artworks', 1)
            ->has('release.contributors', 1)
            ->has('release.milestones', 1)
        );

    $this->actingAs($owner)
        ->get(route('studio.groups.reputation', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupReputation')
            ->where('studioGroup.slug', $group->slug)
            ->has('trustSignals')
            ->has('reputation.top_contributors')
        );
});

it('expands group search across public releases projects challenges and events', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'name' => 'Signal Crew',
        'slug' => 'signal-crew',
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupRelease::query()->create([
        'group_id' => $group->id,
        'title' => 'Neon Release Marker',
        'slug' => 'neon-release-marker',
        'summary' => 'Searchable release summary',
        'status' => GroupRelease::STATUS_RELEASED,
        'current_stage' => GroupRelease::STAGE_RELEASED,
        'visibility' => GroupRelease::VISIBILITY_PUBLIC,
        'created_by_user_id' => $owner->id,
        'released_at' => now(),
        'published_at' => now(),
    ]);

    GroupProject::query()->create([
        'group_id' => $group->id,
        'title' => 'Orbit Project Marker',
        'slug' => 'orbit-project-marker',
        'summary' => 'Searchable project summary',
        'visibility' => GroupProject::VISIBILITY_PUBLIC,
        'status' => GroupProject::STATUS_ACTIVE,
        'created_by_user_id' => $owner->id,
    ]);

    GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Pulse Challenge Marker',
        'slug' => 'pulse-challenge-marker',
        'summary' => 'Searchable challenge summary',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_GROUP_ONLY,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'created_by_user_id' => $owner->id,
    ]);

    GroupEvent::query()->create([
        'group_id' => $group->id,
        'title' => 'Echo Event Marker',
        'slug' => 'echo-event-marker',
        'summary' => 'Searchable event summary',
        'event_type' => GroupEvent::TYPE_SHOWCASE,
        'visibility' => GroupEvent::VISIBILITY_PUBLIC,
        'status' => GroupEvent::STATUS_PUBLISHED,
        'created_by_user_id' => $owner->id,
        'published_at' => now(),
    ]);

    foreach (['neon release marker', 'orbit project marker', 'pulse challenge marker', 'echo event marker'] as $term) {
        $this->getJson('/api/search/groups?q=' . urlencode($term))
            ->assertOk()
            ->assertJsonPath('data.0.slug', 'signal-crew');
    }
});

it('matches group search against badge labels and active member names', function () {
    $owner = User::factory()->create(['name' => 'Owner Search']);
    $member = User::factory()->create(['name' => 'Nova Curator']);
    $group = Group::factory()->for($owner, 'owner')->create([
        'name' => 'Signal Atlas',
        'slug' => 'signal-atlas',
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $member->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $release = GroupRelease::query()->create([
        'group_id' => $group->id,
        'title' => 'Atlas Release',
        'slug' => 'atlas-release',
        'summary' => 'Badge search support release.',
        'status' => GroupRelease::STATUS_RELEASED,
        'current_stage' => GroupRelease::STAGE_RELEASED,
        'visibility' => GroupRelease::VISIBILITY_PUBLIC,
        'created_by_user_id' => $owner->id,
        'featured_artwork_id' => $artwork->id,
        'released_at' => now(),
        'published_at' => now(),
    ]);

    GroupReleaseContributor::query()->create([
        'group_release_id' => $release->id,
        'user_id' => $member->id,
        'role_label' => 'Curator',
        'sort_order' => 1,
    ]);

    app(\App\Services\GroupReputationService::class)->refreshGroup($group);

    $badgeSearchResponse = $this->getJson('/api/search/groups?q=' . urlencode('first release'))
        ->assertOk()
        ->assertJsonPath('data.0.slug', 'signal-atlas');

    expect($badgeSearchResponse->json('data.0.badge_keys'))->toContain('first_release');

    $this->getJson('/api/search/groups?q=' . urlencode('nova curator'))
        ->assertOk()
        ->assertJsonPath('data.0.slug', 'signal-atlas');
});

it('renders public profile group contribution history for real group activity', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $release = GroupRelease::query()->create([
        'group_id' => $group->id,
        'title' => 'Profile Release Marker',
        'slug' => 'profile-release-marker',
        'summary' => 'A release for profile history.',
        'status' => GroupRelease::STATUS_RELEASED,
        'current_stage' => GroupRelease::STAGE_RELEASED,
        'visibility' => GroupRelease::VISIBILITY_PUBLIC,
        'created_by_user_id' => $owner->id,
        'released_at' => now(),
        'published_at' => now(),
    ]);

    GroupReleaseContributor::query()->create([
        'group_release_id' => $release->id,
        'user_id' => $contributor->id,
        'role_label' => 'Packaging Lead',
        'sort_order' => 1,
    ]);

    GroupProject::query()->create([
        'group_id' => $group->id,
        'title' => 'Profile Project Marker',
        'slug' => 'profile-project-marker',
        'summary' => 'Project tied to profile history.',
        'visibility' => GroupProject::VISIBILITY_PUBLIC,
        'status' => GroupProject::STATUS_ACTIVE,
        'created_by_user_id' => $owner->id,
    ]);

    $project = GroupProject::query()->where('group_id', $group->id)->firstOrFail();

    GroupProjectMember::query()->create([
        'group_project_id' => $project->id,
        'user_id' => $contributor->id,
        'role_label' => 'Art Director',
        'is_lead' => false,
    ]);

    Artwork::factory()->for($contributor, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $contributor->id,
        'primary_author_user_id' => $contributor->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'group_review_status' => 'approved',
        'published_at' => now(),
    ]);

    app(\App\Services\GroupReputationService::class)->refreshGroup($group);

    expect(GroupContributorStat::query()->where('group_id', $group->id)->where('user_id', $contributor->id)->exists())->toBeTrue();

    $this->get(route('profile.show', ['username' => strtolower((string) $contributor->username)]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Profile/ProfileShow')
            ->where('groupContributionHistory.0.group.slug', $group->slug)
            ->where('groupContributionHistory.0.counts.releases', 1)
            ->where('groupContributionHistory.0.counts.credited_artworks', 1)
            ->has('groupContributionHistory.0.role_labels', 2)
        );
});

it('notifies contributors about release assignment milestones and earned badges', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.store', ['group' => $group]), [
            'title' => 'Notification Release Marker',
            'summary' => 'A release used for notification coverage.',
            'description' => 'Notification coverage body.',
            'visibility' => GroupRelease::VISIBILITY_PUBLIC,
            'status' => GroupRelease::STATUS_PLANNED,
            'current_stage' => GroupRelease::STAGE_CONCEPT,
            'featured_artwork_id' => $artwork->id,
        ])
        ->assertRedirect();

    $release = GroupRelease::query()->where('group_id', $group->id)->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.attach-contributor', ['group' => $group, 'release' => $release]), [
            'user_id' => $contributor->id,
            'role_label' => 'Packaging Lead',
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.milestones.store', ['group' => $group, 'release' => $release]), [
            'title' => 'Final assets',
            'summary' => 'Prepare the last package.',
            'status' => 'active',
            'owner_user_id' => $contributor->id,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.publish', ['group' => $group, 'release' => $release]))
        ->assertRedirect();

    expect(Notification::query()->where('user_id', $contributor->id)->where('type', 'group_release_contributor_added')->exists())->toBeTrue()
        ->and(Notification::query()->where('user_id', $contributor->id)->where('type', 'group_milestone_assigned')->exists())->toBeTrue()
        ->and(Notification::query()->where('user_id', $contributor->id)->where('type', 'group_member_badge_earned')->exists())->toBeTrue()
        ->and(Notification::query()->where('user_id', $owner->id)->where('type', 'group_badge_earned')->exists())->toBeTrue();
});

it('notifies followers when a release is scheduled and assignees when milestones are due soon', function () {
    $owner = User::factory()->create();
    $follower = User::factory()->create();
    $assignee = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $assignee->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $this->actingAs($follower)->post(route('groups.follow', ['group' => $group]))->assertOk();

    $release = GroupRelease::query()->create([
        'group_id' => $group->id,
        'title' => 'Scheduled Marker Release',
        'slug' => 'scheduled-marker-release',
        'summary' => 'A release that gets scheduled.',
        'status' => GroupRelease::STATUS_PLANNED,
        'current_stage' => GroupRelease::STAGE_CONCEPT,
        'visibility' => GroupRelease::VISIBILITY_PUBLIC,
        'created_by_user_id' => $owner->id,
    ]);

    $this->actingAs($owner)
        ->patch(route('studio.groups.releases.update', ['group' => $group, 'release' => $release]), [
            'title' => $release->title,
            'summary' => $release->summary,
            'description' => $release->description,
            'status' => GroupRelease::STATUS_SCHEDULED,
            'current_stage' => GroupRelease::STAGE_PUBLISHING,
            'visibility' => GroupRelease::VISIBILITY_PUBLIC,
            'planned_release_at' => now()->addDays(7)->toISOString(),
            'is_featured' => false,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.releases.milestones.store', ['group' => $group, 'release' => $release]), [
            'title' => 'Ship assets',
            'summary' => 'Packaging delivery',
            'status' => 'active',
            'owner_user_id' => $assignee->id,
            'due_date' => now()->addDays(2)->toDateString(),
        ])
        ->assertRedirect();

    expect(Notification::query()->where('user_id', $follower->id)->where('type', 'group_release_scheduled')->exists())->toBeTrue()
        ->and(Notification::query()->where('user_id', $assignee->id)->where('type', 'group_milestone_due_soon')->exists())->toBeTrue();
});

it('allows creating a group with richer profile metadata', function () {
    $owner = User::factory()->create();

    $this->actingAs($owner)
        ->post(route('studio.groups.store'), [
            'name' => 'Warp Collective',
            'slug' => 'warp-collective',
            'headline' => 'Retro visual lab',
            'bio' => 'Full about text for the collective.',
            'type' => 'Studio',
            'founded_at' => '2021-06-15',
            'avatar_path' => 'https://cdn.example.test/groups/warp-avatar.webp',
            'banner_path' => 'https://cdn.example.test/groups/warp-cover.webp',
            'website_url' => 'https://warp.example.test',
            'links_json' => [
                ['label' => 'Behance', 'url' => 'https://behance.example.test/warp'],
            ],
            'visibility' => Group::VISIBILITY_PUBLIC,
            'membership_policy' => Group::MEMBERSHIP_INVITE_ONLY,
        ])
        ->assertRedirect();

    $group = Group::query()->where('slug', 'warp-collective')->firstOrFail();

    expect($group->type)->toBe('Studio')
        ->and($group->headline)->toBe('Retro visual lab')
        ->and($group->avatar_path)->toBe('https://cdn.example.test/groups/warp-avatar.webp')
        ->and($group->banner_path)->toBe('https://cdn.example.test/groups/warp-cover.webp');
});

it('stores uploaded group media during group creation', function () {
    $disk = (string) config('uploads.object_storage.disk', 's3');
    Storage::fake($disk);

    $owner = User::factory()->create();

    $this->actingAs($owner)
        ->post(route('studio.groups.store'), [
            'name' => 'Pixel Forge',
            'slug' => 'pixel-forge',
            'headline' => 'Shared pixel craft',
            'bio' => 'A tiny studio with a big release calendar.',
            'visibility' => Group::VISIBILITY_PUBLIC,
            'membership_policy' => Group::MEMBERSHIP_INVITE_ONLY,
            'avatar_file' => UploadedFile::fake()->image('group-avatar.png', 512, 512),
            'banner_file' => UploadedFile::fake()->image('group-banner.png', 1600, 600),
        ])
        ->assertRedirect();

    $group = Group::query()->where('slug', 'pixel-forge')->firstOrFail();

    expect($group->avatar_path)->toStartWith('groups/' . $group->id . '/avatar/')
        ->and($group->banner_path)->toStartWith('groups/' . $group->id . '/banner/');

    Storage::disk($disk)->assertExists((string) $group->avatar_path);
    Storage::disk($disk)->assertExists((string) $group->banner_path);
});

it('lets owners update uploaded group media and featured artwork selection', function () {
    $disk = (string) config('uploads.object_storage.disk', 's3');
    Storage::fake($disk);

    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $featuredArtwork = Artwork::factory()->for($owner, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $owner->id,
        'primary_author_user_id' => $owner->id,
        'artwork_status' => 'published',
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_public' => true,
        'is_approved' => true,
        'published_at' => now(),
    ]);

    $this->actingAs($owner)
        ->post(route('studio.groups.update', ['group' => $group]), [
            '_method' => 'PATCH',
            'name' => 'Warp Collective Updated',
            'slug' => 'warp-collective-updated',
            'headline' => 'Refined release identity',
            'bio' => 'Now with sharper curation.',
            'visibility' => Group::VISIBILITY_PUBLIC,
            'membership_policy' => Group::MEMBERSHIP_INVITE_ONLY,
            'featured_artwork_id' => $featuredArtwork->id,
            'avatar_file' => UploadedFile::fake()->image('updated-avatar.png', 512, 512),
            'banner_file' => UploadedFile::fake()->image('updated-banner.png', 1600, 600),
        ])
        ->assertRedirect(route('studio.groups.settings', ['group' => 'warp-collective-updated']));

    $group->refresh();

    expect($group->slug)->toBe('warp-collective-updated')
        ->and($group->featured_artwork_id)->toBe($featuredArtwork->id)
        ->and($group->avatar_path)->toStartWith('groups/' . $group->id . '/avatar/')
        ->and($group->banner_path)->toStartWith('groups/' . $group->id . '/banner/');

    Storage::disk($disk)->assertExists((string) $group->avatar_path);
    Storage::disk($disk)->assertExists((string) $group->banner_path);
});

it('renders the studio invitations page with pending invites for authorized members', function () {
    $owner = User::factory()->create();
    $invitee = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupInvitation::query()->create([
        'group_id' => $group->id,
        'invited_user_id' => $invitee->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => GroupInvitation::STATUS_PENDING,
        'token' => Str::random(64),
        'invited_at' => now(),
        'expires_at' => now()->addDays(7),
    ]);

    $this->actingAs($owner)
        ->get(route('studio.groups.invitations', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupInvitations')
            ->where('studioGroup.slug', $group->slug)
            ->has('invitations', 1)
            ->where('invitations.0.user.username', $invitee->username)
            ->where('invitations.0.status', Group::STATUS_PENDING)
        );
});

it('allows owners to invite and revoke a pending group invitation', function () {
    $owner = User::factory()->create();
    $invitee = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->actingAs($owner)
        ->postJson(route('studio.groups.members.store', ['group' => $group]), [
            'username' => $invitee->username,
            'role' => 'contributor',
            'note' => 'Join the next release pack.',
            'expires_in_days' => 5,
        ])
        ->assertOk()
        ->assertJsonPath('member.status', GroupInvitation::STATUS_PENDING);

    $invitation = GroupInvitation::query()
        ->where('group_id', $group->id)
        ->where('invited_user_id', $invitee->id)
        ->firstOrFail();

    expect($invitation->role)->toBe(Group::ROLE_MEMBER)
        ->and($invitation->status)->toBe(GroupInvitation::STATUS_PENDING)
        ->and(GroupMember::query()->where('group_id', $group->id)->where('user_id', $invitee->id)->exists())->toBeFalse();

    $this->actingAs($owner)
        ->deleteJson(route('studio.groups.invitations.destroy', ['group' => $group, 'invitation' => $invitation]))
        ->assertOk();

    expect($invitation->fresh()->status)->toBe(GroupInvitation::STATUS_REVOKED);
});

it('allows non-managing members to view the members page but not the invitations manager', function () {
    $owner = User::factory()->create();
    $editor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $editor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $this->actingAs($editor)
        ->get(route('studio.groups.members', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupMembers')
            ->where('canManageMembers', false)
            ->where('studioGroup.permissions.can_manage_members', false)
            ->missing('endpoints.invite')
        );

    $this->actingAs($editor)
        ->get(route('studio.groups.invitations', ['group' => $group]))
        ->assertForbidden();
});

it('manages v3 projects challenges and events across studio and public group pages', function () {
    $owner = User::factory()->create();
    $viewer = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->actingAs($owner)
        ->post(route('studio.groups.projects.store', ['group' => $group]), [
            'title' => 'Launch Capsule',
            'summary' => 'Primary release planning hub.',
            'description' => 'Tracks the flagship group release.',
            'status' => GroupProject::STATUS_ACTIVE,
            'visibility' => GroupProject::VISIBILITY_PUBLIC,
            'start_date' => now()->toDateString(),
            'target_date' => now()->addWeeks(2)->toDateString(),
        ])
        ->assertRedirect();

    $project = GroupProject::query()->where('group_id', $group->id)->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.projects.status', ['group' => $group, 'project' => $project]), [
            'status' => GroupProject::STATUS_RELEASED,
        ])
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.challenges.store', ['group' => $group]), [
            'title' => 'Spring Prompt',
            'summary' => 'A short challenge for the next drop.',
            'description' => 'Members submit concept work for the release window.',
            'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
            'participation_scope' => GroupChallenge::PARTICIPATION_GROUP_ONLY,
            'status' => GroupChallenge::STATUS_DRAFT,
            'start_at' => now()->toDateTimeString(),
            'end_at' => now()->addDays(7)->toDateTimeString(),
        ])
        ->assertRedirect();

    $challenge = GroupChallenge::query()->where('group_id', $group->id)->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.challenges.publish', ['group' => $group, 'challenge' => $challenge]))
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.events.store', ['group' => $group]), [
            'title' => 'Release Stream',
            'summary' => 'Live reveal and Q&A.',
            'description' => 'A public livestream for the group release.',
            'event_type' => GroupEvent::TYPE_LIVESTREAM,
            'visibility' => GroupEvent::VISIBILITY_PUBLIC,
            'status' => GroupEvent::STATUS_DRAFT,
            'start_at' => now()->addDays(3)->toDateTimeString(),
            'end_at' => now()->addDays(3)->addHour()->toDateTimeString(),
            'timezone' => 'UTC',
        ])
        ->assertRedirect();

    $event = GroupEvent::query()->where('group_id', $group->id)->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.events.publish', ['group' => $group, 'event' => $event]))
        ->assertRedirect();

    expect(GroupActivityItem::query()->where('group_id', $group->id)->count())->toBeGreaterThanOrEqual(4);

    $this->actingAs($owner)
        ->get(route('studio.groups.show', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupDashboard')
            ->where('dashboard.projects_count', 1)
            ->where('dashboard.active_challenges_count', 1)
            ->where('dashboard.events_count', 1)
            ->has('recentProjects', 1)
            ->has('recentChallenges', 1)
            ->has('recentEvents', 1)
            ->has('recentActivity')
        );

    $this->actingAs($viewer)
        ->get(route('groups.show', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('group.featured_project.title', 'Launch Capsule')
            ->where('group.active_challenge.title', 'Spring Prompt')
            ->where('group.upcoming_event.title', 'Release Stream')
        );

    $this->actingAs($viewer)
        ->get(route('groups.section', ['group' => $group, 'section' => 'projects']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->where('section', 'projects')
            ->where('projects.0.title', 'Launch Capsule')
        );

    $this->actingAs($viewer)
        ->get(route('groups.section', ['group' => $group, 'section' => 'challenges']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->where('section', 'challenges')
            ->where('challenges.0.title', 'Spring Prompt')
        );

    $this->actingAs($viewer)
        ->get(route('groups.section', ['group' => $group, 'section' => 'events']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->where('section', 'events')
            ->where('events.0.title', 'Release Stream')
        );

    $this->get('/')
        ->assertOk();
});

it('handles group assets and keeps public activity limited to public items', function () {
    Storage::fake('local');

    $owner = User::factory()->create();
    $viewer = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->actingAs($owner)
        ->post(route('studio.groups.assets.store', ['group' => $group]), [
            'title' => 'Launch Brief',
            'description' => 'Public release brief.',
            'category' => GroupAsset::CATEGORY_REFERENCE,
            'visibility' => GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD,
            'status' => GroupAsset::STATUS_ACTIVE,
            'file' => UploadedFile::fake()->create('launch-brief.pdf', 12, 'application/pdf'),
        ])
        ->assertRedirect();

    $publicAsset = GroupAsset::query()->where('group_id', $group->id)->where('visibility', GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD)->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.assets.store', ['group' => $group]), [
            'title' => 'Internal Source Pack',
            'description' => 'Members-only working files.',
            'category' => GroupAsset::CATEGORY_SOURCE_PACK,
            'visibility' => GroupAsset::VISIBILITY_MEMBERS_ONLY,
            'status' => GroupAsset::STATUS_ACTIVE,
            'file' => UploadedFile::fake()->create('internal-pack.zip', 24, 'application/zip'),
        ])
        ->assertRedirect();

    $internalAsset = GroupAsset::query()->where('group_id', $group->id)->where('visibility', GroupAsset::VISIBILITY_MEMBERS_ONLY)->firstOrFail();

    Storage::disk('local')->assertExists((string) $publicAsset->file_path);
    Storage::disk('local')->assertExists((string) $internalAsset->file_path);

    $this->actingAs($viewer)
        ->get(route('groups.assets.download', ['group' => $group, 'asset' => $publicAsset]))
        ->assertOk();

    $this->actingAs($viewer)
        ->get(route('groups.assets.download', ['group' => $group, 'asset' => $internalAsset]))
        ->assertForbidden();

    $publicActivity = GroupActivityItem::query()->where('group_id', $group->id)->where('visibility', 'public')->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.activity.pin', ['group' => $group, 'item' => $publicActivity]), [
            'is_pinned' => true,
        ])
        ->assertRedirect();

    expect($publicActivity->fresh()->is_pinned)->toBeTrue();

    $this->actingAs($viewer)
        ->get(route('groups.activity.index', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('section', 'activity')
            ->has('activity', 1)
            ->where('activity.0.subject.type', 'group_asset')
            ->where('activity.0.headline', fn (string $headline) => str_contains($headline, 'Launch Brief'))
        );

    $this->actingAs($owner)
        ->get(route('studio.groups.activity', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupActivity')
            ->has('activity', 2)
            ->where('activity.0.is_pinned', true)
        );
});

it('allows invited users to accept a pending group invitation', function () {
    $owner = User::factory()->create();
    $invitee = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $invitation = GroupInvitation::query()->create([
        'group_id' => $group->id,
        'invited_user_id' => $invitee->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => GroupInvitation::STATUS_PENDING,
        'token' => Str::random(64),
        'invited_at' => now(),
        'expires_at' => now()->addDays(7),
    ]);

    $this->actingAs($invitee)
        ->post(route('studio.groups.invitations.accept', ['invitation' => $invitation]))
        ->assertRedirect(route('studio.groups.members', ['group' => $group]));

    $member = GroupMember::query()
        ->where('group_id', $group->id)
        ->where('user_id', $invitee->id)
        ->firstOrFail();

    expect($member->status)->toBe(Group::STATUS_ACTIVE)
        ->and($member->accepted_at)->not->toBeNull()
        ->and($invitation->fresh()->status)->toBe(GroupInvitation::STATUS_ACCEPTED);
});

it('allows owners to change a member role', function () {
    $owner = User::factory()->create();
    $memberUser = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $member = GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $memberUser->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $this->actingAs($owner)
        ->patchJson(route('studio.groups.members.update', ['group' => $group, 'member' => $member]), [
            'role' => Group::ROLE_EDITOR,
        ])
        ->assertOk()
        ->assertJsonPath('member.role', Group::ROLE_EDITOR);

    expect($member->fresh()->role)->toBe(Group::ROLE_EDITOR);
});

it('allows owners to remove active members from the group', function () {
    $owner = User::factory()->create();
    $memberUser = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $member = GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $memberUser->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $this->actingAs($owner)
        ->deleteJson(route('studio.groups.members.destroy', ['group' => $group, 'member' => $member]))
        ->assertOk();

    expect($member->fresh()->status)->toBe(Group::STATUS_REVOKED);
});

it('prevents removing the group owner without ownership transfer', function () {
    $owner = User::factory()->create();
    $admin = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);
    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $admin->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_ADMIN,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $ownerMember = GroupMember::query()
        ->where('group_id', $group->id)
        ->where('user_id', $owner->id)
        ->firstOrFail();

    $this->actingAs($admin)
        ->deleteJson(route('studio.groups.members.destroy', ['group' => $group, 'member' => $ownerMember]))
        ->assertStatus(422)
        ->assertJsonValidationErrors(['member']);
});

it('allows owners to transfer ownership to another active member', function () {
    $owner = User::factory()->create();
    $admin = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $member = GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $admin->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_ADMIN,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $this->actingAs($owner)
        ->postJson(route('studio.groups.members.transfer', ['group' => $group, 'member' => $member]))
        ->assertOk();

    $group->refresh();

    expect($group->owner_user_id)->toBe($admin->id)
        ->and($member->fresh()->role)->toBe(Group::ROLE_OWNER)
        ->and(GroupMember::query()->where('group_id', $group->id)->where('user_id', $owner->id)->firstOrFail()->role)->toBe(Group::ROLE_ADMIN);
});

it('filters the group asset library by category and search query in studio', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupAsset::query()->create([
        'group_id' => $group->id,
        'title' => 'Brand Mark Kit',
        'description' => 'Official SVG and PNG logos.',
        'category' => GroupAsset::CATEGORY_LOGO,
        'file_path' => 'group-assets/' . $group->id . '/brand-mark.zip',
        'visibility' => GroupAsset::VISIBILITY_MEMBERS_ONLY,
        'status' => GroupAsset::STATUS_ACTIVE,
        'uploaded_by_user_id' => $owner->id,
        'approved_by_user_id' => $owner->id,
        'is_featured' => false,
        'file_meta_json' => ['original_name' => 'brand-mark.zip'],
    ]);

    GroupAsset::query()->create([
        'group_id' => $group->id,
        'title' => 'Palette Reference',
        'description' => 'Color ramps for the spring pack.',
        'category' => GroupAsset::CATEGORY_PALETTE,
        'file_path' => 'group-assets/' . $group->id . '/palette-reference.pdf',
        'visibility' => GroupAsset::VISIBILITY_MEMBERS_ONLY,
        'status' => GroupAsset::STATUS_ACTIVE,
        'uploaded_by_user_id' => $owner->id,
        'approved_by_user_id' => $owner->id,
        'is_featured' => false,
        'file_meta_json' => ['original_name' => 'palette-reference.pdf'],
    ]);

    $this->actingAs($owner)
        ->get(route('studio.groups.assets.index', ['group' => $group, 'category' => GroupAsset::CATEGORY_LOGO, 'q' => 'brand']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Studio/StudioGroupAssets')
            ->where('listing.filters.category', GroupAsset::CATEGORY_LOGO)
            ->where('listing.filters.q', 'brand')
            ->has('listing.items', 1)
            ->where('listing.items.0.title', 'Brand Mark Kit')
        );
});

it('allows public challenge entries and sends richer v3 notifications', function () {
    $owner = User::factory()->create();
    $follower = User::factory()->create();
    $participant = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    DB::table('group_follows')->insert([
        'group_id' => $group->id,
        'user_id' => $follower->id,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $challenge = GroupChallenge::query()->create([
        'group_id' => $group->id,
        'title' => 'Open Prompt',
        'slug' => 'open-prompt',
        'summary' => 'Public challenge',
        'description' => 'Open to outside participants.',
        'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
        'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
        'status' => GroupChallenge::STATUS_ACTIVE,
        'start_at' => now()->subDay(),
        'end_at' => now()->addDay(),
        'created_by_user_id' => $owner->id,
    ]);

    $participantArtwork = Artwork::factory()->for($participant, 'user')->create([
        'is_public' => true,
        'visibility' => Artwork::VISIBILITY_PUBLIC,
        'is_approved' => true,
        'artwork_status' => 'published',
        'published_at' => now(),
    ]);

    $this->actingAs($participant)
        ->post(route('groups.challenges.entries.store', ['group' => $group, 'challenge' => $challenge]), [
            'artwork_id' => $participantArtwork->id,
        ])
        ->assertRedirect();

    expect(DB::table('group_challenge_artworks')
        ->where('group_challenge_id', $challenge->id)
        ->where('artwork_id', $participantArtwork->id)
        ->exists())->toBeTrue();

    $project = GroupProject::query()->create([
        'group_id' => $group->id,
        'title' => 'Status Capsule',
        'slug' => 'status-capsule',
        'summary' => 'Project status notifications',
        'visibility' => GroupProject::VISIBILITY_PUBLIC,
        'status' => GroupProject::STATUS_ACTIVE,
        'created_by_user_id' => $owner->id,
    ]);

    app(\App\Services\GroupProjectService::class)->updateStatus($project, $owner, GroupProject::STATUS_REVIEW);

    $event = GroupEvent::query()->create([
        'group_id' => $group->id,
        'title' => 'Updated Event',
        'slug' => 'updated-event',
        'summary' => 'Followers should see updates',
        'description' => 'Event update notifications.',
        'event_type' => GroupEvent::TYPE_SHOWCASE,
        'visibility' => GroupEvent::VISIBILITY_PUBLIC,
        'start_at' => now()->addDays(3),
        'end_at' => now()->addDays(3)->addHour(),
        'timezone' => 'UTC',
        'status' => GroupEvent::STATUS_PUBLISHED,
        'published_at' => now()->subHour(),
        'created_by_user_id' => $owner->id,
    ]);

    app(\App\Services\GroupEventService::class)->update($event, $owner, [
        'title' => 'Updated Event',
        'summary' => 'Followers should see updates',
        'description' => 'Event update notifications.',
        'event_type' => GroupEvent::TYPE_SHOWCASE,
        'visibility' => GroupEvent::VISIBILITY_PUBLIC,
        'start_at' => now()->addDays(4)->toDateTimeString(),
        'end_at' => now()->addDays(4)->addHour()->toDateTimeString(),
        'timezone' => 'UTC',
    ]);

    $asset = GroupAsset::query()->create([
        'group_id' => $group->id,
        'title' => 'Pending Approval Pack',
        'description' => 'Awaiting approval',
        'category' => GroupAsset::CATEGORY_PROMO,
        'file_path' => 'group-assets/' . $group->id . '/pending-approval.zip',
        'visibility' => GroupAsset::VISIBILITY_MEMBERS_ONLY,
        'status' => GroupAsset::STATUS_ARCHIVED,
        'uploaded_by_user_id' => $participant->id,
        'approved_by_user_id' => null,
        'is_featured' => false,
        'file_meta_json' => ['original_name' => 'pending-approval.zip'],
    ]);

    app(\App\Services\GroupAssetService::class)->update($asset, $owner, [
        'title' => 'Pending Approval Pack',
        'description' => 'Approved for members',
        'category' => GroupAsset::CATEGORY_PROMO,
        'visibility' => GroupAsset::VISIBILITY_MEMBERS_ONLY,
        'status' => GroupAsset::STATUS_ACTIVE,
        'linked_project_id' => null,
        'is_featured' => false,
    ]);

    expect(Notification::query()->where('user_id', $follower->id)->where('type', 'group_project_status_changed')->exists())->toBeTrue()
        ->and(Notification::query()->where('user_id', $follower->id)->where('type', 'group_event_updated')->exists())->toBeTrue()
        ->and(Notification::query()->where('user_id', $participant->id)->where('type', 'group_asset_approved')->exists())->toBeTrue();
});

it('creates group drafts in group context for contributors', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();
    $categoryId = createCategoryForGroupsFeatureTests();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $response = $this->actingAs($contributor)->postJson('/api/artworks', [
        'title' => 'Crew Draft',
        'description' => 'Draft created in group context.',
        'category' => $categoryId,
        'group' => $group->slug,
        'is_mature' => false,
    ]);

    $response->assertCreated();

    $artwork = Artwork::query()->findOrFail((int) $response->json('artwork_id'));

    expect($artwork->group_id)->toBe($group->id)
        ->and($artwork->published_as_type)->toBe(Artwork::PUBLISHED_AS_GROUP)
        ->and($artwork->published_as_id)->toBe($group->id)
        ->and($artwork->uploaded_by_user_id)->toBe($contributor->id)
        ->and($artwork->primary_author_user_id)->toBe($contributor->id)
        ->and($artwork->artwork_status)->toBe('draft');
});

it('prevents contributors from publishing group drafts directly', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->unpublished()->for($contributor, 'user')->create([
        'group_id' => $group->id,
        'uploaded_by_user_id' => $contributor->id,
        'primary_author_user_id' => $contributor->id,
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
    ]);

    $this->actingAs($contributor)
        ->postJson("/api/uploads/{$artwork->id}/publish", [
            'group' => $group->slug,
            'visibility' => 'public',
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['group']);
});

it('prevents non-members from publishing as a group', function () {
    $owner = User::factory()->create();
    $outsider = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $artwork = Artwork::factory()->unpublished()->for($outsider, 'user')->create([
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
    ]);

    $this->actingAs($outsider)
        ->postJson("/api/uploads/{$artwork->id}/publish", [
            'group' => $group->slug,
            'visibility' => 'public',
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['group']);
});

it('returns public groups from search', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'name' => 'Searchable Crew',
        'slug' => 'searchable-crew',
        'visibility' => Group::VISIBILITY_PUBLIC,
        'status' => Group::LIFECYCLE_ACTIVE,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->getJson(route('api.search.groups', ['q' => 'searchable']))
        ->assertOk()
        ->assertJsonPath('data.0.slug', 'searchable-crew')
        ->assertJsonPath('data.0.type', 'group');
});

it('keeps unlisted groups off the directory while still allowing direct access', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_UNLISTED,
        'status' => Group::LIFECYCLE_ACTIVE,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->get(route('groups.index'))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupIndex')
            ->missing('groups.data.0.slug')
        );

    $this->get(route('groups.show', ['group' => $group]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('group.slug', $group->slug)
            ->where('group.visibility', Group::VISIBILITY_UNLISTED)
        );
});

it('prevents publishing as an archived group', function () {
    $owner = User::factory()->create();
    $editor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'status' => Group::LIFECYCLE_ARCHIVED,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $editor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->unpublished()->for($editor, 'user')->create([
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
    ]);

    $this->actingAs($editor)
        ->postJson("/api/uploads/{$artwork->id}/publish", [
            'group' => $group->slug,
            'visibility' => 'public',
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['group']);
});

it('handles group join requests and approvals', function () {
    $owner = User::factory()->create();
    $applicant = User::factory()->create();

    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
        'membership_policy' => Group::MEMBERSHIP_REQUEST_TO_JOIN,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->actingAs($applicant)
        ->post(route('groups.join-requests.store', ['group' => $group]), [
            'message' => 'I can help with release artwork.',
            'desired_role' => 'contributor',
        ])
        ->assertRedirect();

    $joinRequest = GroupJoinRequest::query()->firstOrFail();

    expect($joinRequest->status)->toBe(GroupJoinRequest::STATUS_PENDING)
        ->and($joinRequest->desired_role)->toBe(Group::ROLE_MEMBER);

    $this->actingAs($owner)
        ->post(route('studio.groups.join-requests.approve', ['group' => $group, 'joinRequest' => $joinRequest]), [
            'role' => 'editor',
            'review_notes' => 'Approved for release support.',
        ])
        ->assertRedirect();

    expect($joinRequest->fresh()->status)->toBe(GroupJoinRequest::STATUS_APPROVED)
        ->and(GroupMember::query()
            ->where('group_id', $group->id)
            ->where('user_id', $applicant->id)
            ->where('status', Group::STATUS_ACTIVE)
            ->where('role', Group::ROLE_EDITOR)
            ->exists())->toBeTrue();
});

it('submits contributor artwork for group review instead of direct publish', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->unpublished()->for($contributor, 'user')->create([
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
        'slug' => 'review-me-artwork',
    ]);

    $this->actingAs($contributor)
        ->postJson("/api/uploads/{$artwork->id}/submit-review", [
            'group' => $group->slug,
            'title' => 'Co-op release teaser',
            'description' => 'Ready for group review.',
            'visibility' => 'public',
            'tags' => ['teaser', 'group'],
        ])
        ->assertOk()
        ->assertJson([
            'success' => true,
            'artwork_id' => $artwork->id,
            'status' => 'submitted_for_review',
            'group_review_status' => 'submitted',
        ]);

    $artwork->refresh();

    expect($artwork->group_id)->toBe($group->id)
        ->and($artwork->group_review_status)->toBe('submitted')
        ->and($artwork->published_as_type)->toBe(Artwork::PUBLISHED_AS_GROUP)
        ->and($artwork->is_public)->toBeFalse()
        ->and($artwork->artwork_status)->toBe('draft');
});

it('renders public group posts and recruitment payloads', function () {
    $owner = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupRecruitmentProfile::query()->create([
        'group_id' => $group->id,
        'is_recruiting' => true,
        'headline' => 'Looking for animation support',
        'description' => 'Need collaborators for short-form loops.',
        'roles_json' => ['Animator', 'Sound designer'],
        'skills_json' => ['After Effects', 'Pixel art'],
        'contact_mode' => 'join_request',
        'visibility' => 'public',
    ]);

    GroupPost::query()->create([
        'group_id' => $group->id,
        'author_user_id' => $owner->id,
        'type' => GroupPost::TYPE_ANNOUNCEMENT,
        'title' => 'Spring update',
        'slug' => 'spring-update',
        'excerpt' => 'A new release calendar is live.',
        'content' => 'Detailed update body.',
        'status' => GroupPost::STATUS_PUBLISHED,
        'is_pinned' => true,
        'published_at' => now(),
    ]);

    $this->get(route('groups.section', ['group' => $group, 'section' => 'posts']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('section', 'posts')
            ->where('recruitment.is_recruiting', true)
            ->where('posts.0.title', 'Spring update')
            ->where('group.pinned_post.title', 'Spring update')
        );
});

it('blocks duplicate join requests and allows managers to reject them', function () {
    $owner = User::factory()->create();
    $applicant = User::factory()->create();

    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
        'membership_policy' => Group::MEMBERSHIP_REQUEST_TO_JOIN,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $this->actingAs($applicant)
        ->post(route('groups.join-requests.store', ['group' => $group]), [
            'message' => 'First request',
            'desired_role' => 'contributor',
        ])
        ->assertRedirect();

    $this->actingAs($applicant)
        ->post(route('groups.join-requests.store', ['group' => $group]), [
            'message' => 'Duplicate request',
            'desired_role' => 'contributor',
        ])
        ->assertSessionHasErrors('group');

    $joinRequest = GroupJoinRequest::query()->firstOrFail();

    $this->actingAs($owner)
        ->post(route('studio.groups.join-requests.reject', ['group' => $group, 'joinRequest' => $joinRequest]), [
            'review_notes' => 'Not a fit right now.',
        ])
        ->assertRedirect();

    expect($joinRequest->fresh()->status)->toBe(GroupJoinRequest::STATUS_REJECTED);
});

it('supports explicit allow permission overrides for contributors', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $member = GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    expect($group->fresh()->canManagePosts($contributor))->toBeFalse();

    $this->actingAs($owner)
        ->patchJson(route('studio.groups.members.permissions.update', ['group' => $group, 'member' => $member]), [
            'permission_overrides' => [
                ['key' => Group::PERMISSION_MANAGE_POSTS, 'is_allowed' => true],
            ],
        ])
        ->assertOk()
        ->assertJsonPath('member.permission_overrides.0.key', Group::PERMISSION_MANAGE_POSTS)
        ->assertJsonPath('member.permission_overrides.0.is_allowed', true);

    expect($group->fresh()->canManagePosts($contributor))->toBeTrue();
});

it('supports explicit deny permission overrides for editor capabilities', function () {
    $owner = User::factory()->create();
    $editor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $member = GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $editor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_EDITOR,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    expect($group->fresh()->canManagePosts($editor))->toBeTrue()
        ->and($group->fresh()->canReviewSubmissions($editor))->toBeTrue();

    $this->actingAs($owner)
        ->patchJson(route('studio.groups.members.permissions.update', ['group' => $group, 'member' => $member]), [
            'permission_overrides' => [
                ['key' => Group::PERMISSION_MANAGE_POSTS, 'is_allowed' => false],
                ['key' => Group::PERMISSION_REVIEW_SUBMISSIONS, 'is_allowed' => false],
            ],
        ])
        ->assertOk();

    $reloaded = $group->fresh();

    expect($reloaded->canManagePosts($editor))->toBeFalse()
        ->and($reloaded->canReviewSubmissions($editor))->toBeFalse();
});

it('allows authorized reviewers to request changes and reject submissions', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    GroupMember::query()->create([
        'group_id' => $group->id,
        'user_id' => $contributor->id,
        'invited_by_user_id' => $owner->id,
        'role' => Group::ROLE_MEMBER,
        'status' => Group::STATUS_ACTIVE,
        'invited_at' => now(),
        'accepted_at' => now(),
    ]);

    $artwork = Artwork::factory()->unpublished()->for($contributor, 'user')->create([
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
    ]);

    $this->actingAs($contributor)
        ->postJson("/api/uploads/{$artwork->id}/submit-review", [
            'group' => $group->slug,
            'title' => 'Review me',
            'description' => 'Needs review.',
            'visibility' => 'public',
        ])
        ->assertOk();

    $artwork->refresh();

    $this->actingAs($owner)
        ->post(route('studio.groups.artworks.needs-changes', ['group' => $group, 'artwork' => $artwork]), [
            'review_notes' => 'Tighten the crop.',
        ])
        ->assertRedirect();

    expect($artwork->fresh()->group_review_status)->toBe('needs_changes');

    $this->actingAs($contributor)
        ->postJson("/api/uploads/{$artwork->id}/submit-review", [
            'group' => $group->slug,
            'title' => 'Review me again',
            'description' => 'Updated version.',
            'visibility' => 'public',
        ])
        ->assertOk();

    $artwork->refresh();

    $this->actingAs($owner)
        ->post(route('studio.groups.artworks.reject', ['group' => $group, 'artwork' => $artwork]), [
            'review_notes' => 'Still not ready.',
        ])
        ->assertRedirect();

    expect($artwork->fresh()->group_review_status)->toBe('rejected');
});

it('blocks unauthorized members from reviewing group submissions', function () {
    $owner = User::factory()->create();
    $contributor = User::factory()->create();
    $reviewer = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create();

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    foreach ([$contributor, $reviewer] as $user) {
        GroupMember::query()->create([
            'group_id' => $group->id,
            'user_id' => $user->id,
            'invited_by_user_id' => $owner->id,
            'role' => Group::ROLE_MEMBER,
            'status' => Group::STATUS_ACTIVE,
            'invited_at' => now(),
            'accepted_at' => now(),
        ]);
    }

    $artwork = Artwork::factory()->unpublished()->for($contributor, 'user')->create([
        'is_public' => false,
        'visibility' => Artwork::VISIBILITY_PRIVATE,
        'is_approved' => false,
        'artwork_status' => 'draft',
        'slug' => 'review-target-artwork',
    ]);

    $this->actingAs($contributor)
        ->postJson("/api/uploads/{$artwork->id}/submit-review", [
            'group' => $group->slug,
            'title' => 'Review target',
            'description' => 'Queued for review.',
            'visibility' => 'public',
        ])
        ->assertOk();

    $artwork->refresh();

    $this->actingAs($reviewer)
        ->post(route('studio.groups.artworks.approve', ['group' => $group, 'artwork' => $artwork]), [
            'review_notes' => 'I should not be able to do this.',
        ])
        ->assertForbidden();
});

it('updates recruitment with controlled values, notifies followers, and enriches group search', function () {
    $owner = User::factory()->create();
    $follower = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    DB::table('group_follows')->insert([
        'group_id' => $group->id,
        'user_id' => $follower->id,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $this->actingAs($owner)
        ->patch(route('studio.groups.recruitment.update', ['group' => $group]), [
            'is_recruiting' => true,
            'headline' => 'Looking for loop artists',
            'description' => 'We need animation support for short-form loops.',
            'roles_json' => ['Animator', 'Sound Designer'],
            'skills_json' => ['After Effects', 'Pixel Art'],
            'contact_mode' => 'join_request',
            'visibility' => 'public',
        ])
        ->assertRedirect();

    expect(Notification::query()
        ->where('user_id', $follower->id)
        ->where('type', 'group_recruitment_updated')
        ->exists())->toBeTrue();

    $this->getJson(route('api.search.groups', ['q' => 'loop']))
        ->assertOk()
        ->assertJsonPath('data.0.is_recruiting', true)
        ->assertJsonPath('data.0.recruitment_headline', 'Looking for loop artists');
});

it('publishes and pins posts, hides archived posts from public listings, and supports group post reporting with seo payloads', function () {
    $owner = User::factory()->create();
    $viewer = User::factory()->create();
    $group = Group::factory()->for($owner, 'owner')->create([
        'visibility' => Group::VISIBILITY_PUBLIC,
    ]);

    app(GroupMembershipService::class)->ensureOwnerMembership($group);

    $draft = GroupPost::query()->create([
        'group_id' => $group->id,
        'author_user_id' => $owner->id,
        'type' => GroupPost::TYPE_ANNOUNCEMENT,
        'title' => 'Roadmap drop',
        'slug' => 'roadmap-drop',
        'excerpt' => 'The next release roadmap is ready.',
        'content' => 'Full roadmap copy.',
        'status' => GroupPost::STATUS_DRAFT,
        'is_pinned' => false,
    ]);

    $archived = GroupPost::query()->create([
        'group_id' => $group->id,
        'author_user_id' => $owner->id,
        'type' => GroupPost::TYPE_UPDATE,
        'title' => 'Old note',
        'slug' => 'old-note',
        'excerpt' => 'Old note excerpt.',
        'content' => 'Old content.',
        'status' => GroupPost::STATUS_DRAFT,
        'is_pinned' => false,
    ]);

    $this->actingAs($owner)
        ->post(route('studio.groups.posts.publish', ['group' => $group, 'post' => $draft]))
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.posts.pin', ['group' => $group, 'post' => $draft]))
        ->assertRedirect();

    $this->actingAs($owner)
        ->post(route('studio.groups.posts.archive', ['group' => $group, 'post' => $archived]))
        ->assertRedirect();

    $this->get(route('groups.section', ['group' => $group, 'section' => 'posts']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupShow')
            ->where('posts.0.title', 'Roadmap drop')
            ->where('group.pinned_post.title', 'Roadmap drop')
        );

    $this->get(route('groups.posts.show', ['group' => $group, 'post' => $draft]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page) => $page
            ->component('Group/GroupPostShow')
            ->where('seo.canonical', route('groups.posts.show', ['group' => $group, 'post' => $draft]))
            ->where('seo.og_type', 'article')
        );

    $this->actingAs($viewer)
        ->postJson(route('api.reports.store'), [
            'target_type' => 'group_post',
            'target_id' => $draft->id,
            'reason' => 'Spam-like announcement',
        ])
        ->assertCreated();

    expect(Report::query()->where('target_type', 'group_post')->where('target_id', $draft->id)->count())->toBe(1);
});
