<?php

use App\Livewire\Admin\ConfirmDisableTwoFactor;
use App\Livewire\Admin\TwoFactorSetup;
use Illuminate\Support\Facades\Auth;
use Livewire\Livewire;
use PragmaRX\Google2FA\Google2FA;

/**
 * Operators setting up their own two-factor, from the console.
 *
 * Mirrors tests/Feature/CustomerTwoFactorTest.php on purpose: Fortify's four
 * actions only ever touch the model they are handed (no guard, no auth()
 * call inside them — confirmed by reading the vendor source), so the same
 * shape App\Livewire\Settings already used for the portal works here
 * unchanged, just pointed at the `operator` guard instead of the default
 * one via ConfirmsPassword::confirmationGuard().
 *
 * Its own component and route (App\Livewire\Admin\TwoFactorSetup,
 * admin.two-factor-setup) rather than a card on Admin\Settings — see
 * RequireOperatorTwoFactor for why: that component also carries staff
 * management, site visibility, network restrictions, account changes, and
 * the two-factor policy switch itself, none of which an unenrolled operator
 * should get for free.
 *
 * What matters here is the gate: every action is re-checked against a
 * recent password confirmation on the server, because a Livewire action is
 * reachable by anyone who can post to /livewire/update, and "the button was
 * not on screen" has never stopped anyone. Uses a non-Owner role throughout
 * — this has to work for every operator, not only the one who can flip the
 * compulsory switch.
 */
it('refuses every two-factor action without a recent password confirmation', function () {
    $op = operator('Support');

    // onDisableConfirmed is the R23 forwarding listener ConfirmDisableTwoFactor
    // reaches — it must lead straight into the same guard as disableTwoFactor,
    // not around it.
    foreach (['enableTwoFactor', 'cancelSetup', 'confirmTwoFactor', 'disableTwoFactor', 'regenerateRecoveryCodes', 'onDisableConfirmed'] as $action) {
        Livewire::actingAs($op, 'operator')
            ->test(TwoFactorSetup::class)
            ->call($action)
            ->assertForbidden();
    }

    expect($op->refresh()->two_factor_secret)->toBeNull();
});

it('takes a password, then sets two-factor up and confirms it with a real code', function () {
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->assertHasNoErrors();

    $page->call('enableTwoFactor');
    expect($op->refresh()->two_factor_secret)->not->toBeNull()
        // Not on until a code has been accepted: someone who scans nothing and
        // closes the tab must not be locked out of their own account.
        ->and($op->two_factor_confirmed_at)->toBeNull();

    $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->two_factor_secret));

    $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();

    expect($op->refresh()->two_factor_confirmed_at)->not->toBeNull()
        ->and($page->get('recoveryCodes'))->toBeArray()->not->toBeEmpty();
});

it('rejects a wrong code instead of switching it on', function () {
    $op = operator('Support');

    Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->call('enableTwoFactor')
        ->set('twoFactorCode', '000000')
        ->call('confirmTwoFactor')
        ->assertHasErrors('twoFactorCode');

    expect($op->refresh()->two_factor_confirmed_at)->toBeNull();
});

it('does not accept a wrong password, and says so without hinting', function () {
    $op = operator('Support');

    Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'falsch')
        ->call('confirmPassword')
        ->assertHasErrors('confirmablePassword')
        ->call('enableTwoFactor')
        ->assertForbidden();
});

it('never puts the secret where the browser can see it', function () {
    // A Livewire property travels to the browser and back in the component
    // snapshot. The QR image is derived from the secret; the secret is not.
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->call('enableTwoFactor');

    $secret = decrypt($op->refresh()->two_factor_secret);

    expect(json_encode($page->snapshot))->not->toContain($secret);
});

it('actually renders the enrolment control on the page, not just in the component', function () {
    // The original gap was exactly this: a component with the right methods
    // but a blade that never called them. Assert on rendered HTML, not
    // component state, so a forgotten wire:click regresses this test too.
    $op = operator('Support');

    // Nothing to enrol with yet — the password gate is what shows.
    $page = Livewire::actingAs($op, 'operator')->test(TwoFactorSetup::class);
    $page->assertSee(__('two_factor_setup.confirm_first'))
        ->assertSee(__('two_factor_setup.state_off'));

    $page->set('confirmablePassword', 'password')->call('confirmPassword');
    $page->assertSee(__('two_factor_setup.enable'));

    $page->call('enableTwoFactor');
    $page->assertSee(__('two_factor_setup.activate'))
        ->assertSee(__('two_factor_setup.code'));

    $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret));
    $page->set('twoFactorCode', $code)->call('confirmTwoFactor');

    $page->assertSee(__('two_factor_setup.state_on'))
        ->assertSee(__('two_factor_setup.codes_title'))
        ->assertSee(__('two_factor_setup.disable'));
});

it('lets an operator regenerate recovery codes and disable two-factor once confirmed', function () {
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->call('enableTwoFactor');

    $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret));
    $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();

    $firstCodes = $op->refresh()->two_factor_recovery_codes;

    $page->call('regenerateRecoveryCodes');
    expect($op->refresh()->two_factor_recovery_codes)->not->toBe($firstCodes);

    $page->call('disableTwoFactor')->assertHasNoErrors();
    expect($op->refresh()->two_factor_confirmed_at)->toBeNull()
        ->and($op->two_factor_secret)->toBeNull();
});

it('confirms disabling two-factor through the modal without turning it off itself', function () {
    // R23: the modal takes no arguments and cannot see whose session this is
    // — it only dispatches back to the page, which still runs
    // disableTwoFactor() with its own password check (see the "refuses every
    // two-factor action" case above, which now covers onDisableConfirmed too).
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->call('enableTwoFactor');

    $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret));
    $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();

    Livewire::actingAs($op, 'operator')
        ->test(ConfirmDisableTwoFactor::class)
        ->call('confirm')
        ->assertDispatched('two-factor-disable-confirmed');

    expect($op->refresh()->two_factor_confirmed_at)->not->toBeNull();
});

it('disables two-factor once the page receives the confirmed event', function () {
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->call('enableTwoFactor');

    $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret));
    $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors();

    $page->call('onDisableConfirmed')->assertHasNoErrors();

    expect($op->refresh()->two_factor_confirmed_at)->toBeNull()
        ->and($op->two_factor_secret)->toBeNull();
});

it('sends the operator to sign in instead of a 500 when the session has ended', function () {
    // render() has no requireConfirmedPassword() gate in front of it — unlike
    // every action above, Livewire calls it on every update including a bare
    // property sync, so it is directly reachable with no other guard in the
    // way once the session that owned the page has ended.
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')->test(TwoFactorSetup::class);

    // The page sat open long enough for the operator's own session to end.
    Auth::guard('operator')->logout();

    $page->call('$refresh')->assertRedirect(route('admin.login'));
});

it('lets an operator back out of a half-finished setup, taking the secret with it', function () {
    // Before cancelSetup() existed, the only way out of the QR step was to
    // navigate away — which left an unconfirmed secret on the account that
    // nothing on the console ever showed.
    $op = operator('Support');

    $page = Livewire::actingAs($op, 'operator')
        ->test(TwoFactorSetup::class)
        ->set('confirmablePassword', 'password')
        ->call('confirmPassword')
        ->call('enableTwoFactor');

    expect($op->refresh()->two_factor_secret)->not->toBeNull();

    $page->call('cancelSetup');

    expect($op->refresh()->two_factor_secret)->toBeNull()
        ->and($op->two_factor_confirmed_at)->toBeNull();
});
