<?php

declare(strict_types=1);

use App\Models\Finding;
use App\Models\EvidenceSnapshot;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\StoredReport;
use App\Models\User;
use App\Models\Workspace;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\GovernanceDecisions\GovernanceDecisionRegisterBuilder;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('builds open and recently closed decision rows from current exception truth', function (): void {
    $workspace = Workspace::factory()->create();
    $owner = User::factory()->create(['name' => 'Decision Owner']);
    $approver = User::factory()->create(['name' => 'Decision Approver']);

    $visibleTenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'name' => 'Visible ManagedEnvironment',
        'external_id' => 'visible-tenant',
    ]);
    $hiddenTenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'name' => 'Hidden ManagedEnvironment',
        'external_id' => 'hidden-tenant',
    ]);

    $pendingApproval = makeFindingExceptionWithCurrentDecision(
        tenant: $visibleTenant,
        owner: $owner,
        actor: $owner,
        status: FindingException::STATUS_PENDING,
        validityState: FindingException::VALIDITY_MISSING_SUPPORT,
        decisionType: FindingExceptionDecision::TYPE_REQUESTED,
        decisionReason: 'Pending workspace approval',
        exceptionAttributes: [
            'requested_at' => now()->subDays(2),
            'review_due_at' => now()->addDay(),
        ],
        decisionAttributes: [
            'decided_at' => now()->subDays(2),
        ],
    );

    $followUpNeeded = makeFindingExceptionWithCurrentDecision(
        tenant: $visibleTenant,
        owner: $owner,
        actor: $approver,
        status: FindingException::STATUS_EXPIRING,
        validityState: FindingException::VALIDITY_EXPIRING,
        decisionType: FindingExceptionDecision::TYPE_APPROVED,
        decisionReason: 'Approved until remediation completes',
        exceptionAttributes: [
            'approved_by_user_id' => (int) $approver->getKey(),
            'approved_at' => now()->subDays(5),
            'effective_from' => now()->subDays(5),
            'expires_at' => now()->addDays(2),
            'review_due_at' => now()->addDay(),
        ],
        decisionAttributes: [
            'decided_at' => now()->subDays(5),
        ],
    );

    $recentlyRejected = makeFindingExceptionWithCurrentDecision(
        tenant: $visibleTenant,
        owner: $owner,
        actor: $approver,
        status: FindingException::STATUS_REJECTED,
        validityState: FindingException::VALIDITY_REJECTED,
        decisionType: FindingExceptionDecision::TYPE_REJECTED,
        decisionReason: 'Evidence bundle was incomplete',
        exceptionAttributes: [
            'approved_by_user_id' => (int) $approver->getKey(),
            'rejected_at' => now()->subDays(3),
            'review_due_at' => now()->subDays(4),
        ],
        decisionAttributes: [
            'decided_at' => now()->subDays(3),
        ],
    );

    makeFindingExceptionWithCurrentDecision(
        tenant: $visibleTenant,
        owner: $owner,
        actor: $approver,
        status: FindingException::STATUS_REVOKED,
        validityState: FindingException::VALIDITY_REVOKED,
        decisionType: FindingExceptionDecision::TYPE_REVOKED,
        decisionReason: 'Closed long ago',
        exceptionAttributes: [
            'approved_by_user_id' => (int) $approver->getKey(),
            'revoked_at' => now()->subDays(45),
            'review_due_at' => now()->subDays(46),
        ],
        decisionAttributes: [
            'decided_at' => now()->subDays(45),
        ],
    );

    makeFindingExceptionWithCurrentDecision(
        tenant: $hiddenTenant,
        owner: $owner,
        actor: $owner,
        status: FindingException::STATUS_PENDING,
        validityState: FindingException::VALIDITY_MISSING_SUPPORT,
        decisionType: FindingExceptionDecision::TYPE_REQUESTED,
        decisionReason: 'Hidden tenant request',
        exceptionAttributes: [
            'requested_at' => now()->subDay(),
            'review_due_at' => now()->addDays(2),
        ],
        decisionAttributes: [
            'decided_at' => now()->subDay(),
        ],
    );

    $builder = app(GovernanceDecisionRegisterBuilder::class);

    $openPayload = $builder->build(
        workspace: $workspace,
        visibleTenants: [$visibleTenant],
        registerState: 'open',
    );

    $openRows = collect($openPayload['rows'])->keyBy('exception_id');

    expect($openPayload['counts'])->toMatchArray([
        'open' => 2,
        'recently_closed' => 1,
    ])
        ->and($openRows->keys()->all())->toBe([
            (int) $pendingApproval->getKey(),
            (int) $followUpNeeded->getKey(),
        ])
        ->and($openRows[(int) $pendingApproval->getKey()]['tenant_name'])->toBe('Visible ManagedEnvironment')
        ->and($openRows[(int) $pendingApproval->getKey()]['owner_name'])->toBe('Decision Owner')
        ->and($openRows[(int) $pendingApproval->getKey()]['next_action_label'])->toBe('Review approval')
        ->and($openRows[(int) $followUpNeeded->getKey()]['next_action_label'])->toBe('Review follow-up');

    $recentlyClosedPayload = $builder->build(
        workspace: $workspace,
        visibleTenants: [$visibleTenant],
        registerState: 'recently_closed',
    );

    expect($recentlyClosedPayload['counts'])->toMatchArray([
        'open' => 2,
        'recently_closed' => 1,
    ])
        ->and(collect($recentlyClosedPayload['rows'])->pluck('exception_id')->all())->toBe([
            (int) $recentlyRejected->getKey(),
        ])
        ->and($recentlyClosedPayload['rows'][0]['closure_reason'])->toBe('Evidence bundle was incomplete')
        ->and($recentlyClosedPayload['rows'][0]['status'])->toBe(FindingException::STATUS_REJECTED);
});

it('keeps missing owner visible instead of omitting follow-up-needed rows', function (): void {
    $workspace = Workspace::factory()->create();
    $requester = User::factory()->create();
    $tenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);

    $unownedException = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: null,
        actor: $requester,
        status: FindingException::STATUS_EXPIRED,
        validityState: FindingException::VALIDITY_EXPIRED,
        decisionType: FindingExceptionDecision::TYPE_APPROVED,
        decisionReason: 'Expired and needs a fresh decision',
        exceptionAttributes: [
            'requested_by_user_id' => (int) $requester->getKey(),
            'owner_user_id' => null,
            'approved_by_user_id' => (int) $requester->getKey(),
            'approved_at' => now()->subDays(20),
            'effective_from' => now()->subDays(20),
            'expires_at' => now()->subDay(),
            'review_due_at' => now()->subDays(2),
        ],
        decisionAttributes: [
            'decided_at' => now()->subDays(20),
        ],
    );

    $payload = app(GovernanceDecisionRegisterBuilder::class)->build(
        workspace: $workspace,
        visibleTenants: [$tenant],
        registerState: 'open',
    );

    expect($payload['rows'])->toHaveCount(1)
        ->and($payload['rows'][0]['exception_id'])->toBe((int) $unownedException->getKey())
        ->and($payload['rows'][0]['owner_name'])->toBeNull()
        ->and($payload['rows'][0]['next_action_label'])->toBe('Review follow-up');
});

it('exposes missing proof and aggregate detail proof states truthfully', function (): void {
    $workspace = Workspace::factory()->create();
    $actor = User::factory()->create();
    $tenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);

    $withoutProof = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_PENDING,
        validityState: FindingException::VALIDITY_MISSING_SUPPORT,
        decisionType: FindingExceptionDecision::TYPE_REQUESTED,
        decisionReason: 'No proof linked',
    );

    $withMultipleProof = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_ACTIVE,
        validityState: FindingException::VALIDITY_VALID,
        decisionType: FindingExceptionDecision::TYPE_APPROVED,
        decisionReason: 'Multiple proof linked',
        exceptionAttributes: [
            'approved_by_user_id' => (int) $actor->getKey(),
            'approved_at' => now()->subDays(3),
            'effective_from' => now()->subDays(3),
            'evidence_summary' => ['reference_count' => 2],
            'review_due_at' => now()->addDays(2),
        ],
    );

    $withMultipleProof->evidenceReferences()->createMany([
        [
            'workspace_id' => (int) $workspace->getKey(),
            'managed_environment_id' => (int) $tenant->getKey(),
            'source_type' => 'evidence_snapshot',
            'source_id' => 'snapshot-001',
            'label' => 'Snapshot summary',
            'summary_payload' => [],
        ],
        [
            'workspace_id' => (int) $workspace->getKey(),
            'managed_environment_id' => (int) $tenant->getKey(),
            'source_type' => 'review_pack',
            'source_id' => 'review-pack-001',
            'label' => 'Review pack summary',
            'summary_payload' => [],
        ],
    ]);

    $payload = app(GovernanceDecisionRegisterBuilder::class)->build(
        workspace: $workspace,
        visibleTenants: [$tenant],
        registerState: 'open',
    );

    $rows = collect($payload['rows'])->keyBy('exception_id');

    expect($rows[(int) $withoutProof->getKey()])
        ->toMatchArray([
            'proof_count' => 0,
            'proof_state' => 'not_linked',
            'proof_label' => 'No linked proof',
            'proof_url' => null,
            'proof_url_label' => null,
            'operation_run_state' => 'not_linked',
            'operation_run_url' => null,
            'operation_run_label' => 'No operation linked',
        ])
        ->and($rows[(int) $withMultipleProof->getKey()]['proof_count'])->toBe(2)
        ->and($rows[(int) $withMultipleProof->getKey()]['proof_state'])->toBe('linked_detail_section')
        ->and($rows[(int) $withMultipleProof->getKey()]['proof_label'])->toBe('2 proof items')
        ->and($rows[(int) $withMultipleProof->getKey()]['proof_url'])->toContain('/admin/workspaces/')
        ->and($rows[(int) $withMultipleProof->getKey()]['proof_url'])->not->toContain('/admin/t')
        ->and($rows[(int) $withMultipleProof->getKey()]['proof_url_label'])->toBe('View proof');
});

it('links a single same-scope evidence snapshot and its operation when authorized', function (): void {
    $workspace = Workspace::factory()->create();
    $actor = User::factory()->create();
    $tenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);
    createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
    $this->actingAs($actor);

    $run = OperationRun::factory()->forTenant($tenant)->create([
        'type' => 'tenant.evidence.snapshot.generate',
        'status' => OperationRunStatus::Completed->value,
        'outcome' => OperationRunOutcome::Succeeded->value,
        'completed_at' => now(),
    ]);

    $snapshot = EvidenceSnapshot::query()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'operation_run_id' => (int) $run->getKey(),
        'status' => EvidenceSnapshotStatus::Active->value,
        'completeness_state' => EvidenceCompletenessState::Complete->value,
        'summary' => ['finding_count' => 1],
        'generated_at' => now(),
    ]);

    $exception = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_ACTIVE,
        validityState: FindingException::VALIDITY_VALID,
        decisionType: FindingExceptionDecision::TYPE_APPROVED,
        decisionReason: 'Evidence snapshot proof',
        exceptionAttributes: [
            'approved_by_user_id' => (int) $actor->getKey(),
            'approved_at' => now()->subDay(),
            'effective_from' => now()->subDay(),
            'evidence_summary' => ['reference_count' => 1],
        ],
    );

    $exception->evidenceReferences()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'source_type' => 'evidence_snapshot',
        'source_id' => (string) $snapshot->getKey(),
        'label' => 'Evidence snapshot',
        'summary_payload' => [],
    ]);

    $payload = app(GovernanceDecisionRegisterBuilder::class)->build(
        workspace: $workspace,
        visibleTenants: [$tenant],
        registerState: 'open',
    );

    $row = collect($payload['rows'])->firstWhere('exception_id', (int) $exception->getKey());

    expect($row)->toMatchArray([
        'proof_count' => 1,
        'proof_state' => 'linked_evidence',
        'proof_label' => '1 proof item',
        'proof_url_label' => 'View evidence',
        'operation_run_state' => 'linked_run',
        'operation_run_label' => 'View operation',
    ])
        ->and($row['proof_url'])->toContain('/admin/workspaces/')
        ->and($row['proof_url'])->toContain('/evidence/')
        ->and($row['proof_url'])->not->toContain('/admin/t')
        ->and($row['operation_run_url'])->toBe(OperationRunLinks::tenantlessView($run))
        ->and($row['operation_run_url'])->not->toContain('/admin/t');
});

it('links a same-scope source finding operation when no evidence operation exists', function (): void {
    $workspace = Workspace::factory()->create();
    $actor = User::factory()->create();
    $tenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);
    createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
    $this->actingAs($actor);

    $run = OperationRun::factory()->forTenant($tenant)->create([
        'type' => 'tenant.finding.generate',
        'status' => OperationRunStatus::Completed->value,
        'outcome' => OperationRunOutcome::Succeeded->value,
        'completed_at' => now(),
    ]);

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $workspace->getKey(),
        'current_operation_run_id' => (int) $run->getKey(),
    ]);

    $exception = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_PENDING,
        validityState: FindingException::VALIDITY_MISSING_SUPPORT,
        decisionType: FindingExceptionDecision::TYPE_REQUESTED,
        decisionReason: 'Finding source operation',
        exceptionAttributes: [
            'finding_id' => (int) $finding->getKey(),
        ],
    );

    $payload = app(GovernanceDecisionRegisterBuilder::class)->build(
        workspace: $workspace,
        visibleTenants: [$tenant],
        registerState: 'open',
    );

    $row = collect($payload['rows'])->firstWhere('exception_id', (int) $exception->getKey());

    expect($row)->toMatchArray([
        'proof_count' => 0,
        'proof_state' => 'not_linked',
        'proof_label' => 'No linked proof',
        'operation_run_state' => 'linked_run',
        'operation_run_label' => 'View operation',
        'operation_run_url' => OperationRunLinks::tenantlessView($run),
    ])
        ->and($row['operation_run_url'])->not->toContain('/admin/t');
});

it('links a single same-scope stored report when authorized', function (): void {
    $workspace = Workspace::factory()->create();
    $actor = User::factory()->create();
    $tenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);
    createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
    $this->actingAs($actor);

    $report = StoredReport::factory()->permissionPosture()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'fingerprint' => hash('sha256', 'decision-register-report'),
    ]);

    $exception = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_PENDING,
        validityState: FindingException::VALIDITY_MISSING_SUPPORT,
        decisionType: FindingExceptionDecision::TYPE_REQUESTED,
        decisionReason: 'Stored report proof',
        exceptionAttributes: [
            'evidence_summary' => ['reference_count' => 1],
        ],
    );

    $exception->evidenceReferences()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'source_type' => 'stored_report',
        'source_id' => (string) $report->getKey(),
        'label' => 'Permission posture report',
        'summary_payload' => [],
    ]);

    $payload = app(GovernanceDecisionRegisterBuilder::class)->build(
        workspace: $workspace,
        visibleTenants: [$tenant],
        registerState: 'open',
    );

    $row = collect($payload['rows'])->firstWhere('exception_id', (int) $exception->getKey());

    expect($row)->toMatchArray([
        'proof_count' => 1,
        'proof_state' => 'linked_report',
        'proof_label' => '1 proof item',
        'proof_url_label' => 'View report',
        'operation_run_state' => 'not_linked',
        'operation_run_url' => null,
    ])
        ->and($row['proof_url'])->toContain('/stored-reports/')
        ->and($row['proof_url'])->not->toContain('/admin/t');
});

it('does not invent direct artifact or operation links from loose identifiers or cross-scope runs', function (): void {
    $workspace = Workspace::factory()->create();
    $actor = User::factory()->create();
    $tenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);
    createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
    $otherTenant = ManagedEnvironment::factory()->create([
        'workspace_id' => (int) $workspace->getKey(),
    ]);
    createUserWithTenant(tenant: $otherTenant, user: $actor, role: 'owner', workspaceRole: 'owner');
    $this->actingAs($actor);

    $otherRun = OperationRun::factory()->forTenant($otherTenant)->create([
        'type' => 'tenant.evidence.snapshot.generate',
        'status' => OperationRunStatus::Completed->value,
        'outcome' => OperationRunOutcome::Succeeded->value,
    ]);

    $sameScopeSnapshotWithOtherRun = EvidenceSnapshot::query()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'operation_run_id' => (int) $otherRun->getKey(),
        'status' => EvidenceSnapshotStatus::Active->value,
        'completeness_state' => EvidenceCompletenessState::Complete->value,
        'summary' => ['finding_count' => 1],
        'generated_at' => now(),
    ]);

    $looseIdentifier = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_PENDING,
        validityState: FindingException::VALIDITY_MISSING_SUPPORT,
        decisionType: FindingExceptionDecision::TYPE_REQUESTED,
        decisionReason: 'Loose proof identifier',
        exceptionAttributes: [
            'evidence_summary' => ['reference_count' => 1],
        ],
    );

    $looseIdentifier->evidenceReferences()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'source_type' => 'evidence_snapshot',
        'source_id' => 'snapshot-001',
        'label' => 'Loose evidence snapshot',
        'summary_payload' => [],
    ]);

    $crossScopeRun = makeFindingExceptionWithCurrentDecision(
        tenant: $tenant,
        owner: $actor,
        actor: $actor,
        status: FindingException::STATUS_ACTIVE,
        validityState: FindingException::VALIDITY_VALID,
        decisionType: FindingExceptionDecision::TYPE_APPROVED,
        decisionReason: 'Cross-scope operation should not link',
        exceptionAttributes: [
            'approved_by_user_id' => (int) $actor->getKey(),
            'approved_at' => now()->subDay(),
            'effective_from' => now()->subDay(),
            'evidence_summary' => ['reference_count' => 1],
        ],
    );

    $crossScopeRun->evidenceReferences()->create([
        'workspace_id' => (int) $workspace->getKey(),
        'managed_environment_id' => (int) $tenant->getKey(),
        'source_type' => 'evidence_snapshot',
        'source_id' => (string) $sameScopeSnapshotWithOtherRun->getKey(),
        'label' => 'Snapshot with cross-scope run',
        'summary_payload' => [],
    ]);

    $payload = app(GovernanceDecisionRegisterBuilder::class)->build(
        workspace: $workspace,
        visibleTenants: [$tenant],
        registerState: 'open',
    );

    $rows = collect($payload['rows'])->keyBy('exception_id');

    expect($rows[(int) $looseIdentifier->getKey()])
        ->toMatchArray([
            'proof_count' => 1,
            'proof_state' => 'linked_detail_section',
            'proof_url_label' => 'View proof',
            'operation_run_state' => 'not_linked',
            'operation_run_url' => null,
        ])
        ->and($rows[(int) $crossScopeRun->getKey()])
        ->toMatchArray([
            'proof_state' => 'linked_evidence',
            'proof_url_label' => 'View evidence',
            'operation_run_state' => 'run_not_available',
            'operation_run_url' => null,
            'operation_run_label' => 'No operation linked',
        ]);
});

/**
 * @param  array<string, mixed>  $exceptionAttributes
 * @param  array<string, mixed>  $decisionAttributes
 */
function makeFindingExceptionWithCurrentDecision(
    ManagedEnvironment $tenant,
    ?User $owner,
    User $actor,
    string $status,
    string $validityState,
    string $decisionType,
    string $decisionReason,
    array $exceptionAttributes = [],
    array $decisionAttributes = [],
): FindingException {
    $requesterId = $exceptionAttributes['requested_by_user_id'] ?? (int) $actor->getKey();

    $finding = Finding::factory()->for($tenant)->create([
        'workspace_id' => (int) $tenant->workspace_id,
    ]);

    $exception = FindingException::query()->create(array_merge([
        'workspace_id' => (int) $tenant->workspace_id,
        'managed_environment_id' => (int) $tenant->getKey(),
        'finding_id' => (int) $finding->getKey(),
        'requested_by_user_id' => $requesterId,
        'owner_user_id' => $owner?->getKey(),
        'status' => $status,
        'current_validity_state' => $validityState,
        'request_reason' => 'Decision register test setup',
        'requested_at' => now()->subDays(7),
        'review_due_at' => now()->addDays(7),
        'evidence_summary' => ['reference_count' => 0],
    ], $exceptionAttributes));

    $decision = $exception->decisions()->create(array_merge([
        'workspace_id' => (int) $tenant->workspace_id,
        'managed_environment_id' => (int) $tenant->getKey(),
        'actor_user_id' => (int) $actor->getKey(),
        'decision_type' => $decisionType,
        'reason' => $decisionReason,
        'metadata' => [],
        'decided_at' => now()->subDays(7),
    ], $decisionAttributes));

    $exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();

    return $exception->fresh(['tenant', 'owner', 'currentDecision']);
}
