<?php

namespace Tests\Feature;

use Tests\TestCase;

/**
 * One upload ceiling, agreed on by everything in the path.
 *
 * A file crossing this system passes four separate size checks — nginx,
 * PHP, Livewire's own temporary-upload rule, and the component's validation —
 * and before this they disagreed: the rules said 5 MB, Livewire's undeclared
 * default said 12 MB, nginx said 25 MB. Whichever was smallest won, with a
 * message written by whoever happened to own that layer, and a 413 from nginx
 * is an error page rather than something a receptionist can act on.
 *
 * These assertions are about the ordering that keeps the error readable:
 * nginx must be the most generous, then PHP, then the application. Get it
 * backwards and a file at the limit is refused as a malformed request instead
 * of an oversized file.
 */
class UploadLimitsTest extends TestCase
{
    private function iniBytes(string $value): int
    {
        $value = trim($value);
        $unit = strtolower(substr($value, -1));
        $number = (int) $value;

        return match ($unit) {
            'g' => $number * 1024 * 1024 * 1024,
            'm' => $number * 1024 * 1024,
            'k' => $number * 1024,
            default => $number,
        };
    }

    private function phpIni(): string
    {
        return file_get_contents(base_path('docker/php/php.ini'));
    }

    private function iniValue(string $key): string
    {
        preg_match('/^' . preg_quote($key, '/') . '\s*=\s*(\S+)/m', $this->phpIni(), $m);

        $this->assertNotEmpty($m, "{$key} is not set in docker/php/php.ini");

        return $m[1];
    }

    public function test_the_application_ceiling_is_the_one_the_client_asked_for(): void
    {
        $this->assertSame(153600, (int) config('uploads.max_kb'), '150 MB, in kilobytes.');
    }

    public function test_php_accepts_a_file_at_the_ceiling(): void
    {
        $ceiling = ((int) config('uploads.max_kb')) * 1024;

        $this->assertGreaterThanOrEqual(
            $ceiling,
            $this->iniBytes($this->iniValue('upload_max_filesize')),
            'upload_max_filesize must not be the thing that refuses a file the app allows.'
        );
    }

    public function test_the_whole_request_is_allowed_to_be_bigger_than_the_file(): void
    {
        // The POST carries the file plus Livewire's snapshot and the form
        // fields. Equal limits mean a file at exactly the ceiling is rejected
        // as an oversized request — a different, and far less useful, error.
        $this->assertGreaterThan(
            $this->iniBytes($this->iniValue('upload_max_filesize')),
            $this->iniBytes($this->iniValue('post_max_size')),
            'post_max_size must exceed upload_max_filesize.'
        );
    }

    public function test_nginx_is_the_most_generous_of_the_three(): void
    {
        $conf = file_get_contents(base_path('docker/nginx/default.conf'));

        preg_match('/client_max_body_size\s+(\S+);/', $conf, $m);
        $this->assertNotEmpty($m, 'client_max_body_size is not set.');

        $this->assertGreaterThanOrEqual(
            $this->iniBytes($this->iniValue('post_max_size')),
            $this->iniBytes(rtrim($m[1], ';')),
            'nginx refuses before PHP does, and its refusal is an error page — so it must be the looser limit.'
        );
    }

    public function test_livewire_does_not_refuse_before_the_application_does(): void
    {
        // Livewire validates the temporary upload before any component sees
        // it. Its undeclared default is 12 MB, which silently capped every
        // upload in the system regardless of the rules the component declared.
        $rules = config('livewire.temporary_file_upload.rules');

        $this->assertIsArray($rules, 'The temporary upload rules must be declared, not left to the package default.');
        $this->assertContains(
            'max:' . config('uploads.max_kb'),
            $rules,
            'Livewire must allow what the application allows.'
        );
    }

    public function test_a_slow_upload_is_given_time_to_arrive(): void
    {
        // 150 MB over Egyptian mobile data is minutes, not seconds. The clock
        // that cuts it off is max_input_time — the body being received — not
        // max_execution_time.
        $this->assertGreaterThanOrEqual(300, (int) $this->iniValue('max_input_time'));
        $this->assertGreaterThanOrEqual(
            10,
            (int) config('livewire.temporary_file_upload.max_upload_time'),
            'Livewire invalidates an upload still in flight after this many minutes.'
        );
    }

    public function test_pictures_keep_a_sane_ceiling_of_their_own(): void
    {
        // A logo is not a video: it is carried on every page load.
        $this->assertLessThan((int) config('uploads.max_kb'), (int) config('uploads.image_max_kb'));
        $this->assertLessThan((int) config('uploads.image_max_kb'), (int) config('uploads.icon_max_kb'));
    }
}
