<?php

declare(strict_types=1);

use App\Models\AlertRule;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\User;
use App\Notifications\Findings\FindingEventNotification;
use App\Services\Findings\FindingNotificationService;
use App\Services\Findings\FindingWorkflowService;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Carbon\CarbonImmutable;
use Filament\Facades\Filament;
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 latestFindingNotificationFor(User $user): ?\Illuminate\Notifications\DatabaseNotification
{
    return $user->notifications()
        ->where('type', FindingEventNotification::class)
        ->latest('id')
        ->first();
}

function findingNotificationCountFor(User $user, string $eventType): int
{
    return $user->notifications()
        ->where('type', FindingEventNotification::class)
        ->get()
        ->filter(fn ($notification): bool => data_get($notification->data, 'finding_event.event_type') === $eventType)
        ->count();
}

function runEvaluateAlertsForWorkspace(int $workspaceId): void
{
    $operationRun = OperationRun::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => null,
        'type' => 'alerts.evaluate',
        'status' => OperationRunStatus::Queued->value,
        'outcome' => OperationRunOutcome::Pending->value,
    ]);

    $job = new \App\Jobs\Alerts\EvaluateAlertsJob($workspaceId, (int) $operationRun->getKey());
    $job->handle(
        app(\App\Services\Alerts\AlertDispatchService::class),
        app(\App\Services\OperationRunService::class),
        app(FindingNotificationService::class),
    );
}

it('emits assignment notifications only when a new assignee is committed', function (): void {
    [$owner, $tenant] = $this->actingAsFindingOperator();
    $firstAssignee = User::factory()->create(['name' => 'First Assignee']);
    createUserWithTenant(tenant: $tenant, user: $firstAssignee, role: 'operator');

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

    $finding = $this->makeFindingForWorkflow($tenant, Finding::STATUS_TRIAGED, [
        'owner_user_id' => (int) $owner->getKey(),
    ]);

    $workflow = app(FindingWorkflowService::class);

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

    $firstNotification = latestFindingNotificationFor($firstAssignee);

    expect($firstNotification)->not->toBeNull()
        ->and(data_get($firstNotification?->data, 'title'))->toBe('Finding assigned')
        ->and(data_get($firstNotification?->data, 'status'))->toBe('info')
        ->and(data_get($firstNotification?->data, 'icon'))->toBe(spec230ExpectedNotificationIcon('info'))
        ->and(data_get($firstNotification?->data, 'actions.0.label'))->toBe('Open finding')
        ->and(data_get($firstNotification?->data, 'actions.0.target'))->toBe('finding_detail')
        ->and(data_get($firstNotification?->data, 'finding_event.event_type'))->toBe(AlertRule::EVENT_FINDINGS_ASSIGNED);

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

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

    expect(findingNotificationCountFor($firstAssignee, AlertRule::EVENT_FINDINGS_ASSIGNED))->toBe(1);

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

    $secondNotification = latestFindingNotificationFor($secondAssignee);

    expect($secondNotification)->not->toBeNull()
        ->and(data_get($secondNotification?->data, 'finding_event.event_type'))->toBe(AlertRule::EVENT_FINDINGS_ASSIGNED)
        ->and(findingNotificationCountFor($secondAssignee, AlertRule::EVENT_FINDINGS_ASSIGNED))->toBe(1);
});

it('dedupes repeated reopen dispatches for the same reopen occurrence', function (): void {
    $now = CarbonImmutable::parse('2026-04-22T09:30:00Z');
    CarbonImmutable::setTestNow($now);

    [$owner, $tenant] = $this->actingAsFindingOperator();
    $assignee = User::factory()->create(['name' => 'Assigned Operator']);
    createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator');

    $finding = $this->makeFindingForWorkflow($tenant, Finding::STATUS_RESOLVED, [
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $assignee->getKey(),
    ]);

    $reopened = app(FindingWorkflowService::class)->reopenBySystem(
        finding: $finding,
        tenant: $tenant,
        reopenedAt: $now,
    );

    expect(findingNotificationCountFor($assignee, AlertRule::EVENT_FINDINGS_REOPENED))->toBe(1);

    app(FindingNotificationService::class)->dispatch($reopened->fresh(), AlertRule::EVENT_FINDINGS_REOPENED);

    expect(findingNotificationCountFor($assignee, AlertRule::EVENT_FINDINGS_REOPENED))->toBe(1);
});

it('sends due soon and overdue notifications once per due cycle and resets when due_at changes', function (): void {
    $now = CarbonImmutable::parse('2026-04-22T10:00:00Z');
    CarbonImmutable::setTestNow($now);

    [$owner, $tenant] = createUserWithTenant(role: 'owner');
    $workspaceId = (int) $tenant->workspace_id;
    Filament::setTenant($tenant, true);
    $this->actingAs($owner);

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

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

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

    $closedFinding = Finding::factory()->for($tenant)->closed()->create([
        'workspace_id' => $workspaceId,
        'owner_user_id' => (int) $owner->getKey(),
        'assignee_user_id' => (int) $assignee->getKey(),
        'due_at' => $now->subHours(1),
    ]);

    runEvaluateAlertsForWorkspace($workspaceId);
    runEvaluateAlertsForWorkspace($workspaceId);

    expect(findingNotificationCountFor($assignee, AlertRule::EVENT_FINDINGS_DUE_SOON))->toBe(1)
        ->and(findingNotificationCountFor($owner, AlertRule::EVENT_FINDINGS_OVERDUE))->toBe(1);

    $dueSoonNotification = $assignee->notifications()
        ->where('type', FindingEventNotification::class)
        ->get()
        ->first(fn ($notification): bool => data_get($notification->data, 'finding_event.event_type') === AlertRule::EVENT_FINDINGS_DUE_SOON);
    $overdueNotification = $owner->notifications()
        ->where('type', FindingEventNotification::class)
        ->get()
        ->first(fn ($notification): bool => data_get($notification->data, 'finding_event.event_type') === AlertRule::EVENT_FINDINGS_OVERDUE);

    expect($dueSoonNotification)->not->toBeNull()
        ->and(data_get($dueSoonNotification?->data, 'status'))->toBe('warning')
        ->and(data_get($dueSoonNotification?->data, 'icon'))->toBe(spec230ExpectedNotificationIcon('warning'))
        ->and(data_get($dueSoonNotification?->data, 'actions.0.label'))->toBe('Open finding');
    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.label'))->toBe('Open finding');

    expect($assignee->notifications()
        ->where('type', FindingEventNotification::class)
        ->get()
        ->contains(fn ($notification): bool => (int) data_get($notification->data, 'finding_event.finding_id') === (int) $closedFinding->getKey()))
        ->toBeFalse();

    $dueSoonFinding->forceFill([
        'due_at' => $now->addHours(12),
    ])->save();

    $overdueFinding->forceFill([
        'due_at' => $now->addDay()->subHour(),
    ])->save();

    runEvaluateAlertsForWorkspace($workspaceId);

    expect(findingNotificationCountFor($assignee, AlertRule::EVENT_FINDINGS_DUE_SOON))->toBe(2)
        ->and(findingNotificationCountFor($owner, AlertRule::EVENT_FINDINGS_OVERDUE))->toBe(1);

    $dueSoonFinding->forceFill([
        'due_at' => $now->addDays(5),
    ])->save();

    CarbonImmutable::setTestNow($now->addDays(2));
    $overdueFinding->forceFill([
        'due_at' => CarbonImmutable::now('UTC')->subHour(),
    ])->save();

    runEvaluateAlertsForWorkspace($workspaceId);

    expect(findingNotificationCountFor($owner, AlertRule::EVENT_FINDINGS_OVERDUE))->toBe(2);
});
