<?php

use Illuminate\Support\Facades\File;

/**
 * R24 — a modal is never taller than the screen.
 *
 * A modal grew with its content, so a form ran off the bottom of the window and
 * took its own save button with it: reachable only by scrolling the page BEHIND
 * the backdrop, which on a phone means not reachable. Reported exactly that way.
 *
 * Two halves, both checked here: the panel is capped once for every modal in the
 * published package view, and any modal carrying a field uses the component that
 * keeps its header and footer outside the scroll region.
 */

/**
 * Every modal view, by way of its component.
 *
 * Found through the components rather than by globbing the views, because a view
 * is only a modal if a ModalComponent renders it — and the file names follow the
 * class names, which is what makes the mapping possible at all.
 *
 * @return array<string, string> path => contents
 */
function modalViews(): array
{
    $views = [];

    foreach (File::allFiles(app_path('Livewire')) as $file) {
        if (! str_contains($file->getContents(), 'extends ModalComponent')) {
            continue;
        }

        $kebab = strtolower((string) preg_replace('/([a-z0-9])([A-Z])/', '$1-$2', $file->getFilenameWithoutExtension()));
        $folder = str_contains($file->getRelativePath(), 'Admin') ? 'admin/' : '';
        $path = resource_path('views/livewire/'.$folder.$kebab.'.blade.php');

        if (File::exists($path)) {
            $views[$folder.$kebab.'.blade.php'] = File::get($path);
        }
    }

    return $views;
}

it('caps the panel once, for every modal there is', function () {
    // In the published package view, so no modal can opt out of it and none has
    // to remember to. dvh rather than vh: on a phone the browser's own chrome
    // counts towards vh, which is exactly the case where the last centimetre
    // decides whether the button is reachable.
    $wrapper = File::get(resource_path('views/vendor/wire-elements-modal/modal.blade.php'));

    expect($wrapper)->toContain('max-h-[calc(100dvh-')
        ->toContain('flex-col')
        ->toContain('overflow-hidden')
        // The flex child has to be allowed to shrink, or capping the panel
        // achieves nothing at all.
        ->toContain('min-h-0');
});

it('keeps the header and the footer out of the scroll region', function () {
    $modal = File::get(resource_path('views/components/ui/modal.blade.php'));

    expect($modal)->toContain('shrink-0')
        ->toContain('overflow-y-auto')
        // min-h-0 on the body is the line that makes the overflow engage.
        ->toContain('min-h-0 flex-1 overflow-y-auto')
        // Reaching the end of the body must not start scrolling the page behind
        // the backdrop.
        ->toContain('overscroll-contain');
});

it('builds every modal with a field through the component', function () {
    // The criterion is the field, not the line count: a two-line confirmation
    // with one button has nothing that needs to stay put, and converting those
    // would be churn. The moment somebody adds an input to one, this fails and
    // says so.
    $offenders = [];

    foreach (modalViews() as $name => $body) {
        $hasField = preg_match('/<form|<input|<textarea|<select|x-ui\.(?:input|otp-input|checkbox)/', $body) === 1;

        if ($hasField && ! str_contains($body, '<x-ui.modal')) {
            $offenders[] = $name;
        }
    }

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

it('puts no Blade directive in a component tag attribute list', function () {
    // It lands in the attribute bag and breaks the view outright — a compiled
    // `if … endif` where an attribute belongs. It cost this project two
    // debugging rounds already: @disabled on x-ui.button, and a conditional
    // wire:poll on x-ui.modal. Such attributes go on an element of their own.
    $offenders = [];

    foreach (File::allFiles(resource_path('views')) as $file) {
        if (! str_ends_with($file->getFilename(), '.blade.php')) {
            continue;
        }

        // Comments stripped: this repository has three times read its own
        // explanation of a mistake as the mistake.
        $body = (string) preg_replace('/\{\{--.*?--\}\}/s', '', $file->getContents());

        // An opening component tag, up to its closing bracket, containing a
        // directive. Deliberately narrow: @class and @style ARE supported in
        // that position by Blade itself.
        if (preg_match('/<x-[\w.:-]+(?:[^>]*?)\s@(?!class|style)\w+\s*\(/s', $body) === 1) {
            $offenders[] = $file->getRelativePathname();
        }
    }

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

it('submits from the footer through form=, since the button is outside the form', function () {
    // The price of a footer that does not scroll away — and an HTML attribute
    // rather than a workaround. A submit button in the footer of a modal whose
    // form is in the body does nothing at all without it.
    $offenders = [];

    foreach (modalViews() as $name => $body) {
        if (! str_contains($body, '<x-slot:footer>') || ! str_contains($body, '<form')) {
            continue;
        }

        // Only where the footer actually holds a submit.
        $footer = (string) strstr($body, '<x-slot:footer>');

        if (! str_contains($footer, 'type="submit"')) {
            continue;
        }

        // One needle, not two: Pest's toContain() is variadic, so a second
        // argument is another string it must contain — not a message. Passing
        // the file name there asserted that the footer contains its own
        // filename, which is why this failed on a file that was correct.
        expect($footer)->toContain('form="');
    }
});
