<?php

use App\Livewire\Sessions as PortalSessions;
use App\Models\LoginSession;
use App\Models\User;
use App\Models\UserDevice;
use App\Services\Devices\SessionRegistry;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;

/**
 * Seeing where an account is signed in, and ending one of those places.
 *
 * The scoping tests are the point. Operators and customers are two guards and
 * two tables (R21), so operator 1 and customer 1 share an integer and nothing
 * else — an unscoped query here would let either of them sign the other out,
 * and it would look exactly like a working feature while doing it.
 */
function makeSession(string $guard, int $id, string $sessionId, string $device = 'Chrome auf macOS', ?int $lastActivity = null): LoginSession
{
    $deviceRow = UserDevice::create([
        'guard' => $guard,
        'authenticatable_id' => $id,
        'token_hash' => hash('sha256', Str::random(40)),
        'name' => $device,
        'last_seen_at' => now(),
    ]);

    DB::table('sessions')->insert([
        'id' => $sessionId,
        'ip_address' => '203.0.113.10',
        'user_agent' => 'test',
        'payload' => '',
        'last_activity' => $lastActivity ?? now()->getTimestamp(),
    ]);

    return LoginSession::create([
        'device_id' => $deviceRow->id,
        'session_id' => $sessionId,
        'ip_address' => '203.0.113.10',
        'last_seen_at' => now(),
    ]);
}

it('lists the sessions of one identity and marks the current one', function () {
    $user = User::factory()->create();
    $here = makeSession('web', $user->id, 'session-here', 'Chrome auf macOS');
    makeSession('web', $user->id, 'session-there', 'Safari auf iPhone');

    $rows = app(SessionRegistry::class)->for('web', $user->id, $here->uuid);

    expect($rows)->toHaveCount(2)
        ->and($rows->firstWhere('uuid', $here->uuid)->is_current)->toBeTrue()
        ->and($rows->firstWhere('device', 'Safari auf iPhone')->is_current)->toBeFalse();
});

it('does not show a customer the sessions of the operator with the same id', function () {
    $user = User::factory()->create();
    makeSession('web', $user->id, 'session-customer');
    makeSession('operator', $user->id, 'session-operator', 'Firefox auf Linux');

    $rows = app(SessionRegistry::class)->for('web', $user->id);

    expect($rows)->toHaveCount(1)
        ->and($rows->first()->device)->not->toBe('Firefox auf Linux');
});

it('refuses to end a session that belongs to the other identity', function () {
    $user = User::factory()->create();
    $operatorSession = makeSession('operator', $user->id, 'session-operator');

    $ended = app(SessionRegistry::class)->end('web', $user->id, $operatorSession->uuid);

    expect($ended)->toBeFalse()
        ->and(DB::table('sessions')->where('id', 'session-operator')->exists())->toBeTrue();
});

it('ending a session removes the framework row, which is what signs the browser out', function () {
    $user = User::factory()->create();
    $other = makeSession('web', $user->id, 'session-there');

    expect(app(SessionRegistry::class)->end('web', $user->id, $other->uuid))->toBeTrue()
        ->and(DB::table('sessions')->where('id', 'session-there')->exists())->toBeFalse()
        ->and(LoginSession::query()->where('uuid', $other->uuid)->exists())->toBeFalse();
});

it('leaves the session doing the asking alone when ending the others', function () {
    // Signing somebody out of the page they are using to sign other people out
    // is not what the button says, and leaves them unable to see whether it
    // worked.
    $user = User::factory()->create();
    $here = makeSession('web', $user->id, 'session-here');
    makeSession('web', $user->id, 'session-b');
    makeSession('web', $user->id, 'session-c');

    $ended = app(SessionRegistry::class)->endOthers('web', $user->id, $here->uuid);

    expect($ended)->toBe(2)
        ->and(DB::table('sessions')->where('id', 'session-here')->exists())->toBeTrue()
        ->and(DB::table('sessions')->count())->toBe(1);
});

it('hides a session whose framework row has expired but not yet been collected', function () {
    // Laravel collects expired sessions by lottery, so on a quiet installation
    // rows sit there for days. Listing them offers somebody a laptop they shut
    // last month as somewhere they are signed in right now.
    $user = User::factory()->create();
    makeSession('web', $user->id, 'session-stale', 'Chrome auf Windows',
        lastActivity: now()->subMinutes((int) config('session.lifetime') + 5)->getTimestamp());

    expect(app(SessionRegistry::class)->for('web', $user->id))->toHaveCount(0);
});

it('lets a customer end another session from the settings page', function () {
    $user = User::factory()->create();
    $other = makeSession('web', $user->id, 'session-there', 'Safari auf iPhone');

    Livewire::actingAs($user)
        ->test(PortalSessions::class)
        ->assertSee('Safari auf iPhone')
        ->call('end', $other->uuid)
        ->assertDispatched('notify', message: __('sessions.ended'));

    expect(DB::table('sessions')->where('id', 'session-there')->exists())->toBeFalse();
});

it('ends the others only when the confirmation modal says so', function () {
    // R23: the destructive one goes through the product's own modal, which
    // mutates nothing and raises an event this component catches — so the
    // identity resolution stays at the one place that already had it.
    $user = User::factory()->create();
    makeSession('web', $user->id, 'session-b');
    makeSession('web', $user->id, 'session-c');

    Livewire::actingAs($user)
        ->test(PortalSessions::class)
        ->dispatch('sessions-end-others-confirmed')
        ->assertDispatched('notify');

    expect(DB::table('sessions')->count())->toBe(0);
});
