<?php

declare(strict_types=1);

use App\Support\Baselines\Compare\CompareFindingCandidate;
use App\Support\Baselines\Compare\CompareState;
use App\Support\Baselines\Compare\CompareSubjectIdentity;
use App\Support\Baselines\Compare\CompareSubjectProjection;
use App\Support\Baselines\Compare\CompareSubjectResult;

it('serializes compare subject results with structured finding and diagnostics payloads', function (): void {
    $result = new CompareSubjectResult(
        subjectIdentity: new CompareSubjectIdentity(
            domainKey: 'entra',
            subjectClass: 'control',
            subjectTypeKey: 'conditionalAccessPolicy',
            externalSubjectId: 'cap-1',
            subjectKey: 'cap-1',
        ),
        projection: new CompareSubjectProjection(
            platformSubjectClass: 'control',
            domainKey: 'entra',
            subjectTypeKey: 'conditionalAccessPolicy',
            operatorLabel: 'Conditional Access Policy',
            summaryKind: 'control_snapshot',
            additionalLabels: ['family' => 'identity'],
        ),
        baselineAvailability: 'available',
        currentStateAvailability: 'available',
        compareState: CompareState::Drift,
        trustLevel: 'limited_confidence',
        evidenceQuality: 'meta',
        severityRecommendation: 'medium',
        findingCandidate: new CompareFindingCandidate(
            changeType: 'different_version',
            severity: 'medium',
            fingerprintBasis: [
                'policy_type' => 'conditionalAccessPolicy',
                'subject_key' => 'cap-1',
                'change_type' => 'different_version',
            ],
            evidencePayload: [
                'summary' => ['kind' => 'control_snapshot'],
            ],
        ),
        diagnostics: [
            'strategy_key' => 'future_control',
        ],
    );

    $payload = $result->toArray();

    expect($result->hasFindingCandidate())->toBeTrue()
        ->and($result->isGapState())->toBeFalse()
        ->and($payload['compare_state'])->toBe(CompareState::Drift->value)
        ->and($payload['baseline_availability'])->toBe('available')
        ->and($payload['current_state_availability'])->toBe('available')
        ->and($payload['projection']['platform_subject_class'])->toBe('control')
        ->and($payload['projection']['summary_kind'])->toBe('control_snapshot')
        ->and($payload['finding_candidate']['change_type'])->toBe('different_version')
        ->and($payload['finding_candidate']['severity'])->toBe('medium')
        ->and($payload['diagnostics']['strategy_key'])->toBe('future_control');
});

it('requires a finding candidate for drift results', function (): void {
    expect(fn (): CompareSubjectResult => new CompareSubjectResult(
        subjectIdentity: new CompareSubjectIdentity('intune', 'policy', 'deviceConfiguration', 'policy-1', 'policy-1'),
        projection: new CompareSubjectProjection('policy', 'intune', 'deviceConfiguration', 'Policy 1'),
        baselineAvailability: 'available',
        currentStateAvailability: 'missing',
        compareState: CompareState::Drift,
        trustLevel: 'limited_confidence',
        evidenceQuality: 'meta',
    ))->toThrow(InvalidArgumentException::class, 'require a finding candidate');
});

it('rejects non-drift results that still try to write findings', function (): void {
    expect(fn (): CompareSubjectResult => new CompareSubjectResult(
        subjectIdentity: new CompareSubjectIdentity('entra', 'control', 'conditionalAccessPolicy', 'cap-1', 'cap-1'),
        projection: new CompareSubjectProjection('control', 'entra', 'conditionalAccessPolicy', 'CAP 1'),
        baselineAvailability: 'available',
        currentStateAvailability: 'unknown',
        compareState: CompareState::Incomplete,
        trustLevel: 'unusable',
        evidenceQuality: 'missing',
        findingCandidate: new CompareFindingCandidate(
            changeType: 'different_version',
            severity: 'high',
            fingerprintBasis: ['subject_key' => 'cap-1'],
            evidencePayload: [],
        ),
    ))->toThrow(InvalidArgumentException::class, 'Only drift compare subject results');
});

it('treats unsupported, incomplete, ambiguous, and failed states as gap states', function (): void {
    $states = [
        CompareState::Unsupported,
        CompareState::Incomplete,
        CompareState::Ambiguous,
        CompareState::Failed,
    ];

    foreach ($states as $state) {
        $result = new CompareSubjectResult(
            subjectIdentity: new CompareSubjectIdentity('entra', 'control', 'conditionalAccessPolicy', 'cap-1', 'cap-1'),
            projection: new CompareSubjectProjection('control', 'entra', 'conditionalAccessPolicy', 'CAP 1'),
            baselineAvailability: 'available',
            currentStateAvailability: 'unknown',
            compareState: $state,
            trustLevel: 'unusable',
            evidenceQuality: 'missing',
            diagnostics: [
                'reason_code' => 'strategy_failed',
                'gap_record' => ['reason_code' => 'strategy_failed'],
            ],
        );

        expect($result->isGapState())->toBeTrue()
            ->and($result->gapReasonCode())->toBe('strategy_failed')
            ->and($result->gapRecord())->toBe(['reason_code' => 'strategy_failed']);
    }
});