<?php

declare(strict_types=1);

use App\Models\AlertRule;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Support\Workspaces\WorkspaceContext;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

/**
 * @return array<int, array<string, mixed>>
 */
function invokeSlaDueEvents(int $workspaceId, CarbonImmutable $windowStart): array
{
    $job = new \App\Jobs\Alerts\EvaluateAlertsJob($workspaceId);
    $reflection = new ReflectionMethod($job, 'slaDueEvents');

    /** @var array<int, array<string, mixed>> $events */
    $events = $reflection->invoke($job, $workspaceId, $windowStart);

    return $events;
}

it('produces one sla due event per tenant and summarizes current overdue open findings', function (): void {
    $now = CarbonImmutable::parse('2026-02-24T12:00:00Z');
    CarbonImmutable::setTestNow($now);

    [$user, $tenantA] = createUserWithTenant(role: 'owner');
    $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);
    $tenantB = ManagedEnvironment::factory()->create(['workspace_id' => $workspaceId]);
    $tenantC = ManagedEnvironment::factory()->create(['workspace_id' => $workspaceId]);

    $windowStart = $now->subHour();

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenantA->getKey(),
        'status' => Finding::STATUS_IN_PROGRESS,
        'severity' => Finding::SEVERITY_CRITICAL,
        'due_at' => $now->subMinutes(10),
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenantA->getKey(),
        'status' => Finding::STATUS_TRIAGED,
        'severity' => Finding::SEVERITY_HIGH,
        'due_at' => $now->subDays(1),
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenantA->getKey(),
        'status' => Finding::STATUS_NEW,
        'severity' => Finding::SEVERITY_MEDIUM,
        'due_at' => $windowStart,
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenantA->getKey(),
        'status' => Finding::STATUS_RESOLVED,
        'severity' => Finding::SEVERITY_LOW,
        'due_at' => $now->subMinutes(5),
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenantB->getKey(),
        'status' => Finding::STATUS_REOPENED,
        'severity' => Finding::SEVERITY_HIGH,
        'due_at' => $now->subDays(2),
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenantC->getKey(),
        'status' => Finding::STATUS_NEW,
        'severity' => Finding::SEVERITY_LOW,
        'due_at' => $now->subMinutes(20),
    ]);

    $events = invokeSlaDueEvents($workspaceId, $windowStart);

    expect($events)->toHaveCount(2);

    $eventsByTenant = collect($events)->keyBy(static fn (array $event): int => (int) $event['managed_environment_id']);

    expect($eventsByTenant->keys()->all())
        ->toEqualCanonicalizing([(int) $tenantA->getKey(), (int) $tenantC->getKey()]);

    $tenantAEvent = $eventsByTenant->get((int) $tenantA->getKey());

    expect($tenantAEvent)
        ->not->toBeNull()
        ->and($tenantAEvent['event_type'])->toBe(AlertRule::EVENT_SLA_DUE)
        ->and($tenantAEvent['severity'])->toBe(Finding::SEVERITY_CRITICAL)
        ->and($tenantAEvent['metadata'])->toMatchArray([
            'overdue_total' => 3,
            'overdue_by_severity' => [
                'critical' => 1,
                'high' => 1,
                'medium' => 1,
                'low' => 0,
            ],
        ])
        ->and($tenantAEvent['metadata'])->not->toHaveKey('finding_ids');

    $tenantCEvent = $eventsByTenant->get((int) $tenantC->getKey());

    expect($tenantCEvent)
        ->not->toBeNull()
        ->and($tenantCEvent['severity'])->toBe(Finding::SEVERITY_LOW)
        ->and($tenantCEvent['metadata'])->toMatchArray([
            'overdue_total' => 1,
            'overdue_by_severity' => [
                'critical' => 0,
                'high' => 0,
                'medium' => 0,
                'low' => 1,
            ],
        ]);
});

it('gates sla due events to newly overdue open findings after window start', function (): void {
    $now = CarbonImmutable::parse('2026-02-24T12:00:00Z');
    CarbonImmutable::setTestNow($now);

    [$user, $tenant] = createUserWithTenant(role: 'owner');
    $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);

    $windowStart = $now->subHour();

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenant->getKey(),
        'status' => Finding::STATUS_NEW,
        'severity' => Finding::SEVERITY_HIGH,
        'due_at' => $now->subDays(1),
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenant->getKey(),
        'status' => Finding::STATUS_NEW,
        'severity' => Finding::SEVERITY_MEDIUM,
        'due_at' => $windowStart,
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenant->getKey(),
        'status' => Finding::STATUS_CLOSED,
        'severity' => Finding::SEVERITY_CRITICAL,
        'due_at' => $now->subMinutes(5),
    ]);

    expect(invokeSlaDueEvents($workspaceId, $windowStart))->toBe([]);
});

it('uses a stable fingerprint per tenant and alert window', function (): void {
    $now = CarbonImmutable::parse('2026-02-24T12:00:00Z');
    CarbonImmutable::setTestNow($now);

    [$user, $tenant] = createUserWithTenant(role: 'owner');
    $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenant->getKey(),
        'status' => Finding::STATUS_NEW,
        'severity' => Finding::SEVERITY_HIGH,
        'due_at' => $now->subMinute(),
    ]);

    $windowA = $now->subMinutes(5);
    $windowB = $now->subMinutes(2);

    $first = invokeSlaDueEvents($workspaceId, $windowA);
    $second = invokeSlaDueEvents($workspaceId, $windowA);
    $third = invokeSlaDueEvents($workspaceId, $windowB);

    expect($first)->toHaveCount(1)
        ->and($second)->toHaveCount(1)
        ->and($third)->toHaveCount(1)
        ->and($first[0]['fingerprint_key'])->toBe($second[0]['fingerprint_key'])
        ->and($first[0]['fingerprint_key'])->not->toBe($third[0]['fingerprint_key']);
});

it('keeps aggregate sla due alerts separate from finding-level due soon reminders', function (): void {
    $now = CarbonImmutable::parse('2026-04-22T12:00:00Z');
    CarbonImmutable::setTestNow($now);

    [$user, $tenant] = createUserWithTenant(role: 'owner');
    $workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenant->getKey(),
        'status' => Finding::STATUS_TRIAGED,
        'severity' => Finding::SEVERITY_HIGH,
        'due_at' => $now->subHour(),
    ]);

    Finding::factory()->create([
        'workspace_id' => $workspaceId,
        'managed_environment_id' => $tenant->getKey(),
        'status' => Finding::STATUS_IN_PROGRESS,
        'severity' => Finding::SEVERITY_CRITICAL,
        'due_at' => $now->addHours(6),
    ]);

    $events = invokeSlaDueEvents($workspaceId, $now->subDay());

    expect($events)->toHaveCount(1)
        ->and($events[0]['event_type'])->toBe(AlertRule::EVENT_SLA_DUE)
        ->and($events[0]['metadata'])->toMatchArray([
            'overdue_total' => 1,
            'overdue_by_severity' => [
                'critical' => 0,
                'high' => 1,
                'medium' => 0,
                'low' => 0,
            ],
        ]);
});
