<?php

declare(strict_types=1);

use App\Models\AlertRule;
use App\Models\Finding;
use App\Models\ManagedEnvironmentMembership;
use App\Models\User;
use App\Notifications\Findings\FindingEventNotification;
use App\Services\Auth\CapabilityResolver;
use App\Services\Findings\FindingNotificationService;
use App\Services\Findings\FindingWorkflowService;
use Carbon\CarbonImmutable;
use Filament\Notifications\Notification as FilamentNotification;

afterEach(function (): void {
    CarbonImmutable::setTestNow();
});

if (! function_exists('spec230ExpectedNotificationIcon')) {
    function spec230ExpectedNotificationIcon(string $status): string
    {
        return (string) data_get(
            FilamentNotification::make()->status($status)->getDatabaseMessage(),
            'icon',
            '',
        );
    }
}

function dispatchedFindingNotificationsFor(User $user): \Illuminate\Support\Collection
{
    return $user->notifications()
        ->where('type', FindingEventNotification::class)
        ->orderBy('id')
        ->get();
}

it('uses the documented recipient precedence for assignment reopen due soon and overdue', function (): void {
    [$owner, $tenant] = createUserWithTenant(role: 'owner');
    $service = app(FindingNotificationService::class);

    $assignee = User::factory()->create(['name' => 'Assignee']);
    createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator');

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'status' => Finding::STATUS_TRIAGED,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $assignee->getKey(),
        'due_at' => now()->addHours(6),
    ]);

    $service->dispatch($finding, AlertRule::EVENT_FINDINGS_ASSIGNED);
    $service->dispatch($finding, AlertRule::EVENT_FINDINGS_REOPENED);
    $service->dispatch($finding, AlertRule::EVENT_FINDINGS_DUE_SOON);
    $service->dispatch($finding, AlertRule::EVENT_FINDINGS_OVERDUE);

    expect(dispatchedFindingNotificationsFor($assignee)
        ->pluck('data.finding_event.event_type')
        ->all())
        ->toContain(AlertRule::EVENT_FINDINGS_ASSIGNED, AlertRule::EVENT_FINDINGS_REOPENED, AlertRule::EVENT_FINDINGS_DUE_SOON);

    expect(dispatchedFindingNotificationsFor($owner)
        ->pluck('data.finding_event.event_type')
        ->all())
        ->toContain(AlertRule::EVENT_FINDINGS_OVERDUE);

    $assignedNotification = dispatchedFindingNotificationsFor($assignee)
        ->first(fn ($notification): bool => data_get($notification->data, 'finding_event.event_type') === AlertRule::EVENT_FINDINGS_ASSIGNED);
    $overdueNotification = dispatchedFindingNotificationsFor($owner)
        ->first(fn ($notification): bool => data_get($notification->data, 'finding_event.event_type') === AlertRule::EVENT_FINDINGS_OVERDUE);

    expect($assignedNotification)->not->toBeNull()
        ->and(data_get($assignedNotification?->data, 'status'))->toBe('info')
        ->and(data_get($assignedNotification?->data, 'actions.0.target'))->toBe('finding_detail');
    expect($overdueNotification)->not->toBeNull()
        ->and(data_get($overdueNotification?->data, 'status'))->toBe('danger')
        ->and(data_get($overdueNotification?->data, 'icon'))->toBe(spec230ExpectedNotificationIcon('danger'))
        ->and(data_get($overdueNotification?->data, 'actions.0.target'))->toBe('finding_detail');

    $fallbackFinding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'status' => Finding::STATUS_REOPENED,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => null,
        'due_at' => now()->addHours(2),
    ]);

    $service->dispatch($fallbackFinding, AlertRule::EVENT_FINDINGS_REOPENED);
    $service->dispatch($fallbackFinding, AlertRule::EVENT_FINDINGS_DUE_SOON);

    $ownerEventTypes = dispatchedFindingNotificationsFor($owner)
        ->pluck('data.finding_event.event_type')
        ->all();

    expect($ownerEventTypes)->toContain(AlertRule::EVENT_FINDINGS_REOPENED, AlertRule::EVENT_FINDINGS_DUE_SOON);
});

it('suppresses direct delivery when the preferred recipient loses tenant access', function (): void {
    [$owner, $tenant] = createUserWithTenant(role: 'owner');
    $assignee = User::factory()->create(['name' => 'Removed Assignee']);
    createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator');

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'status' => Finding::STATUS_REOPENED,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $assignee->getKey(),
    ]);

    ManagedEnvironmentMembership::query()
        ->where('managed_environment_id', (int) $tenant->getKey())
        ->where('user_id', (int) $assignee->getKey())
        ->delete();

    $assignee->workspaces()
        ->wherePivot('workspace_id', (int) $tenant->workspace_id)
        ->detach((int) $tenant->workspace_id);

    app(CapabilityResolver::class)->clearCache();
    app(\App\Services\Auth\WorkspaceCapabilityResolver::class)->clearCache();

    $result = app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_REOPENED);

    expect($result['direct_delivery_status'])->toBe('suppressed')
        ->and(dispatchedFindingNotificationsFor($assignee))->toHaveCount(0)
        ->and(dispatchedFindingNotificationsFor($owner))->toHaveCount(0);
});

it('does not broaden delivery to the owner when the assignee is present but no longer entitled', function (): void {
    [$owner, $tenant] = createUserWithTenant(role: 'owner');
    $assignee = User::factory()->create(['name' => 'Current Assignee']);
    createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator');

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'status' => Finding::STATUS_TRIAGED,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $assignee->getKey(),
        'due_at' => now()->addHours(3),
    ]);

    $resolver = \Mockery::mock(CapabilityResolver::class);
    $resolver->shouldReceive('isMember')->andReturnTrue();
    $resolver->shouldReceive('can')
        ->andReturnUsing(function (User $user): bool {
            return $user->name !== 'Current Assignee';
        });
    app()->instance(CapabilityResolver::class, $resolver);

    $result = app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_DUE_SOON);

    expect($result['direct_delivery_status'])->toBe('suppressed')
        ->and(dispatchedFindingNotificationsFor($assignee))->toHaveCount(0)
        ->and(dispatchedFindingNotificationsFor($owner))->toHaveCount(0);
});

it('suppresses owner-only assignment edits and assignee clears from creating direct notifications', function (): void {
    [$owner, $tenant] = createUserWithTenant(role: 'owner');
    $assignee = User::factory()->create(['name' => 'Assigned Operator']);
    createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator');

    $replacementOwner = User::factory()->create(['name' => 'Replacement Owner']);
    createUserWithTenant(tenant: $tenant, user: $replacementOwner, role: 'manager');

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'status' => Finding::STATUS_TRIAGED,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $assignee->getKey(),
    ]);

    $workflow = app(FindingWorkflowService::class);

    $workflow->assign(
        finding: $finding,
        tenant: $tenant,
        actor: $owner,
        assigneeUserId: (int) $assignee->getKey(),
        ownerUserId: (int) $replacementOwner->getKey(),
    );

    $workflow->assign(
        finding: $finding->fresh(),
        tenant: $tenant,
        actor: $owner,
        assigneeUserId: null,
        ownerUserId: (int) $replacementOwner->getKey(),
    );

    expect(dispatchedFindingNotificationsFor($assignee))->toHaveCount(0)
        ->and(dispatchedFindingNotificationsFor($owner))->toHaveCount(0)
        ->and(dispatchedFindingNotificationsFor($replacementOwner))->toHaveCount(0);
});

it('suppresses due notifications for terminal findings', function (): void {
    [$owner, $tenant] = createUserWithTenant(role: 'owner');

    $finding = Finding::factory()->for($tenant)->closed()->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $owner->getKey(),
        'due_at' => now()->subHour(),
    ]);

    $result = app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_OVERDUE);

    expect($result['direct_delivery_status'])->toBe('suppressed')
        ->and(dispatchedFindingNotificationsFor($owner))->toHaveCount(0);
});

it('sends one direct notification when owner and assignee are the same entitled user', function (): void {
    [$owner, $tenant] = createUserWithTenant(role: 'owner');

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
        'status' => Finding::STATUS_IN_PROGRESS,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $owner->getKey(),
        'due_at' => now()->subHour(),
    ]);

    $result = app(FindingNotificationService::class)->dispatch($finding, AlertRule::EVENT_FINDINGS_OVERDUE);

    expect($result['direct_delivery_status'])->toBe('sent')
        ->and(dispatchedFindingNotificationsFor($owner))->toHaveCount(1)
        ->and(data_get(dispatchedFindingNotificationsFor($owner)->first(), 'data.finding_event.recipient_reason'))->toBe('current_owner')
        ->and(data_get(dispatchedFindingNotificationsFor($owner)->first(), 'data.status'))->toBe('danger')
        ->and(data_get(dispatchedFindingNotificationsFor($owner)->first(), 'data.actions.0.label'))->toBe('Open finding');
});
