<?php

declare(strict_types=1);

use App\Support\Governance\Controls\CanonicalControlCatalog;
use App\Support\Governance\Controls\CanonicalControlResolutionRequest;
use App\Support\Governance\Controls\CanonicalControlResolver;

it('resolves multiple Microsoft subject families to one stable canonical control identity', function (): void {
    $resolver = app(CanonicalControlResolver::class);

    $configurationResult = $resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'evidence',
        subjectFamilyKey: 'deviceConfiguration',
        workload: 'intune',
        signalKey: 'intune.device_configuration_drift',
    ));
    $complianceResult = $resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'review',
        subjectFamilyKey: 'deviceCompliancePolicy',
        workload: 'intune',
        signalKey: 'intune.device_compliance_policy',
    ));

    expect($configurationResult->toArray()['control']['control_key'])->toBe('endpoint_hardening_compliance')
        ->and($complianceResult->toArray()['control']['control_key'])->toBe('endpoint_hardening_compliance')
        ->and($configurationResult->toArray()['control']['name'])->toBe($complianceResult->toArray()['control']['name']);
});

it('uses supplied signal context instead of letting workload labels become primary identity', function (): void {
    $resolver = app(CanonicalControlResolver::class);

    $strongAuthentication = $resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'evidence',
        subjectFamilyKey: 'conditional_access_policy',
        workload: 'entra',
        signalKey: 'conditional_access.require_mfa',
    ));
    $accessEnforcement = $resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'evidence',
        subjectFamilyKey: 'conditional_access_policy',
        workload: 'entra',
        signalKey: 'conditional_access.policy_state',
    ));

    expect($strongAuthentication->toArray()['control']['control_key'])->toBe('strong_authentication')
        ->and($accessEnforcement->toArray()['control']['control_key'])->toBe('conditional_access_enforcement');
});

it('returns explicit unresolved reason codes instead of fallback labels', function (): void {
    $resolver = app(CanonicalControlResolver::class);

    expect($resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'unknown',
        consumerContext: 'evidence',
        subjectFamilyKey: 'deviceConfiguration',
    ))->toArray())->toMatchArray([
        'status' => 'unresolved',
        'reason_code' => 'unsupported_provider',
    ]);

    expect($resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'evidence',
    ))->toArray())->toMatchArray([
        'status' => 'unresolved',
        'reason_code' => 'insufficient_context',
    ]);

    expect($resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'evidence',
        subjectFamilyKey: 'not_bound',
    ))->toArray())->toMatchArray([
        'status' => 'unresolved',
        'reason_code' => 'missing_binding',
    ]);
});

it('fails deterministically when a binding context is ambiguous', function (): void {
    $resolver = new CanonicalControlResolver(new CanonicalControlCatalog([
        spec236ControlDefinition('first_control', [
            'microsoft_bindings' => [
                spec236Binding('shared_subject', primary: false),
            ],
        ]),
        spec236ControlDefinition('second_control', [
            'microsoft_bindings' => [
                spec236Binding('shared_subject', primary: false),
            ],
        ]),
    ]));

    $result = $resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'evidence',
        subjectFamilyKey: 'shared_subject',
        workload: 'entra',
        signalKey: 'shared.signal',
    ))->toArray();

    expect($result['status'])->toBe('ambiguous')
        ->and($result['reason_code'])->toBe('ambiguous_binding')
        ->and($result['candidate_control_keys'])->toBe(['first_control', 'second_control']);
});

it('keeps retired controls resolvable for historical references', function (): void {
    $resolver = new CanonicalControlResolver(new CanonicalControlCatalog([
        spec236ControlDefinition('retired_control', [
            'historical_status' => 'retired',
            'microsoft_bindings' => [
                spec236Binding('retired_subject'),
            ],
        ]),
    ]));

    $result = $resolver->resolve(new CanonicalControlResolutionRequest(
        provider: 'microsoft',
        consumerContext: 'review',
        subjectFamilyKey: 'retired_subject',
        workload: 'entra',
        signalKey: 'shared.signal',
    ))->toArray();

    expect($result['status'])->toBe('resolved')
        ->and($result['control']['control_key'])->toBe('retired_control')
        ->and($result['control']['historical_status'])->toBe('retired');
});

/**
 * @param  array<string, mixed>  $overrides
 * @return array<string, mixed>
 */
function spec236ControlDefinition(string $controlKey, array $overrides = []): array
{
    return array_replace_recursive([
        'control_key' => $controlKey,
        'name' => str_replace('_', ' ', ucfirst($controlKey)),
        'domain_key' => 'identity_access',
        'subdomain_key' => 'test_subjects',
        'control_class' => 'preventive',
        'summary' => 'Test summary.',
        'operator_description' => 'Test operator description.',
        'detectability_class' => 'direct_technical',
        'evaluation_strategy' => 'state_evaluated',
        'evidence_archetypes' => ['configuration_snapshot'],
        'artifact_suitability' => [
            'baseline' => true,
            'drift' => true,
            'finding' => true,
            'exception' => true,
            'evidence' => true,
            'review' => true,
            'report' => true,
        ],
        'historical_status' => 'active',
        'microsoft_bindings' => [],
    ], $overrides);
}

/**
 * @return array<string, mixed>
 */
function spec236Binding(string $subjectFamilyKey, bool $primary = true): array
{
    return [
        'subject_family_key' => $subjectFamilyKey,
        'workload' => 'entra',
        'signal_keys' => ['shared.signal'],
        'supported_contexts' => ['evidence', 'review'],
        'primary' => $primary,
    ];
}
