<?php

declare(strict_types=1);

use App\Models\StoredReport;
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 Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

function sampleRoleDefinitions(): array
{
    return [
        [
            'id' => 'def-ga-001',
            'displayName' => 'Global Administrator',
            'templateId' => '62e90394-69f5-4237-9190-012177145e10',
            'isBuiltIn' => true,
        ],
        [
            'id' => 'def-ua-002',
            'displayName' => 'User Administrator',
            'templateId' => 'fe930be7-5e62-47db-91af-98c3a49a38b1',
            'isBuiltIn' => true,
        ],
        [
            'id' => 'def-reader-003',
            'displayName' => 'Directory Readers',
            'templateId' => '88d8e3e3-8f55-4a1e-953a-9b9898b87601',
            'isBuiltIn' => true,
        ],
    ];
}

function sampleRoleAssignments(): array
{
    return [
        [
            'id' => 'assign-1',
            'roleDefinitionId' => 'def-ga-001',
            'principalId' => 'user-aaa',
            'directoryScopeId' => '/',
            'principal' => [
                '@odata.type' => '#microsoft.graph.user',
                'displayName' => 'Alice Admin',
            ],
        ],
        [
            'id' => 'assign-2',
            'roleDefinitionId' => 'def-ua-002',
            'principalId' => 'user-bbb',
            'directoryScopeId' => '/',
            'principal' => [
                '@odata.type' => '#microsoft.graph.user',
                'displayName' => 'Bob Useradmin',
            ],
        ],
        [
            'id' => 'assign-3',
            'roleDefinitionId' => 'def-reader-003',
            'principalId' => 'group-ccc',
            'directoryScopeId' => '/',
            'principal' => [
                '@odata.type' => '#microsoft.graph.group',
                'displayName' => 'Readers Group',
            ],
        ],
    ];
}

/**
 * Build a mock GraphClientInterface that returns specific data for entra types.
 */
function buildGraphMock(
    array $roleDefinitions,
    array $roleAssignments,
    bool $failDefinitions = false,
    bool $failAssignments = false,
): GraphClientInterface {
    return new class($roleDefinitions, $roleAssignments, $failDefinitions, $failAssignments) implements GraphClientInterface
    {
        public function __construct(
            private readonly array $roleDefinitions,
            private readonly array $roleAssignments,
            private readonly bool $failDefinitions,
            private readonly bool $failAssignments,
        ) {}

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

        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);
        }
    };
}

function buildReportService(GraphClientInterface $graphMock): EntraAdminRolesReportService
{
    return new EntraAdminRolesReportService(
        graphClient: $graphMock,
        catalog: new HighPrivilegeRoleCatalog,
        graphOptionsResolver: app(MicrosoftGraphOptionsResolver::class),
        productTelemetryRecorder: app(\App\Support\ProductTelemetry\ProductTelemetryRecorder::class),
    );
}

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

it('creates a new report with correct attributes', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $service = buildReportService(buildGraphMock(sampleRoleDefinitions(), sampleRoleAssignments()));

    $result = $service->generate($tenant);

    expect($result->created)->toBeTrue()
        ->and($result->storedReportId)->toBeInt()
        ->and($result->fingerprint)->toBeString()->toHaveLength(64)
        ->and($result->payload)->toBeArray();

    $report = StoredReport::find($result->storedReportId);

    expect($report)->not->toBeNull()
        ->and($report->report_type)->toBe(StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
        ->and($report->managed_environment_id)->toBe((int) $tenant->getKey())
        ->and($report->fingerprint)->toBe($result->fingerprint)
        ->and($report->previous_fingerprint)->toBeNull();
});

it('deduplicates when fingerprint matches latest report', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $service = buildReportService(buildGraphMock(sampleRoleDefinitions(), sampleRoleAssignments()));

    $first = $service->generate($tenant);
    expect($first->created)->toBeTrue();

    $second = $service->generate($tenant);
    expect($second->created)->toBeFalse()
        ->and($second->storedReportId)->toBe($first->storedReportId)
        ->and($second->fingerprint)->toBe($first->fingerprint);

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

    expect($count)->toBe(1);
});

it('chains previous_fingerprint when data changes', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $originalAssignments = sampleRoleAssignments();
    $service1 = buildReportService(buildGraphMock(sampleRoleDefinitions(), $originalAssignments));
    $first = $service1->generate($tenant);

    // Add a new assignment to produce a different fingerprint
    $changedAssignments = array_merge($originalAssignments, [[
        'id' => 'assign-new',
        'roleDefinitionId' => 'def-ga-001',
        'principalId' => 'user-zzz',
        'directoryScopeId' => '/',
        'principal' => [
            '@odata.type' => '#microsoft.graph.user',
            'displayName' => 'Zach New',
        ],
    ]]);

    $service2 = buildReportService(buildGraphMock(sampleRoleDefinitions(), $changedAssignments));
    $second = $service2->generate($tenant);

    expect($second->created)->toBeTrue()
        ->and($second->fingerprint)->not->toBe($first->fingerprint)
        ->and(StoredReport::find($second->storedReportId)->previous_fingerprint)->toBe($first->fingerprint);
});

it('throws when role definitions fetch fails', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $service = buildReportService(buildGraphMock([], [], failDefinitions: true));

    $service->generate($tenant);
})->throws(RuntimeException::class, 'Failed to fetch Entra role definitions');

it('throws when role assignments fetch fails', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $service = buildReportService(buildGraphMock(sampleRoleDefinitions(), [], failAssignments: true));

    $service->generate($tenant);
})->throws(RuntimeException::class, 'Failed to fetch Entra role assignments');

it('produces expected payload structure', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $service = buildReportService(buildGraphMock(sampleRoleDefinitions(), sampleRoleAssignments()));
    $result = $service->generate($tenant);

    $payload = $result->payload;

    expect($payload)
        ->toHaveKeys(['provider_key', 'domain', 'measured_at', 'role_definitions', 'role_assignments', 'totals', 'high_privilege'])
        ->and($payload['provider_key'])->toBe('microsoft')
        ->and($payload['domain'])->toBe('entra.admin_roles')
        ->and($payload['role_definitions'])->toHaveCount(3)
        ->and($payload['role_assignments'])->toHaveCount(3)
        ->and($payload['totals']['roles_total'])->toBe(3)
        ->and($payload['totals']['assignments_total'])->toBe(3)
        // Only GA is in the high-privilege catalog
        ->and($payload['totals']['high_privilege_assignments'])->toBe(1)
        ->and($payload['high_privilege'])->toHaveCount(1);

    // Verify first high-privilege entry shape
    $ga = collect($payload['high_privilege'])->firstWhere('role_template_id', '62e90394-69f5-4237-9190-012177145e10');

    expect($ga)->not->toBeNull()
        ->and($ga['role_display_name'])->toBe('Global Administrator')
        ->and($ga['principal_id'])->toBe('user-aaa')
        ->and($ga['principal_type'])->toBe('user')
        ->and($ga['severity'])->toBe('critical');
});

it('computes deterministic fingerprint regardless of assignment order', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $assignments = sampleRoleAssignments();
    $reversedAssignments = array_reverse($assignments);

    $service1 = buildReportService(buildGraphMock(sampleRoleDefinitions(), $assignments));
    $result1 = $service1->generate($tenant);

    // Delete the report to allow re-creation
    StoredReport::query()->where('id', $result1->storedReportId)->delete();

    $service2 = buildReportService(buildGraphMock(sampleRoleDefinitions(), $reversedAssignments));
    $result2 = $service2->generate($tenant);

    expect($result2->fingerprint)->toBe($result1->fingerprint);
});

it('handles zero assignments gracefully', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $service = buildReportService(buildGraphMock(sampleRoleDefinitions(), []));
    $result = $service->generate($tenant);

    expect($result->created)->toBeTrue()
        ->and($result->payload['totals']['assignments_total'])->toBe(0)
        ->and($result->payload['totals']['high_privilege_assignments'])->toBe(0)
        ->and($result->payload['high_privilege'])->toBeEmpty();
});

it('resolves principal types correctly', function (): void {
    [$user, $tenant] = createUserWithTenant();
    ensureDefaultProviderConnection($tenant);

    $assignments = [
        [
            'id' => 'a1',
            'roleDefinitionId' => 'def-ga-001',
            'principalId' => 'p-user',
            'directoryScopeId' => '/',
            'principal' => ['@odata.type' => '#microsoft.graph.user', 'displayName' => 'User'],
        ],
        [
            'id' => 'a2',
            'roleDefinitionId' => 'def-ga-001',
            'principalId' => 'p-group',
            'directoryScopeId' => '/',
            'principal' => ['@odata.type' => '#microsoft.graph.group', 'displayName' => 'Group'],
        ],
        [
            'id' => 'a3',
            'roleDefinitionId' => 'def-ga-001',
            'principalId' => 'p-sp',
            'directoryScopeId' => '/',
            'principal' => ['@odata.type' => '#microsoft.graph.servicePrincipal', 'displayName' => 'SP'],
        ],
        [
            'id' => 'a4',
            'roleDefinitionId' => 'def-ga-001',
            'principalId' => 'p-unknown',
            'directoryScopeId' => '/',
            'principal' => ['displayName' => 'NoType'],
        ],
    ];

    $service = buildReportService(buildGraphMock(sampleRoleDefinitions(), $assignments));
    $result = $service->generate($tenant);

    $types = collect($result->payload['high_privilege'])->pluck('principal_type')->toArray();

    expect($types)->toBe(['user', 'group', 'servicePrincipal', 'unknown']);
});
