<?php

use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\ManagedEnvironment;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\Intune\RbacOnboardingService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);
beforeEach(function () {
    config()->set('tenantpilot.features.conditional_access', false);
});

function fakeTenant(): ManagedEnvironment
{
    $tenant = ManagedEnvironment::create([
        'managed_environment_id' => '00000000-0000-0000-0000-000000000000',
        'name' => 'ManagedEnvironment One',
        'app_client_id' => null,
        'app_client_secret' => null,
        'is_current' => true,
    ]);

    $connection = ProviderConnection::factory()->dedicated()->consentGranted()->create([
        'managed_environment_id' => (int) $tenant->getKey(),
        'provider' => 'microsoft',
        'is_default' => true,
        'consent_status' => 'granted',
        'entra_tenant_id' => (string) $tenant->managed_environment_id,
    ]);

    ProviderCredential::factory()->create([
        'provider_connection_id' => (int) $connection->getKey(),
        'type' => 'client_secret',
        'payload' => [
            'client_id' => 'app-client-123',
            'client_secret' => 'secret',
        ],
    ]);

    return $tenant;
}

it('creates group membership and role assignment', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => 'group-1']),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(true, []),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => [
                ['id' => 'assign-1', 'resourceScopes' => ['/'], 'roleDefinition' => ['id' => 'role-1']],
            ]]),
            ['GET', 'deviceManagement/roleAssignments/assign-1'] => new GraphResponse(true, ['id' => 'assign-1', 'members' => ['group-1'], 'resourceScopes' => ['/']]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-1']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => throw new RuntimeException("Unexpected Graph request: {$method} {$path}"),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('ok');
    expect($result['group_id'])->toBe('group-1');
    expect($result['role_assignment_id'])->toBe('assign-1');
    $tenant->refresh();
    expect($tenant->rbac_group_id)->toBe('group-1');
    expect($tenant->rbac_role_assignment_id)->toBe('assign-1');
    expect($tenant->rbac_role_definition_id)->toBe('role-1');
    expect($tenant->rbac_role_display_name)->toBe('Policy and Profile Manager');
});

it('is idempotent when group membership already exists', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(false, [], 400, [['message' => 'One or more added object references already exist']]),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => [
                ['id' => 'assign-1', 'resourceScopes' => ['/'], 'roleDefinition' => ['id' => 'role-1']],
            ]]),
            ['GET', 'deviceManagement/roleAssignments/assign-1'] => new GraphResponse(true, ['id' => 'assign-1', 'members' => ['group-1'], 'resourceScopes' => ['/']]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-1']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => new GraphResponse(true, ['value' => []]),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'existing',
        'existing_group_id' => 'group-1',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('ok');
    expect($result['group_id'])->toBe('group-1');
});

it('requires a role definition id', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);
    $graph->shouldReceive('request')->never();
    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('error');
    expect(strtolower($result['message']))->toContain('roledefinitionid');
});

it('is idempotent when membership add returns a request-exception style message', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $message = 'HTTP request returned status code 400: {"error":{"code":"Request_BadRequest","message":"One or more added object references already exist for the following modified properties: \'members\'."}}';

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) use ($message) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(false, [], 400, [$message]),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => [
                ['id' => 'assign-1', 'resourceScopes' => ['/'], 'roleDefinition' => ['id' => 'role-1']],
            ]]),
            ['GET', 'deviceManagement/roleAssignments/assign-1'] => new GraphResponse(true, ['id' => 'assign-1', 'members' => ['group-1'], 'resourceScopes' => ['/']]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-1']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => new GraphResponse(true, ['value' => []]),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'existing',
        'existing_group_id' => 'group-1',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('ok');
    expect($result['group_id'])->toBe('group-1');
});

it('is idempotent when group membership reference already exists', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $groupId = 'group-1';
    $servicePrincipalId = 'sp-1';

    $error = [
        'error' => [
            'code' => 'Request_BadRequest',
            'message' => 'One or more added object references already exist for the following modified properties: \'members\'.',
        ],
    ];

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) use ($groupId, $servicePrincipalId, $error) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => $servicePrincipalId]]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => $groupId]),
            ['POST', "groups/{$groupId}/members/\$ref"] => new GraphResponse(false, $error, 400),
            ['GET', 'deviceManagement/roleDefinitions'] => new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-1']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'identity/conditionalAccess/policies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => throw new RuntimeException("Unexpected Graph request: {$method} {$path}"),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('ok');
    expect($result['group_id'])->toBe('group-1');
    expect($result['role_assignment_id'])->toBe('assign-1');
    $tenant->refresh();
    expect($tenant->rbac_group_id)->toBe('group-1');
    expect($tenant->rbac_role_assignment_id)->toBe('assign-1');
});

it('surfaces membership errors with step, path, and status context', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturn(
        new GraphResponse(true, ['value' => [['id' => 'sp-1']]]), // service principal
        new GraphResponse(true, ['value' => []]), // existing group lookup
        new GraphResponse(true, ['id' => 'group-1']), // create group
        new GraphResponse(false, ['error' => ['code' => 'Request_BadRequest', 'message' => 'Different failure']], 400), // add member fails
    );

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('error');
    expect($result['message'])->toContain('step=ensureGroupMembership');
    expect($result['message'])->toContain('path=/groups/group-1/members/$ref');
    expect($result['message'])->toContain('status=400');
});

it('continues when role assignment member fetch fails and creates new assignment', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => 'group-1']),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(true, []),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => [
                ['id' => 'assign-1', 'resourceScopes' => ['/'], 'roleDefinition' => ['id' => 'role-1']],
            ]]),
            ['GET', 'deviceManagement/roleAssignments/assign-1'] => new GraphResponse(false, [
                'error' => ['code' => 'BadRequest', 'message' => 'Unsupported $expand for members'],
            ], 400),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-2']),
            default => new GraphResponse(true, ['value' => []]),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('ok');
    expect($result['role_assignment_id'])->toBe('assign-2');
});

it('surfaces role assignment create errors with status and message', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => 'group-1']),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(true, []),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(false, [
                'error' => ['code' => 'BadRequest', 'message' => 'Invalid members@odata.bind'],
            ], 400),
            default => new GraphResponse(true, ['value' => []]),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('error');
    expect($result['message'])->toContain('step=createRoleAssignment');
    expect($result['message'])->toContain('status=400');
    expect($result['message'])->toContain('Invalid members@odata.bind');
});

it('handles unsupported AAD account API gracefully with manual setup instructions', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => 'group-1', 'displayName' => 'TenantPilot-Intune-RBAC']),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(true, []),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(false, [
                'error' => ['code' => 'BadRequest', 'message' => 'This API is not supported for AAD accounts (no addressUrl for Microsoft.Intune.Rbac,False).'],
            ], 400, [], [], ['request_id' => 'req-123', 'client_request_id' => 'client-456']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => new GraphResponse(true, ['value' => []]),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    // Should be partial success, not complete failure
    expect($result['status'])->toBe('manual_assignment_required');
    expect($result['message'])->toContain('Intune RBAC API does not support automated role assignments');
    expect($result['message'])->toContain('partially complete');
    expect($result['message'])->toContain('TenantPilot-Intune-RBAC');
    expect($result['message'])->toContain('group-1');
    expect($result['message'])->toContain('request_id=req-123');
    expect($result['message'])->toContain('client_request_id=client-456');
    expect($result['group_id'])->toBe('group-1');
    expect($result['role_assignment_id'])->toBeNull();
    expect($result['warnings'])->toContain('manual_role_assignment_required');
    expect($result['steps'])->toContain('role_assignment_manual_required');
});

it('lists role assignments without unsupported fields or expands', function () {
    $tenant = fakeTenant();
    $checked = false;

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) use (&$checked) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => 'group-1']),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(true, []),
            ['GET', 'deviceManagement/roleAssignments'] => tap(new GraphResponse(true, ['value' => [[
                'id' => 'assign-1',
                'members' => ['group-1'],
                'resourceScopes' => ['/'],
                'roleDefinition' => ['id' => 'role-1', 'displayName' => 'Policy and Profile Manager'],
            ]]]), function () use ($options, &$checked) {
                expect($options['query']['$select'] ?? null)->toBe('id,displayName,resourceScopes,members');
                expect($options['query']['$expand'] ?? null)->toBe('roleDefinition($select=id,displayName)');
                $checked = true;
            }),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-1']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => throw new RuntimeException("Unexpected Graph request: {$method} {$path}"),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($checked)->toBeTrue();
    expect($result['status'])->toBe('ok');
    expect($result['role_assignment_id'])->toBe('assign-1');
});

it('falls back when role assignment members are missing without crashing', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->andReturnUsing(function (string $method, string $path, array $options = []) {
        $path = ltrim($path, '/');

        return match ([$method, $path]) {
            ['GET', 'servicePrincipals'] => new GraphResponse(true, ['value' => [['id' => 'sp-1']]]),
            ['GET', 'groups'] => new GraphResponse(true, ['value' => []]),
            ['POST', 'groups'] => new GraphResponse(true, ['id' => 'group-1']),
            ['POST', 'groups/group-1/members/$ref'] => new GraphResponse(true, []),
            ['GET', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['value' => [[
                'id' => 'assign-1',
                'resourceScopes' => ['/'],
                'roleDefinition' => ['id' => 'role-1'],
            ]]]),
            ['GET', 'deviceManagement/roleAssignments/assign-1'] => new GraphResponse(false, [], 400, [
                ['error' => ['code' => 'BadRequest', 'message' => 'expand not allowed']],
            ]),
            ['POST', 'deviceManagement/roleAssignments'] => new GraphResponse(true, ['id' => 'assign-2']),
            ['GET', 'deviceManagement/deviceConfigurations?$top=1'] => new GraphResponse(true, ['value' => []]),
            ['GET', 'deviceManagement/deviceCompliancePolicies?$top=1'] => new GraphResponse(true, ['value' => []]),
            default => throw new RuntimeException("Unexpected Graph request: {$method} {$path}"),
        };
    });

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('ok');
    expect($result['role_assignment_id'])->toBe('assign-2');
});

it('fails when service principal is missing', function () {
    $tenant = fakeTenant();

    $graph = \Mockery::mock(GraphClientInterface::class);

    $graph->shouldReceive('request')->once()->andReturn(
        new GraphResponse(true, ['value' => []])
    );

    app()->instance(GraphClientInterface::class, $graph);

    $service = app(RbacOnboardingService::class);

    $result = $service->run($tenant, [
        'group_mode' => 'create',
        'role_definition_id' => 'role-1',
        'role_display_name' => 'Policy and Profile Manager',
        'scope' => 'all_devices',
    ], null, 'access-token');

    expect($result['status'])->toBe('error');
    expect(strtolower($result['message']))->toContain('service principal');
});
