<?php

declare(strict_types=1);

use App\Jobs\ScanEntraAdminRolesJob;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\StoredReport;
use App\Services\EntraAdminRoles\EntraAdminRolesFindingGenerator;
use App\Services\EntraAdminRoles\EntraAdminRolesReportService;
use App\Services\EntraAdminRoles\HighPrivilegeRoleCatalog;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function buildScanReportService(GraphClientInterface $graphClient): EntraAdminRolesReportService
{
    return new EntraAdminRolesReportService(
        $graphClient,
        new HighPrivilegeRoleCatalog,
        app(MicrosoftGraphOptionsResolver::class),
        app(\App\Support\ProductTelemetry\ProductTelemetryRecorder::class),
    );
}

function scanJobRoleDefs(): array
{
    return [
        [
            'id' => 'def-ga',
            'displayName' => 'Global Administrator',
            'templateId' => '62e90394-69f5-4237-9190-012177145e10',
            'isBuiltIn' => true,
        ],
    ];
}

function scanJobAssignments(): array
{
    return [
        [
            'id' => 'assign-1',
            'roleDefinitionId' => 'def-ga',
            'principalId' => 'user-aaa',
            'directoryScopeId' => '/',
            'principal' => [
                '@odata.type' => '#microsoft.graph.user',
                'displayName' => 'Alice Admin',
            ],
        ],
    ];
}

function scanJobGraphMock(bool $failDefinitions = false, bool $failAssignments = false): GraphClientInterface
{
    return new class(scanJobRoleDefs(), scanJobAssignments(), $failDefinitions, $failAssignments) implements GraphClientInterface
    {
        public function __construct(
            private readonly array $roleDefs,
            private readonly array $assignments,
            private readonly bool $failDefs,
            private readonly bool $failAssigns,
        ) {}

        public function listPolicies(string $policyType, array $options = []): GraphResponse
        {
            return match ($policyType) {
                'entraRoleDefinitions' => new GraphResponse(
                    success: ! $this->failDefs,
                    data: $this->failDefs ? [] : $this->roleDefs,
                    status: $this->failDefs ? 403 : 200,
                    errors: $this->failDefs ? ['Forbidden'] : [],
                ),
                'entraRoleAssignments' => new GraphResponse(
                    success: ! $this->failAssigns,
                    data: $this->failAssigns ? [] : $this->assignments,
                    status: $this->failAssigns ? 403 : 200,
                    errors: $this->failAssigns ? ['Forbidden'] : [],
                ),
                default => new GraphResponse(success: false, status: 404),
            };
        }

        public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
        {
            return new GraphResponse(success: false, status: 501);
        }

        public function getOrganization(array $options = []): GraphResponse
        {
            return new GraphResponse(success: false, status: 501);
        }

        public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
        {
            return new GraphResponse(success: false, status: 501);
        }

        public function getServicePrincipalPermissions(array $options = []): GraphResponse
        {
            return new GraphResponse(success: false, status: 501);
        }

        public function request(string $method, string $path, array $options = []): GraphResponse
        {
            return new GraphResponse(success: false, status: 501);
        }
    };
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

it('successful run creates OperationRun and records success with counts', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $mock = scanJobGraphMock();

    $job = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );

    $job->handle(
        buildScanReportService($mock),
        new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog),
        app(\App\Services\OperationRunService::class),
    );

    $run = OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->first();

    expect($run)->not->toBeNull()
        ->and($run->status)->toBe(OperationRunStatus::Completed->value)
        ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
        ->and($run->summary_counts)->toBeArray()
        ->and($run->summary_counts['report_created'])->toBe(1)
        ->and($run->summary_counts['findings_created'])->toBeGreaterThanOrEqual(1);

    // Verify report and findings also exist
    $report = StoredReport::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
        ->first();

    expect($report)->not->toBeNull();

    $findingsCount = Finding::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('finding_type', Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES)
        ->count();

    expect($findingsCount)->toBeGreaterThanOrEqual(1);
});

it('skips tenant without active provider connection', function (): void {
    [$user, $tenant] = createUserWithTenant(ensureDefaultMicrosoftProviderConnection: false);
    // Deliberately NOT creating a provider connection

    $job = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );

    $job->handle(
        buildScanReportService(scanJobGraphMock()),
        new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog),
        app(\App\Services\OperationRunService::class),
    );

    $runCount = OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->count();

    expect($runCount)->toBe(0);

    $reportCount = StoredReport::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
        ->count();

    expect($reportCount)->toBe(0);
});

it('skips tenant when the default microsoft provider connection is disabled', function (): void {
    [$user, $tenant] = createUserWithTenant();

    $connection = ensureDefaultProviderConnection($tenant);
    $connection->forceFill([
        'is_enabled' => false,
    ])->save();

    $job = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );

    $job->handle(
        buildScanReportService(scanJobGraphMock()),
        new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog),
        app(\App\Services\OperationRunService::class),
    );

    expect(OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->count())->toBe(0);
});

it('skips tenant when the default microsoft provider connection lacks granted consent', function (): void {
    [$user, $tenant] = createUserWithTenant();

    $connection = ensureDefaultProviderConnection($tenant);
    $connection->forceFill([
        'consent_status' => \App\Support\Providers\ProviderConsentStatus::Required->value,
    ])->save();

    $job = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );

    $job->handle(
        buildScanReportService(scanJobGraphMock()),
        new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog),
        app(\App\Services\OperationRunService::class),
    );

    expect(OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->count())->toBe(0);
});

it('Graph failure marks OperationRun as failed and re-throws', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $mock = scanJobGraphMock(failDefinitions: true);

    $job = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );

    $thrown = false;

    try {
        $job->handle(
            buildScanReportService($mock),
            new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog),
            app(\App\Services\OperationRunService::class),
        );
    } catch (RuntimeException) {
        $thrown = true;
    }

    expect($thrown)->toBeTrue();

    $run = OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->first();

    expect($run)->not->toBeNull()
        ->and($run->status)->toBe(OperationRunStatus::Completed->value)
        ->and($run->outcome)->toBe(OperationRunOutcome::Failed->value)
        ->and($run->failure_summary)->toBeArray()
        ->and($run->failure_summary[0]['code'])->toBe('entra.admin_roles.scan.failed');
});

it('finding generator runs even when report is deduped', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $mock = scanJobGraphMock();

    $reportService = buildScanReportService($mock);
    $findingGenerator = new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog);
    $runService = app(\App\Services\OperationRunService::class);

    // Run 1: creates report
    $job1 = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );
    $job1->handle($reportService, $findingGenerator, $runService);

    $reportCount = StoredReport::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
        ->count();
    expect($reportCount)->toBe(1);

    // Mark existing OperationRun as completed so a new one can be created
    OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->update(['status' => OperationRunStatus::Completed->value]);

    // Run 2: report deduped, but findings still processed
    $job2 = new ScanEntraAdminRolesJob(
        tenantId: (int) $tenant->getKey(),
        workspaceId: (int) $tenant->workspace_id,
    );
    $job2->handle($reportService, $findingGenerator, $runService);

    // Still only 1 report (deduped)
    $reportCount2 = StoredReport::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
        ->count();
    expect($reportCount2)->toBe(1);

    // But the run completed successfully with deduped count
    $latestRun = OperationRun::query()
        ->where('managed_environment_id', $tenant->getKey())
        ->where('type', 'entra.admin_roles.scan')
        ->where('outcome', OperationRunOutcome::Succeeded->value)
        ->latest()
        ->first();

    expect($latestRun)->not->toBeNull()
        ->and($latestRun->summary_counts['report_deduped'])->toBe(1);
});
