<?php

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;

/**
 * Times are stored in UTC and shown in the operator's zone.
 *
 * The bug this exists to prevent: a console announcing that an update would
 * start "spätestens um 15:21" while the clock on the wall said 17:21. Two of
 * the offending views even looked as though they had handled it — they called
 *
 *     ->timezone(config('app.timezone'))
 *
 * which reads like "convert to local time" and, because app.timezone is the
 * STORAGE zone and must stay UTC, converts to UTC: a no-op wearing the costume
 * of a fix. The other twelve views simply printed the database value.
 *
 * So the rule is mechanical rather than remembered: anything absolute that a
 * person will read goes through ->local() first.
 */

/** Absolute formats. diffForHumans() is relative and reads the same anywhere. */
function absoluteTimeCalls(string $source): array
{
    $hits = [];

    foreach (explode("\n", $source) as $n => $line) {
        if (! preg_match('/->(isoFormat|translatedFormat)\(|->format\([\'"][^\'"]*[YmdHis][^\'"]*[\'"]\)/', $line)) {
            continue;
        }

        // A line may format several times; one ->local() per formatted value.
        $formats = preg_match_all('/->(isoFormat|translatedFormat|format)\(/', $line);
        $locals = preg_match_all('/->local\(\)/', $line);

        if ($locals < $formats) {
            $hits[] = ($n + 1).': '.trim($line);
        }
    }

    return $hits;
}

it('converts every displayed time to the display zone', function () {
    $offenders = [];

    $files = collect(File::allFiles(resource_path('views')))
        ->merge(File::allFiles(app_path('Livewire')))
        ->filter(fn ($f) => in_array($f->getExtension(), ['php'], true));

    foreach ($files as $file) {
        $hits = absoluteTimeCalls($file->getContents());

        if ($hits !== []) {
            $offenders[str_replace(base_path().'/', '', $file->getPathname())] = $hits;
        }
    }

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

it('never dresses a UTC no-op up as a timezone conversion', function () {
    // ->timezone(config('app.timezone')) is the specific trap: it looks like a
    // conversion and does nothing, which is worse than no call at all because
    // it stops anyone looking further.
    $offenders = [];

    foreach (collect(File::allFiles(resource_path('views')))->merge(File::allFiles(app_path())) as $file) {
        if (str_contains($file->getContents(), "timezone(config('app.timezone'))")) {
            $offenders[] = str_replace(base_path().'/', '', $file->getPathname());
        }
    }

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

it('keeps storage in UTC', function () {
    // The display zone must never leak into the storage zone. Comparable
    // timestamps across hosts depend on this staying put.
    expect(config('app.timezone'))->toBe('UTC')
        ->and(config('app.display_timezone'))->not->toBe('UTC');
});

it('shows a summer time and a winter time in the right zone', function () {
    // Both sides of the DST boundary, because a fixed +1 offset would pass a
    // January test and be an hour out all summer.
    $winter = Carbon::parse('2026-01-15 14:00:00', 'UTC');
    $summer = Carbon::parse('2026-07-15 14:00:00', 'UTC');

    expect($winter->local()->format('H:i'))->toBe('15:00')
        ->and($summer->local()->format('H:i'))->toBe('16:00');
});

it('leaves the stored value untouched when showing it', function () {
    // ->local() must not mutate the instance it was called on: the same object
    // is often written back or compared after being rendered.
    $stored = Carbon::parse('2026-07-15 14:00:00', 'UTC');
    $stored->local()->format('H:i');

    expect($stored->format('H:i T'))->toBe('14:00 UTC');
});

it('takes a wall-clock field back as the time the operator meant', function () {
    // The half that is easy to forget. A datetime-local field carries no
    // offset, so 21:00 typed by someone in Vienna is 19:00 UTC — parsed
    // without a zone it would be stored as 21:00 UTC and the window would run
    // two hours late.
    expect(\App\Support\LocalTime::fromField('2026-07-15T21:00')->format('H:i T'))->toBe('19:00 UTC')
        // Winter, where the offset is one hour, not two.
        ->and(\App\Support\LocalTime::fromField('2026-01-15T21:00')->format('H:i T'))->toBe('20:00 UTC');
});

it('survives the round trip through a form field', function () {
    // What goes into the field must come back as the same instant.
    $stored = Carbon::parse('2026-07-15 19:00:00', 'UTC');
    $shown = \App\Support\LocalTime::toField($stored);

    expect($shown)->toBe('2026-07-15T21:00')
        ->and(\App\Support\LocalTime::fromField($shown)->equalTo($stored))->toBeTrue();
});

it('treats an empty field as no time rather than as now', function () {
    expect(\App\Support\LocalTime::fromField(''))->toBeNull()
        ->and(\App\Support\LocalTime::fromField('   '))->toBeNull()
        ->and(\App\Support\LocalTime::toField(null))->toBe('');
});
