<?php

use App\Livewire\Admin\Settings as AdminSettings;
use App\Services\Deployment\UpdateChannel;
use App\Services\Deployment\UpdateWindow;
use App\Support\Settings as AppSettings;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;
use Livewire\Livewire;

/**
 * Updating on a schedule instead of on a click.
 *
 * The button does not go away — manual stays possible at any hour. This is the
 * option beside it, and the tests are about the two things it must never do:
 * run outside the window it was given, and do anything at all while it is
 * switched off.
 */
beforeEach(function () {
    File::deleteDirectory(storage_path('app/deploy'));
});

function windowStatus(): void
{
    File::ensureDirectoryExists(storage_path('app/deploy'));
    File::put(storage_path('app/deploy/update-status.json'), json_encode([
        'state' => 'idle',
        'checked_at' => now()->toIso8601String(),
        'behind' => 1,
    ]));
}

function openWindow(array $days, string $from = '05:00', string $to = '06:00'): void
{
    AppSettings::set(UpdateWindow::KEY_ENABLED, true);
    AppSettings::set(UpdateWindow::KEY_DAYS, $days);
    AppSettings::set(UpdateWindow::KEY_FROM, $from);
    AppSettings::set(UpdateWindow::KEY_TO, $to);
}

it('does nothing at all while the automation is off', function () {
    // The switch has to mean it. An installation whose owner deliberately
    // updates by hand must never find itself in maintenance mode at dawn.
    windowStatus();
    AppSettings::set(UpdateWindow::KEY_ENABLED, false);

    $this->artisan('clupilot:auto-update')->assertSuccessful();

    expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
});

it('takes an available release inside the window', function () {
    // Wednesday, 05:30 local.
    Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));
    windowStatus();
    openWindow([3]);

    expect(app(UpdateWindow::class)->isOpen())->toBeTrue();

    $this->artisan('clupilot:auto-update')->assertSuccessful();

    expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeTrue();
});

it('leaves it alone outside the hours', function () {
    // Wednesday, 11:30 local — the right day, the wrong time.
    Carbon::setTestNow(Carbon::parse('2026-07-29 09:30:00', 'UTC'));
    windowStatus();
    openWindow([3]);

    expect(app(UpdateWindow::class)->isOpen())->toBeFalse();

    $this->artisan('clupilot:auto-update');

    expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
});

it('leaves it alone on a day that was not chosen', function () {
    Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));   // Wednesday
    windowStatus();
    openWindow([1, 2]);                                               // Mon, Tue

    expect(app(UpdateWindow::class)->isOpen())->toBeFalse();
});

it('understands a window that runs past midnight', function () {
    // 23:00–02:00 is a perfectly reasonable thing to ask for, and reading it
    // as an empty range would silently mean "never".
    $window = app(UpdateWindow::class);

    // Wednesday 23:30 local — inside, on the chosen day.
    openWindow([3], '23:00', '02:00');
    Carbon::setTestNow(Carbon::parse('2026-07-29 21:30:00', 'UTC'));
    expect($window->isOpen())->toBeTrue();

    // Thursday 01:30 local — still the tail of Wednesday's window.
    Carbon::setTestNow(Carbon::parse('2026-07-29 23:30:00', 'UTC'));
    expect($window->isOpen())->toBeTrue();

    // Thursday 03:30 local — over.
    Carbon::setTestNow(Carbon::parse('2026-07-30 01:30:00', 'UTC'));
    expect($window->isOpen())->toBeFalse();
});

it('finds nothing to do when the operator already updated by hand', function () {
    // Named by the owner as the case that must not surprise them: pressed at
    // four, window at five. Nothing guards against it explicitly — an
    // installation that is current simply has nothing available.
    Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));
    File::ensureDirectoryExists(storage_path('app/deploy'));
    File::put(storage_path('app/deploy/update-status.json'), json_encode([
        'state' => 'idle',
        'checked_at' => now()->toIso8601String(),
        'behind' => 0,
    ]));
    openWindow([3]);

    expect(app(UpdateChannel::class)->state()['available'])->toBeFalse();

    $this->artisan('clupilot:auto-update');

    expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
});

it('does not queue a second run over one already in flight', function () {
    Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));
    windowStatus();
    openWindow([3]);

    app(UpdateChannel::class)->request('owner@example.com');
    $before = File::get(storage_path('app/deploy/update-request.json'));

    $this->artisan('clupilot:auto-update');

    expect(File::get(storage_path('app/deploy/update-request.json')))->toBe($before);
});

it('will not save an automation that could never run', function () {
    // Switched on with no day selected is a configuration that reads as working
    // and does nothing. Refused rather than silently corrected.
    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(AdminSettings::class)
        ->set('autoUpdate', true)
        ->set('autoDays', [])
        ->set('autoFrom', '05:00')
        ->set('autoTo', '06:00')
        ->call('saveUpdateWindow')
        ->assertHasErrors('autoDays');

    expect(app(UpdateWindow::class)->enabled())->toBeFalse();
});

it('saves a window the owner can see again afterwards', function () {
    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(AdminSettings::class)
        ->set('autoUpdate', true)
        ->set('autoDays', [2, 4])
        ->set('autoFrom', '04:30')
        ->set('autoTo', '05:30')
        ->call('saveUpdateWindow')
        ->assertHasNoErrors();

    $window = app(UpdateWindow::class);

    expect($window->enabled())->toBeTrue()
        ->and($window->days())->toBe([2, 4])
        ->and($window->from())->toBe('04:30')
        ->and($window->to())->toBe('05:30');
});

it('needs the capability to change when the installation updates itself', function () {
    Livewire::actingAs(operator('Support'), 'operator')
        ->test(AdminSettings::class)
        ->call('saveUpdateWindow')
        ->assertForbidden();
});
