<?php

use App\Models\BackupItem;
use App\Models\BackupSchedule;
use App\Models\BackupSet;
use App\Models\InventoryItem;
use App\Models\Policy;
use App\Models\PolicyVersion;
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\Inventory\InventoryMetaSanitizer;
use App\Services\Inventory\InventoryMissingService;
use App\Services\Inventory\InventorySyncService;
use App\Services\OperationRunService;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;

uses(RefreshDatabase::class);

function fakeGraphClient(array $policiesByType = [], array $failedTypes = [], ?Throwable $throwable = null): GraphClientInterface
{
    return new class($policiesByType, $failedTypes, $throwable) implements GraphClientInterface
    {
        public function __construct(
            private readonly array $policiesByType,
            private readonly array $failedTypes,
            private readonly ?Throwable $throwable,
        ) {}

        public function listPolicies(string $policyType, array $options = []): GraphResponse
        {
            if ($this->throwable instanceof Throwable) {
                throw $this->throwable;
            }

            if (in_array($policyType, $this->failedTypes, true)) {
                return new GraphResponse(false, [], 403, ['error' => ['code' => 'Forbidden', 'message' => 'forbidden']], [], []);
            }

            return new GraphResponse(true, $this->policiesByType[$policyType] ?? []);
        }

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

        public function getOrganization(array $options = []): GraphResponse
        {
            return new GraphResponse(true, []);
        }

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

        public function getServicePrincipalPermissions(array $options = []): GraphResponse
        {
            return new GraphResponse(true, []);
        }

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

/**
 * Executes an inventory sync against a canonical OperationRun.
 *
 * @param  array<string, mixed>  $selection
 * @return array{opRun: \App\Models\OperationRun, result: array<string, mixed>, selection: array<string, mixed>, selection_hash: string}
 */
function executeInventorySyncNow(ManagedEnvironment $tenant, array $selection): array
{
    $service = app(InventorySyncService::class);
    $opService = app(OperationRunService::class);

    $defaultConnection = ProviderConnection::query()
        ->where('managed_environment_id', (int) $tenant->getKey())
        ->where('provider', 'microsoft')
        ->where('is_default', true)
        ->first();

    if (! $defaultConnection instanceof ProviderConnection) {
        $defaultConnection = ProviderConnection::factory()->consentGranted()->create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'workspace_id' => (int) $tenant->workspace_id,
            'provider' => 'microsoft',
            'entra_tenant_id' => $tenant->managed_environment_id,
            'is_default' => true,
            'consent_status' => 'granted',
        ]);

        ProviderCredential::factory()->create([
            'provider_connection_id' => (int) $defaultConnection->getKey(),
            'type' => 'client_secret',
            'payload' => [
                'client_id' => 'test-client-id',
                'client_secret' => 'test-client-secret',
            ],
        ]);
    }

    $computed = $service->normalizeAndHashSelection($selection);
    $context = array_merge($computed['selection'], [
        'selection_hash' => $computed['selection_hash'],
    ]);

    $opRun = $opService->ensureRunWithIdentity(
        tenant: $tenant,
        type: 'inventory.sync',
        identityInputs: [
            'selection_hash' => $computed['selection_hash'],
        ],
        context: $context,
        initiator: null,
    );

    $result = $service->executeSelection($opRun, $tenant, $context);
    $status = (string) ($result['status'] ?? 'failed');

    $outcome = match ($status) {
        'success' => OperationRunOutcome::Succeeded->value,
        'partial' => OperationRunOutcome::PartiallySucceeded->value,
        default => OperationRunOutcome::Failed->value,
    };

    $opService->updateRun(
        $opRun,
        status: OperationRunStatus::Completed->value,
        outcome: $outcome,
        summaryCounts: [
            'total' => count($computed['selection']['policy_types'] ?? []),
        ],
    );

    return [
        'opRun' => $opRun->refresh(),
        'result' => $result,
        'selection' => $computed['selection'],
        'selection_hash' => $computed['selection_hash'],
    ];
}

test('inventory sync upserts and updates last_seen fields without duplicates', function () {
    $tenant = ManagedEnvironment::factory()->create();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [
            ['id' => 'cfg-1', 'displayName' => 'Config 1', '@odata.type' => '#microsoft.graph.deviceConfiguration'],
        ],
    ]));

    $selection = [
        'policy_types' => ['deviceConfiguration'],
        'categories' => ['Configuration'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ];

    $runA = executeInventorySyncNow($tenant, $selection);
    expect($runA['result']['status'] ?? null)->toBe('success');

    $item = \App\Models\InventoryItem::query()->where('managed_environment_id', $tenant->id)->first();
    expect($item)->not->toBeNull();
    expect($item->external_id)->toBe('cfg-1');
    expect($item->last_seen_operation_run_id)->toBe($runA['opRun']->id);

    $runB = executeInventorySyncNow($tenant, $selection);

    $items = \App\Models\InventoryItem::query()->where('managed_environment_id', $tenant->id)->get();
    expect($items)->toHaveCount(1);

    $items->first()->refresh();
    expect($items->first()->last_seen_operation_run_id)->toBe($runB['opRun']->id);
});

test('inventory sync includes foundation types when include_foundations is true', function () {
    $tenant = ManagedEnvironment::factory()->create();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
        'roleScopeTag' => [
            ['id' => 'tag-1', 'displayName' => 'Scope Tag 1', '@odata.type' => '#microsoft.graph.roleScopeTag'],
        ],
        'assignmentFilter' => [
            ['id' => 'filter-1', 'displayName' => 'Filter 1', '@odata.type' => '#microsoft.graph.assignmentFilter'],
        ],
        'notificationMessageTemplate' => [
            ['id' => 'tmpl-1', 'displayName' => 'Template 1', '@odata.type' => '#microsoft.graph.notificationMessageTemplate'],
        ],
    ]));

    $run = executeInventorySyncNow($tenant, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => [],
        'include_foundations' => true,
        'include_dependencies' => false,
    ]);

    expect($run['result']['status'] ?? null)->toBe('success');

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'roleScopeTag')
        ->where('external_id', 'tag-1')
        ->where('category', 'Foundations')
        ->exists())->toBeTrue();

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'assignmentFilter')
        ->where('external_id', 'filter-1')
        ->where('category', 'Foundations')
        ->exists())->toBeTrue();

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'notificationMessageTemplate')
        ->where('external_id', 'tmpl-1')
        ->where('category', 'Foundations')
        ->exists())->toBeTrue();
});

test('inventory sync captures RBAC foundation rows with sanitized metadata and coverage status', function () {
    $tenant = ManagedEnvironment::factory()->create();

    ensureDefaultProviderConnection($tenant, 'microsoft');

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
        'intuneRoleDefinition' => [
            [
                'id' => 'role-def-1',
                'displayName' => 'Help Desk Operator',
                'description' => 'Built-in support role',
                'isBuiltIn' => true,
                'rolePermissions' => [
                    ['resourceActions' => ['managedDevices/read']],
                    ['resourceActions' => ['deviceConfigurations/read']],
                ],
            ],
        ],
        'intuneRoleAssignment' => [
            [
                'id' => 'role-assignment-1',
                'displayName' => 'Help Desk Assignment',
                'description' => 'ManagedEnvironment-wide assignment',
                'members' => ['group-1', 'group-2'],
                'scopeMembers' => ['scope-member-1'],
                'resourceScopes' => ['scope-1', 'scope-2'],
                'roleDefinition' => [
                    'id' => 'role-def-1',
                    'displayName' => 'Help Desk Operator',
                ],
            ],
        ],
    ]));

    $run = app(InventorySyncService::class)->syncNow($tenant, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => [],
        'include_foundations' => true,
        'include_dependencies' => false,
    ]);

    $definition = InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'intuneRoleDefinition')
        ->where('external_id', 'role-def-1')
        ->first();

    expect($definition)->not->toBeNull();
    expect($definition->category)->toBe('RBAC');
    expect($definition->meta_jsonb)->toMatchArray([
        'is_built_in' => true,
        'role_permission_count' => 2,
    ]);

    $assignment = InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'intuneRoleAssignment')
        ->where('external_id', 'role-assignment-1')
        ->first();

    expect($assignment)->not->toBeNull();
    expect($assignment->category)->toBe('RBAC');
    expect($assignment->meta_jsonb)->toMatchArray([
        'role_definition_id' => 'role-def-1',
        'role_definition_name' => 'Help Desk Operator',
        'member_count' => 2,
        'scope_member_count' => 1,
        'resource_scope_count' => 2,
    ]);

    $coverage = $run->context['inventory']['coverage']['foundation_types'] ?? [];

    $definitionPolicy = Policy::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'intuneRoleDefinition')
        ->where('external_id', 'role-def-1')
        ->first();

    expect($definitionPolicy)->not->toBeNull();
    expect($definitionPolicy?->display_name)->toBe('Help Desk Operator');
    expect($definitionPolicy?->metadata)->toMatchArray([
        'foundation_anchor' => true,
        'foundation_type' => 'intuneRoleDefinition',
        'capture_mode' => 'immutable_backup',
    ]);

    expect($coverage['intuneRoleDefinition']['status'] ?? null)->toBe('succeeded');
    expect($coverage['intuneRoleAssignment']['status'] ?? null)->toBe('succeeded');
    expect($coverage['intuneRoleDefinition']['item_count'] ?? null)->toBe(1);
    expect($coverage['intuneRoleAssignment']['item_count'] ?? null)->toBe(1);
});

test('inventory sync does not sync foundation types when include_foundations is false', function () {
    $tenant = ManagedEnvironment::factory()->create();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'roleScopeTag' => [
            ['id' => 'tag-1', 'displayName' => 'Scope Tag 1', '@odata.type' => '#microsoft.graph.roleScopeTag'],
        ],
    ]));

    $run = executeInventorySyncNow($tenant, [
        'policy_types' => ['roleScopeTag'],
        'categories' => [],
        'include_foundations' => false,
        'include_dependencies' => false,
    ]);

    expect($run['result']['status'] ?? null)->toBe('success');

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'roleScopeTag')
        ->exists())->toBeFalse();
});

test('foundation inventory items store sanitized meta_jsonb after sync (no payload dump)', function () {
    $tenant = ManagedEnvironment::factory()->create();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
        'roleScopeTag' => [
            [
                'id' => 'tag-1',
                'displayName' => 'Scope Tag 1',
                '@odata.type' => '#microsoft.graph.roleScopeTag',
                'veryLargePayload' => str_repeat('x', 10_000),
                'client_secret' => 'should-not-end-up-anywhere',
            ],
        ],
    ]));

    $run = executeInventorySyncNow($tenant, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => [],
        'include_foundations' => true,
        'include_dependencies' => false,
    ]);

    expect($run['result']['status'] ?? null)->toBe('success');

    $foundationItem = \App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'roleScopeTag')
        ->where('external_id', 'tag-1')
        ->first();

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

    $stored = is_array($foundationItem->meta_jsonb) ? $foundationItem->meta_jsonb : [];
    $sanitizer = app(InventoryMetaSanitizer::class);

    expect($stored)->toBe($sanitizer->sanitize($stored));
    expect(json_encode($stored))->not->toContain('Bearer ');
    expect(json_encode($stored))->not->toContain('should-not-end-up-anywhere');
    expect(json_encode($stored))->not->toContain(str_repeat('x', 200));
});

test('inventory sync run counts include foundations when enabled and exclude them when disabled (deterministic)', function () {
    $tenantA = ManagedEnvironment::factory()->create();
    $tenantB = ManagedEnvironment::factory()->create();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [
            [
                'id' => 'pol-1',
                'displayName' => 'Policy 1',
                '@odata.type' => '#microsoft.graph.deviceConfiguration',
            ],
        ],
        'roleScopeTag' => [
            ['id' => 'tag-1', 'displayName' => 'Scope Tag 1', '@odata.type' => '#microsoft.graph.roleScopeTag'],
        ],
        'assignmentFilter' => [
            ['id' => 'filter-1', 'displayName' => 'Filter 1', '@odata.type' => '#microsoft.graph.assignmentFilter'],
        ],
        'notificationMessageTemplate' => [
            [
                'id' => 'tmpl-1',
                'displayName' => 'Template 1',
                '@odata.type' => '#microsoft.graph.notificationMessageTemplate',
            ],
        ],
    ]));

    $runA = executeInventorySyncNow($tenantA, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => [],
        'include_foundations' => true,
        'include_dependencies' => false,
    ]);

    expect($runA['result']['status'] ?? null)->toBe('success');
    expect((int) ($runA['result']['items_observed_count'] ?? 0))->toBe(4);
    expect((int) ($runA['result']['items_upserted_count'] ?? 0))->toBe(4);

    $runB = executeInventorySyncNow($tenantB, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => [],
        'include_foundations' => false,
        'include_dependencies' => false,
    ]);

    expect($runB['result']['status'] ?? null)->toBe('success');
    expect((int) ($runB['result']['items_observed_count'] ?? 0))->toBe(1);
    expect((int) ($runB['result']['items_upserted_count'] ?? 0))->toBe(1);
});

test('configuration policy inventory filtering: settings catalog is not stored as security baseline', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $settingsCatalogLookalike = [
        'id' => 'pol-1',
        'name' => 'Windows 11 SettingsCatalog-Test',
        '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
        'technologies' => ['mdm'],
        'templateReference' => [
            'templateDisplayName' => 'Windows Security Baseline (name only)',
        ],
    ];

    $securityBaseline = [
        'id' => 'pol-2',
        'name' => 'Baseline Policy',
        '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
        'templateReference' => [
            'templateFamily' => 'securityBaseline',
        ],
    ];

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'settingsCatalogPolicy' => [$settingsCatalogLookalike, $securityBaseline],
        'securityBaselinePolicy' => [$settingsCatalogLookalike, $securityBaseline],
    ]));

    $selection = [
        'policy_types' => ['settingsCatalogPolicy', 'securityBaselinePolicy'],
        'categories' => ['Configuration', 'Endpoint Security'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ];

    executeInventorySyncNow($tenant, $selection);

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'securityBaselinePolicy')
        ->where('external_id', 'pol-1')
        ->exists())->toBeFalse();

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'settingsCatalogPolicy')
        ->where('external_id', 'pol-1')
        ->exists())->toBeTrue();

    expect(\App\Models\InventoryItem::query()
        ->where('managed_environment_id', $tenant->id)
        ->where('policy_type', 'securityBaselinePolicy')
        ->where('external_id', 'pol-2')
        ->exists())->toBeTrue();
});

test('meta whitelist drops unknown keys without failing', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $sanitizer = app(InventoryMetaSanitizer::class);

    $meta = $sanitizer->sanitize([
        'odata_type' => '#microsoft.graph.deviceConfiguration',
        'etag' => 'W/\"123\"',
        'scope_tag_ids' => ['0', 'tag-1'],
        'assignment_target_count' => '5',
        'warnings' => ['ok'],
        'unknown_key' => 'should_not_persist',
    ]);

    $item = \App\Models\InventoryItem::query()->create([
        'managed_environment_id' => $tenant->id,
        'policy_type' => 'deviceConfiguration',
        'external_id' => 'cfg-1',
        'display_name' => 'Config 1',
        'meta_jsonb' => $meta,
        'last_seen_at' => now(),
        'last_seen_operation_run_id' => null,
    ]);

    $item->refresh();

    $stored = is_array($item->meta_jsonb) ? $item->meta_jsonb : [];

    expect($stored)->not->toHaveKey('unknown_key');
    expect($stored['assignment_target_count'] ?? null)->toBe(5);
});

test('inventory missing is derived from latest completed run and low confidence on partial runs', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $selection = [
        'policy_types' => ['deviceConfiguration'],
        'categories' => ['Configuration'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ];

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [
            ['id' => 'cfg-1', 'displayName' => 'Config 1', '@odata.type' => '#microsoft.graph.deviceConfiguration'],
        ],
    ]));

    executeInventorySyncNow($tenant, $selection);

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
    ]));

    executeInventorySyncNow($tenant, $selection);

    $missingService = app(InventoryMissingService::class);
    $result = $missingService->missingForSelection($tenant, $selection);

    expect($result['missing'])->toHaveCount(1);
    expect($result['lowConfidence'])->toBeFalse();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
    ], failedTypes: ['deviceConfiguration']));

    executeInventorySyncNow($tenant, $selection);

    $result2 = $missingService->missingForSelection($tenant, $selection);
    expect($result2['missing'])->toHaveCount(1);
    expect($result2['lowConfidence'])->toBeTrue();
});

test('selection isolation: run for selection Y does not affect selection X missing', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $selectionX = [
        'policy_types' => ['deviceConfiguration'],
        'categories' => ['Configuration'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ];

    $selectionY = [
        'policy_types' => ['deviceCompliancePolicy'],
        'categories' => ['Compliance'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ];

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [
            ['id' => 'cfg-1', 'displayName' => 'Config 1', '@odata.type' => '#microsoft.graph.deviceConfiguration'],
        ],
        'deviceCompliancePolicy' => [
            ['id' => 'cmp-1', 'displayName' => 'Compliance 1', '@odata.type' => '#microsoft.graph.deviceCompliancePolicy'],
        ],
    ]));

    executeInventorySyncNow($tenant, $selectionX);
    executeInventorySyncNow($tenant, $selectionY);

    $missingService = app(InventoryMissingService::class);
    $resultX = $missingService->missingForSelection($tenant, $selectionX);

    expect($resultX['missing'])->toHaveCount(0);
});

test('lock prevents overlapping runs for same tenant and selection', function () {
    $tenant = ManagedEnvironment::factory()->create();

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
    ]));

    $selection = [
        'policy_types' => ['deviceConfiguration'],
        'categories' => ['Configuration'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ];

    $hash = app(\App\Services\Inventory\InventorySelectionHasher::class)->hash($selection);
    $lock = Cache::lock("inventory.sync:tenant:{$tenant->id}:selection:{$hash}", 900);
    expect($lock->get())->toBeTrue();

    $run = executeInventorySyncNow($tenant, $selection);

    expect($run['result']['status'] ?? null)->toBe('skipped');
    $codes = is_array($run['result']['error_codes'] ?? null) ? $run['result']['error_codes'] : [];
    expect($codes)->toContain('lock_contended');

    $lock->release();
});

test('inventory sync does not create snapshot or backup rows', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $baseline = [
        'policy_versions' => PolicyVersion::query()->count(),
        'backup_sets' => BackupSet::query()->count(),
        'backup_items' => BackupItem::query()->count(),
        'backup_schedules' => BackupSchedule::query()->count(),
    ];

    app()->instance(GraphClientInterface::class, fakeGraphClient([
        'deviceConfiguration' => [],
    ]));

    executeInventorySyncNow($tenant, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => ['Configuration'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ]);

    expect(PolicyVersion::query()->count())->toBe($baseline['policy_versions']);
    expect(BackupSet::query()->count())->toBe($baseline['backup_sets']);
    expect(BackupItem::query()->count())->toBe($baseline['backup_items']);
    expect(BackupSchedule::query()->count())->toBe($baseline['backup_schedules']);
});

test('run error persistence is safe and does not include bearer tokens', function () {
    $tenant = ManagedEnvironment::factory()->create();

    $throwable = new RuntimeException('Graph failed: Bearer abc.def.ghi');

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

    $run = executeInventorySyncNow($tenant, [
        'policy_types' => ['deviceConfiguration'],
        'categories' => ['Configuration'],
        'include_foundations' => false,
        'include_dependencies' => false,
    ]);

    expect($run['result']['status'] ?? null)->toBe('failed');

    $context = is_array($run['result']['error_context'] ?? null) ? $run['result']['error_context'] : [];
    $message = (string) ($context['message'] ?? '');

    expect($message)->not->toContain('abc.def.ghi');
    expect($message)->toContain('Bearer [REDACTED]');
});
