<?php

use App\Mail\NewDeviceSignInMail;
use App\Models\Operator;
use App\Models\User;
use App\Models\UserDevice;
use App\Services\Devices\DeviceRecognition;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

/**
 * A new-device warning is only worth anything if it is rare.
 *
 * A warning that fires on ordinary behaviour trains its reader to delete it,
 * and the one that matters goes with it. Most of these tests are therefore
 * about the cases that must produce NO mail — they are the feature, far more
 * than the case that does.
 */
function signIn(string $guard, $user, array $cookies = [], string $agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/126.0 Safari/537.36', string $ip = '203.0.113.10')
{
    $request = Request::create('/anmelden', 'POST', [], $cookies, [], [
        'HTTP_USER_AGENT' => $agent,
        'REMOTE_ADDR' => $ip,
    ]);

    return app(DeviceRecognition::class)->recognise($user, $guard, $request);
}

it('does not warn about the very first device on an account', function () {
    // Telling somebody they signed in from a new device, in reply to them
    // creating the account, is noise on the one message that must stay worth
    // reading.
    $user = User::factory()->create();

    $outcome = signIn('web', $user);

    expect($outcome->isNew)->toBeTrue()
        ->and($outcome->shouldWarn)->toBeFalse()
        ->and(UserDevice::query()->where('authenticatable_id', $user->id)->count())->toBe(1);
});

it('warns when a second, unknown device appears', function () {
    $user = User::factory()->create();
    signIn('web', $user);                       // the first, silent

    $outcome = signIn('web', $user);            // no cookie: another browser

    expect($outcome->isNew)->toBeTrue()
        ->and($outcome->shouldWarn)->toBeTrue();
});

it('stays quiet when the same device comes back', function () {
    $user = User::factory()->create();
    $first = signIn('web', $user);
    $token = tokenFor($first->device);

    $again = signIn('web', $user, [DeviceRecognition::COOKIE => $token]);

    expect($again->isNew)->toBeFalse()
        ->and($again->shouldWarn)->toBeFalse()
        ->and($again->device->id)->toBe($first->device->id);
});

it('stays quiet when the browser updates itself', function () {
    // The reason the user-agent string is not what identifies a device. Chrome
    // ships every four weeks; recognising by the string would mean twelve
    // warnings a year, per customer, describing nothing.
    $user = User::factory()->create();
    $first = signIn('web', $user, agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/126.0 Safari/537.36');
    $token = tokenFor($first->device);

    $again = signIn('web', $user,
        cookies: [DeviceRecognition::COOKIE => $token],
        agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/141.0 Safari/537.36',
    );

    expect($again->shouldWarn)->toBeFalse()
        ->and($again->device->id)->toBe($first->device->id);
});

it('stays quiet when the address changes, and follows it', function () {
    // The reason the IP is not what identifies a device: it changes on every
    // train, every café and every mobile handover.
    $user = User::factory()->create();
    $first = signIn('web', $user, ip: '203.0.113.10');
    $token = tokenFor($first->device);

    $again = signIn('web', $user, cookies: [DeviceRecognition::COOKIE => $token], ip: '198.51.100.77');

    expect($again->shouldWarn)->toBeFalse()
        // Still recorded, because the list is where somebody checks it — it is
        // only barred from DECIDING anything.
        ->and($again->device->refresh()->last_ip)->toBe('198.51.100.77');
});

it('relabels a device when the browser changes, without treating it as new', function () {
    $user = User::factory()->create();
    $first = signIn('web', $user, agent: 'Mozilla/5.0 (Windows NT 10.0) Chrome/126.0 Safari/537.36');
    $token = tokenFor($first->device);

    expect($first->device->name)->toBe(__('devices.name_on', ['browser' => 'Chrome', 'platform' => 'Windows']));

    $again = signIn('web', $user,
        cookies: [DeviceRecognition::COOKIE => $token],
        agent: 'Mozilla/5.0 (Windows NT 10.0) Firefox/130.0',
    );

    expect($again->shouldWarn)->toBeFalse()
        ->and($again->device->refresh()->name)->toBe(__('devices.name_on', ['browser' => 'Firefox', 'platform' => 'Windows']));
});

it('does not orphan the first person on a shared browser', function () {
    // Two people, one machine. Issuing a fresh token for the second would
    // replace the cookie, leaving the first with a device row nothing matches
    // — and a new-device warning on their own desk the next time they sign in.
    $alice = User::factory()->create();
    $bob = User::factory()->create();

    $aliceFirst = signIn('web', $alice);
    $token = tokenFor($aliceFirst->device);

    signIn('web', $bob, [DeviceRecognition::COOKIE => $token]);
    $aliceAgain = signIn('web', $alice, [DeviceRecognition::COOKIE => $token]);

    expect($aliceAgain->shouldWarn)->toBeFalse()
        ->and($aliceAgain->device->id)->toBe($aliceFirst->device->id)
        ->and(UserDevice::query()->where('token_hash', hash('sha256', $token))->count())->toBe(2);
});

it('keeps operators and customers apart even at the same id', function () {
    // Operator 1 is not customer 1 (R21). A device shared across the two would
    // let one of them silence the other's warning.
    $user = User::factory()->create();
    $operator = Operator::factory()->create();

    $customerDevice = signIn('web', $user);
    $token = tokenFor($customerDevice->device);

    $operatorSignIn = signIn('operator', $operator, [DeviceRecognition::COOKIE => $token]);

    expect($operatorSignIn->device->id)->not->toBe($customerDevice->device->id)
        ->and($operatorSignIn->device->guard)->toBe('operator');
});

it('sends the warning from the console sign-in path too', function () {
    // Both ways in raise the same event, which is the whole reason it is hooked
    // rather than the two controllers.
    Mail::fake();

    $operator = operator('Owner');
    signIn('operator', $operator);                            // first, silent

    event(new Illuminate\Auth\Events\Login('operator', $operator, false));

    Mail::assertQueued(NewDeviceSignInMail::class);
});

it('never blocks a sign-in when recording the device fails', function () {
    // A safeguard that locks the owner out when it breaks has stopped being
    // one. Deliberate, and tested so nobody removes it as an oversight.
    Mail::fake();

    $user = User::factory()->create();

    // A real failure rather than a stubbed one: the class is final on purpose,
    // and a test that replaces it would prove only that the replacement throws.
    // This is the actual shape of the outage — the device store unreachable.
    Illuminate\Support\Facades\Schema::drop('login_sessions');
    Illuminate\Support\Facades\Schema::drop('user_devices');

    event(new Illuminate\Auth\Events\Login('web', $user, false));

    // No throwsNoExceptions() needed: event() propagates whatever a listener
    // throws, so an escaping exception fails this test on its own.
    Mail::assertNothingQueued();
});

/** The plaintext token that was queued into the cookie for a device. */
function tokenFor(UserDevice $device): string
{
    $queued = collect(Illuminate\Support\Facades\Cookie::getQueuedCookies())
        ->first(fn ($c) => $c->getName() === DeviceRecognition::COOKIE);

    expect($queued)->not->toBeNull();

    return $queued->getValue();
}
