<?php

use App\Http\Middleware\RestrictConsoleNetwork;
use App\Livewire\Admin\Settings as AdminSettings;
use App\Models\User;
use App\Support\Settings;
use Livewire\Livewire;

/**
 * Who may reach the console, by network address.
 *
 * The hostname guard answers under which NAME the console responds, and the
 * caller picks the Host header — so it can never answer who is asking. This
 * can, and the owner keeps the list themselves.
 */
/** Livewire's test harness has no withServerVariables(); set it on the request. */
function fromIp(string $ip): void
{
    request()->server->set('REMOTE_ADDR', $ip);
}

beforeEach(function () {
    config()->set('admin_access.hosts', []);
    config()->set('admin_access.trusted_ranges', ['10.66.0.0/24']);
    Settings::set('console.network_restricted', false);
    Settings::set('console.allowed_ips', []);
});

it('changes nothing until the owner switches it on', function () {
    $this->actingAs(operator('Owner'), 'operator')
        ->withServerVariables(['REMOTE_ADDR' => '203.0.113.9'])
        ->get('/admin')
        ->assertOk();
});

it('lets the management VPN in and turns everyone else away', function () {
    Settings::set('console.network_restricted', true);

    $this->actingAs(operator('Owner'), 'operator')
        ->withServerVariables(['REMOTE_ADDR' => '10.66.0.5'])
        ->get('/admin')
        ->assertOk();

    // 404, never 403: a stranger must not learn a console lives here.
    $this->actingAs(operator('Owner'), 'operator')
        ->withServerVariables(['REMOTE_ADDR' => '203.0.113.9'])
        ->get('/admin')
        ->assertNotFound();
});

it('lets in an address the owner has listed, without the VPN', function () {
    Settings::set('console.network_restricted', true);
    Settings::set('console.allowed_ips', ['203.0.113.0/24']);

    $this->actingAs(operator('Owner'), 'operator')
        ->withServerVariables(['REMOTE_ADDR' => '203.0.113.9'])
        ->get('/admin')
        ->assertOk();

    // A neighbouring range is still nobody.
    $this->actingAs(operator('Owner'), 'operator')
        ->withServerVariables(['REMOTE_ADDR' => '198.51.100.4'])
        ->get('/admin')
        ->assertNotFound();
});

it('knows when a change would lock the owner out', function () {
    // The one check whose failure mode is that nobody can reach the page that
    // would fix it — so it is decided without a request, and tested that way.
    expect(RestrictConsoleNetwork::wouldStillAllow('10.66.0.5', []))->toBeTrue()
        ->and(RestrictConsoleNetwork::wouldStillAllow('198.51.100.4', []))->toBeFalse()
        ->and(RestrictConsoleNetwork::wouldStillAllow('198.51.100.4', ['198.51.100.0/24']))->toBeTrue()
        // Removing the entry you are sitting behind leaves you outside.
        ->and(RestrictConsoleNetwork::wouldStillAllow('203.0.113.9', []))->toBeFalse();
});

it('rejects an entry that is not an address or a range', function () {
    // A typo that silently matches nothing is how someone locks themselves out
    // while believing they have not.
    foreach (['nonsense', '203.0.113.', '203.0.113.9/99', ''] as $bad) {
        Livewire::actingAs(operator('Owner'), 'operator')
            ->test(AdminSettings::class)
            ->set('consoleIp', $bad)
            ->call('addConsoleIp')
            ->assertHasErrors('consoleIp');
    }

    expect(Settings::get('console.allowed_ips'))->toBe([]);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(AdminSettings::class)
        ->set('consoleIp', '203.0.113.0/24')
        ->call('addConsoleIp')
        ->assertHasNoErrors();

    expect(Settings::get('console.allowed_ips'))->toBe(['203.0.113.0/24']);
});

it('needs the capability, not merely an operator account', function () {
    Livewire::actingAs(operator('Support'), 'operator')
        ->test(AdminSettings::class)
        ->call('addConsoleIp')
        ->assertForbidden();
});

it('is re-checked on every Livewire action, not only on the page', function () {
    // The page is not where the actions run.
    $persistent = app(\Livewire\Mechanisms\PersistentMiddleware\PersistentMiddleware::class)
        ->getPersistentMiddleware();

    expect($persistent)->toContain(RestrictConsoleNetwork::class);
});

it('offers a way back in that does not depend on the console', function () {
    // Every gate needs a door that does not depend on itself: the list is
    // managed from the console, and the address you manage it from can change.
    Settings::set('console.network_restricted', true);
    Settings::set('console.allowed_ips', []);

    $this->artisan('clupilot:console-access allow 203.0.113.42')->assertSuccessful();
    expect(Settings::get('console.allowed_ips'))->toBe(['203.0.113.42'])
        ->and(RestrictConsoleNetwork::wouldStillAllow('203.0.113.42', ['203.0.113.42']))->toBeTrue();

    $this->artisan('clupilot:console-access deny 203.0.113.42')->assertSuccessful();
    expect(Settings::get('console.allowed_ips'))->toBe([]);

    // And the restriction itself can be lifted from the shell — the only place
    // that is reachable when the gate has shut on its own owner.
    $this->artisan('clupilot:console-access open')->assertSuccessful();
    expect(RestrictConsoleNetwork::isRestricted())->toBeFalse();
});

it('tells the reverse proxy the same thing it tells itself', function () {
    // The proxy has its own allowlist and it runs FIRST. If the two disagree,
    // everything the owner changes in the console is silently ineffective —
    // and when their address changes they are turned away before reaching the
    // page that would have fixed it.
    Settings::set('console.network_restricted', true);
    Settings::set('console.allowed_ips', ['203.0.113.7']);

    // One assertion on the whole line: the ranges have to arrive together, in
    // the matcher, not merely appear somewhere in the output.
    $this->artisan('clupilot:console-access caddy')
        ->expectsOutputToContain('@allowed remote_ip 10.66.0.0/24 203.0.113.7')
        ->assertSuccessful();
});

it('never opens the proxy to everyone, not even when the restriction is lifted', function () {
    // The rule is absolute: the console is reachable over the VPN or from a
    // listed address, and nowhere else. Letting the console's own switch also
    // open the PROXY would put it on the public internet with one click.
    Settings::set('console.network_restricted', false);
    Settings::set('console.allowed_ips', ['203.0.113.7']);

    $this->artisan('clupilot:console-access caddy')
        ->expectsOutputToContain('@allowed remote_ip 10.66.0.0/24 203.0.113.7')
        ->assertSuccessful();

    $this->artisan('clupilot:console-access caddy')
        ->doesntExpectOutputToContain('0.0.0.0/0')
        ->assertSuccessful();
});

it('never emits a matcher that lets nobody in', function () {
    // An empty remote_ip matcher matches NOTHING in Caddy — the console would
    // be unreachable from everywhere, including from where it gets fixed.
    config()->set('admin_access.trusted_ranges', []);
    Settings::set('console.network_restricted', true);
    Settings::set('console.allowed_ips', []);

    $this->artisan('clupilot:console-access caddy')
        ->expectsOutputToContain('127.0.0.1')
        ->assertSuccessful();
});

it('lets an operator change their own password', function () {
    // There was no way to do it at all: Fortify's updatePasswords feature was
    // off, so an account created with a generated password kept it until
    // someone opened a shell on the server.
    $operator = operator('Owner');

    Livewire::actingAs($operator, 'operator')
        ->test(AdminSettings::class)
        ->set('currentPassword', 'password')
        ->set('newPassword', 'ein-neues-langes-passwort')
        ->set('newPasswordConfirmation', 'ein-neues-langes-passwort')
        ->call('updateOwnPassword')
        ->assertHasNoErrors();

    expect(Illuminate\Support\Facades\Hash::check('ein-neues-langes-passwort', $operator->refresh()->password))->toBeTrue();
});

it('refuses to change a password without the current one', function () {
    // Otherwise an unlocked machine is enough to lock the owner out of their
    // own account with two keystrokes.
    $operator = operator('Owner');

    Livewire::actingAs($operator, 'operator')
        ->test(AdminSettings::class)
        ->set('currentPassword', 'falsch')
        ->set('newPassword', 'ein-neues-langes-passwort')
        ->set('newPasswordConfirmation', 'ein-neues-langes-passwort')
        ->call('updateOwnPassword')
        ->assertHasErrors('currentPassword');

    expect(Illuminate\Support\Facades\Hash::check('password', $operator->refresh()->password))->toBeTrue();
});
