<?php

use App\Livewire\ConfirmDisableTwoFactor;
use App\Livewire\Settings as PortalSettings;
use App\Models\Customer;
use Livewire\Livewire;
use PragmaRX\Google2FA\Google2FA;

/**
 * Customers setting up two-factor themselves.
 *
 * The endpoints existed; only the screen was missing. 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.
 */

function portalUser(): App\Models\User
{
    $customer = Customer::factory()->create(['status' => 'active']);
    $user = $customer->ensureUser();
    $user->forceFill(['password' => bcrypt('password')])->save();

    return $user;
}

it('refuses every two-factor action without a recent password confirmation', function () {
    $user = portalUser();

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

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

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

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

    $page->call('enableTwoFactor');
    expect($user->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($user->two_factor_confirmed_at)->toBeNull();

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

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

    expect($user->refresh()->two_factor_confirmed_at)->not->toBeNull()
        // Shown once, here, because this is the only moment anyone reads them.
        ->and($page->get('recoveryCodes'))->toBeArray()->not->toBeEmpty();
});

it('rejects a wrong code instead of switching it on', function () {
    $user = portalUser();

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

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

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

    Livewire::actingAs($user)
        ->test(PortalSettings::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.
    $user = portalUser();

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

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

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

// R23: confirmation moved from wire:confirm to ConfirmDisableTwoFactor, which
// dispatches back to Settings::disableTwoFactor() instead of mutating
// anything itself.

it('confirms disabling two-factor through the modal without turning it off itself', function () {
    $user = portalUser();

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

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

    Livewire::actingAs($user)
        ->test(ConfirmDisableTwoFactor::class)
        ->call('confirm')
        ->assertDispatched('twofa-disable-confirmed');

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

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

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

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

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

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