<?php

declare(strict_types=1);

use Symfony\Component\Yaml\Yaml;
use Tests\Support\OpsUx\SourceFileScanner;

it('keeps covered derived-state consumers on declared access paths without ad hoc caches', function (): void {
    $root = SourceFileScanner::projectRoot();
    $contractPath = repo_path('specs/167-derived-state-memoization/contracts/request-scoped-derived-state.logical.openapi.yaml');

    /** @var array<string, mixed> $contract */
    $contract = Yaml::parseFile($contractPath);
    $declarations = $contract['x-derived-state-consumers'] ?? [];

    expect($declarations)->toBeArray()->not->toBeEmpty();

    $allowedFamilies = [
        'artifact_truth',
        'operation_ux_guidance',
        'operation_ux_explanation',
        'related_navigation_primary',
        'related_navigation_detail',
        'related_navigation_header',
        'tenant_governance_aggregate',
    ];
    $allowedAccessPatterns = ['row_safe', 'page_safe', 'direct_once', 'widget_safe'];
    $allowedFreshnessPolicies = ['request_stable', 'invalidate_after_mutation', 'no_reuse'];
    $cachePattern = '/static\s+array\s+\$[A-Za-z0-9_]*cache\b/i';
    $violations = [];

    foreach ($declarations as $index => $declaration) {
        if (! is_array($declaration)) {
            $violations[] = [
                'surface' => 'contract',
                'message' => sprintf('Declaration %d must be an object.', $index),
            ];

            continue;
        }

        $surface = trim((string) ($declaration['surface'] ?? ''));
        $family = trim((string) ($declaration['family'] ?? ''));
        $variant = trim((string) ($declaration['variant'] ?? ''));
        $accessPattern = trim((string) ($declaration['accessPattern'] ?? ''));
        $freshnessPolicy = trim((string) ($declaration['freshnessPolicy'] ?? ''));
        $scopeInputs = $declaration['scopeInputs'] ?? null;
        $guardScope = $declaration['guardScope'] ?? null;
        $requiredMarkers = array_values(array_filter(
            $declaration['requiredMarkers'] ?? [],
            static fn (mixed $marker): bool => is_string($marker) && trim($marker) !== '',
        ));
        $maxOccurrences = $declaration['maxOccurrences'] ?? [];

        if ($surface === '' || $variant === '') {
            $violations[] = [
                'surface' => $surface !== '' ? $surface : 'contract',
                'message' => 'Each declaration must provide non-empty surface and variant values.',
            ];
        }

        if (! in_array($family, $allowedFamilies, true)) {
            $violations[] = [
                'surface' => $surface !== '' ? $surface : 'contract',
                'message' => sprintf('Unsupported family "%s".', $family),
            ];
        }

        if (! in_array($accessPattern, $allowedAccessPatterns, true)) {
            $violations[] = [
                'surface' => $surface !== '' ? $surface : 'contract',
                'message' => sprintf('Unsupported accessPattern "%s".', $accessPattern),
            ];
        }

        if (! in_array($freshnessPolicy, $allowedFreshnessPolicies, true)) {
            $violations[] = [
                'surface' => $surface !== '' ? $surface : 'contract',
                'message' => sprintf('Unsupported freshnessPolicy "%s".', $freshnessPolicy),
            ];
        }

        if (! is_array($scopeInputs) || $scopeInputs === []) {
            $violations[] = [
                'surface' => $surface !== '' ? $surface : 'contract',
                'message' => 'Each declaration must include at least one scope input.',
            ];
        }

        if (! is_array($guardScope) || $guardScope === []) {
            $violations[] = [
                'surface' => $surface !== '' ? $surface : 'contract',
                'message' => 'Each declaration must include at least one guardScope path.',
            ];

            continue;
        }

        foreach ($guardScope as $relativePath) {
            $relativePath = trim((string) $relativePath);
            $absolutePath = $root.'/'.$relativePath;

            if ($relativePath === '' || ! is_file($absolutePath)) {
                $violations[] = [
                    'surface' => $surface !== '' ? $surface : 'contract',
                    'message' => sprintf('Missing guardScope file "%s".', $relativePath),
                ];

                continue;
            }

            $source = SourceFileScanner::read($absolutePath);

            foreach ($requiredMarkers as $marker) {
                if (str_contains($source, $marker)) {
                    continue;
                }

                $violations[] = [
                    'surface' => $surface,
                    'file' => SourceFileScanner::relativePath($absolutePath),
                    'message' => sprintf('Missing required marker "%s".', $marker),
                ];
            }

            if (preg_match($cachePattern, $source, $match, PREG_OFFSET_CAPTURE) === 1) {
                $offset = $match[0][1];
                $line = substr_count(substr($source, 0, is_int($offset) ? $offset : 0), "\n") + 1;

                $violations[] = [
                    'surface' => $surface,
                    'file' => SourceFileScanner::relativePath($absolutePath),
                    'line' => $line,
                    'message' => 'Ad hoc local cache detected in guarded surface.',
                    'snippet' => SourceFileScanner::snippet($source, $line),
                ];
            }

            if (! is_array($maxOccurrences)) {
                continue;
            }

            foreach ($maxOccurrences as $occurrenceRule) {
                if (! is_array($occurrenceRule)) {
                    continue;
                }

                $needle = trim((string) ($occurrenceRule['needle'] ?? ''));
                $max = (int) ($occurrenceRule['max'] ?? -1);

                if ($needle === '' || $max < 0) {
                    continue;
                }

                $actual = substr_count($source, $needle);

                if ($actual <= $max) {
                    continue;
                }

                $offset = strpos($source, $needle);
                $line = $offset === false ? 1 : substr_count(substr($source, 0, $offset), "\n") + 1;

                $violations[] = [
                    'surface' => $surface,
                    'file' => SourceFileScanner::relativePath($absolutePath),
                    'line' => $line,
                    'message' => sprintf('Found %d occurrences of "%s"; expected at most %d.', $actual, $needle, $max),
                    'snippet' => SourceFileScanner::snippet($source, $line),
                ];
            }
        }
    }

    if ($violations !== []) {
        $messages = array_map(static function (array $violation): string {
            $location = $violation['surface'] ?? 'contract';

            if (isset($violation['file'])) {
                $location .= ' @ '.$violation['file'];
            }

            if (isset($violation['line'])) {
                $location .= ':'.$violation['line'];
            }

            $message = $location."\n".$violation['message'];

            if (isset($violation['snippet'])) {
                $message .= "\n".$violation['snippet'];
            }

            return $message;
        }, $violations);

        $this->fail(
            "Derived-state consumer guard violations found:\n\n".implode("\n\n", $messages)
        );
    }

    expect($violations)->toBe([]);
});
