<?php

use App\Livewire\Admin\Settings;
use App\Models\Customer;
use App\Models\Operator;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;

// operator() helper defined in RbacTest.

it('updates the operator own account', function () {
    $owner = operator('Owner');

    Livewire::actingAs($owner, 'operator')->test(Settings::class)
        ->set('name', 'New Name')->set('email', 'new@ops.test')->call('saveAccount')->assertHasNoErrors();

    expect($owner->fresh()->name)->toBe('New Name')->and($owner->fresh()->email)->toBe('new@ops.test');
});

it('will not save an account email that collides with an orphaned portal login', function () {
    // A users row with no matching customers row — an email changed on one
    // side only, or legacy data. Customer::where('email', ...) alone cannot
    // see this; the identity still lives in `users`.
    User::factory()->create(['email' => 'orphan-account@ops.test']);

    Livewire::actingAs(operator('Owner'), 'operator')->test(Settings::class)
        ->set('email', 'orphan-account@ops.test')->call('saveAccount')
        ->assertHasErrors(['email']);
});

// Was beim Einladen mit dem PASSWORT passiert — es entsteht keines, das
// jemand weitergeben könnte, und der Link geht per Mail hinaus — steht in
// tests/Feature/Auth/OperatorInvitationTest.php. Hier bleibt nur, was diese
// Seite selbst zusichert: wer einladen darf, mit welcher Rolle, und gegen
// welche Adressen sie sich wehrt.
it('lets an Owner invite staff with a role', function () {
    Mail::fake();

    Livewire::actingAs(operator('Owner'), 'operator')->test(Settings::class)
        ->set('staffName', 'Sam')->set('staffEmail', 'sam@ops.test')->set('staffRole', 'Support')
        ->call('inviteStaff')->assertHasNoErrors();

    $sam = Operator::query()->where('email', 'sam@ops.test')->first();
    expect($sam)->not->toBeNull()->and($sam->hasRole('Support'))->toBeTrue();
});

it('forbids a non-Owner from managing staff', function () {
    Livewire::actingAs(operator('Admin'), 'operator')->test(Settings::class)
        ->set('staffName', 'X')->set('staffEmail', 'x@ops.test')->set('staffRole', 'Support')
        ->call('inviteStaff')->assertForbidden();
    expect(Operator::query()->where('email', 'x@ops.test')->exists())->toBeFalse();
});

it('will not create an operator from a customer email', function () {
    Customer::factory()->create(['email' => 'dup@ops.test']);

    Livewire::actingAs(operator('Owner'), 'operator')->test(Settings::class)
        ->set('staffName', 'Dup')->set('staffEmail', 'dup@ops.test')->set('staffRole', 'Support')
        ->call('inviteStaff')->assertHasErrors(['staffEmail']);
});

it('will not create an operator from an orphaned portal login', function () {
    // Same gap as the account-save case above, at the staff-invite site.
    User::factory()->create(['email' => 'orphan-invite@ops.test']);

    Livewire::actingAs(operator('Owner'), 'operator')->test(Settings::class)
        ->set('staffName', 'Orphan')->set('staffEmail', 'orphan-invite@ops.test')->set('staffRole', 'Support')
        ->call('inviteStaff')->assertHasErrors(['staffEmail']);

    expect(Operator::query()->where('email', 'orphan-invite@ops.test')->exists())->toBeFalse();
});

it('will not escalate a non-staff user via a tampered id', function () {
    // The old risk was one table shared by both groups: a customer's numeric
    // id could be handed to setStaffRole() and land on that same row. That is
    // structurally closed now — setStaffRole() only ever queries `operators`,
    // a table this customer is not in — so the regression to guard against is
    // that no operator anywhere picks up the 'Admin' role as a side effect.
    $adminsBefore = Operator::role('Admin')->count();
    $victim = User::factory()->create(); // a plain (customer) user, no role

    Livewire::actingAs(operator('Owner'), 'operator')->test(Settings::class)->call('setStaffRole', $victim->id, 'Admin');

    expect(Operator::role('Admin')->count())->toBe($adminsBefore);
});

it('forbids a non-Owner from resending an invitation', function () {
    // Same capability as inviting in the first place — a resend is a token
    // for an account nobody has used yet, and issuing one is not a lesser
    // action than creating the account was.
    Mail::fake();

    Livewire::actingAs(operator('Owner'), 'operator')->test(Settings::class)
        ->set('staffName', 'Sam')->set('staffEmail', 'sam@ops.test')->set('staffRole', 'Support')
        ->call('inviteStaff')->assertHasNoErrors();

    $sam = Operator::query()->where('email', 'sam@ops.test')->firstOrFail();

    Livewire::actingAs(operator('Admin'), 'operator')->test(Settings::class)
        ->call('resendInvitation', $sam->id)->assertForbidden();
});

it('protects the last owner and self-role', function () {
    $owner = operator('Owner');
    $support = operator('Support');

    // Cannot change own role.
    Livewire::actingAs($owner, 'operator')->test(Settings::class)->call('setStaffRole', $owner->id, 'Admin');
    expect($owner->fresh()->hasRole('Owner'))->toBeTrue();

    // Only one owner → cannot revoke it.
    Livewire::actingAs($owner, 'operator')->test(Settings::class)->call('revokeStaff', $owner->id);
    expect($owner->fresh()->hasRole('Owner'))->toBeTrue();

    // Owner can change another operator's role.
    Livewire::actingAs($owner, 'operator')->test(Settings::class)->call('setStaffRole', $support->id, 'Billing');
    expect($support->fresh()->hasRole('Billing'))->toBeTrue()->and($support->fresh()->hasRole('Support'))->toBeFalse();
});
