<?php

namespace Tests\Feature;

use App\Livewire\Modals\ConfirmAction;
use App\Livewire\Settings\Sessions;
use App\Models\AuditEvent;
use App\Models\User;
use App\Services\SessionService;
use App\Support\Confirm\ConfirmToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Livewire\Livewire;
use Tests\TestCase;

class SessionManagementTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        // phpunit.xml pins SESSION_DRIVER=array for the suite; the session-management
        // feature only exists with the database driver (rows in the `sessions` table).
        config(['session.driver' => 'database']);
    }

    /** Insert a raw session row for $user; returns the generated id. */
    private function seedSession(User $user, string $id, ?string $ip = '203.0.113.1', ?string $ua = 'Mozilla/5.0 Chrome', int $ago = 0): string
    {
        DB::table('sessions')->insert([
            'id' => $id,
            'user_id' => $user->id,
            'ip_address' => $ip,
            'user_agent' => $ua,
            'payload' => base64_encode(serialize([])),
            'last_activity' => now()->subSeconds($ago)->getTimestamp(),
        ]);

        return $id;
    }

    public function test_for_user_returns_rows_with_current_flagged(): void
    {
        $user = User::factory()->create();
        $other = User::factory()->create();

        $currentId = Session::getId();
        $this->seedSession($user, $currentId, ago: 0);
        $this->seedSession($user, 'sess-old', ago: 600);
        $this->seedSession($other, 'sess-other'); // must NOT appear

        $rows = app(SessionService::class)->forUser($user);

        $this->assertCount(2, $rows);
        // Newest activity first.
        $this->assertSame($currentId, $rows[0]['id']);
        $this->assertTrue($rows[0]['isCurrent']);
        $this->assertSame('sess-old', $rows[1]['id']);
        $this->assertFalse($rows[1]['isCurrent']);
        $this->assertSame('203.0.113.1', $rows[0]['ip']);
        $this->assertStringContainsString('Chrome', $rows[0]['userAgentShort']);
        $this->assertTrue($rows[0]['lastActivity']->isToday());
    }

    public function test_logout_other_devices_deletes_others_keeps_current(): void
    {
        $user = User::factory()->create();
        $currentId = Session::getId();
        $this->seedSession($user, $currentId);
        $this->seedSession($user, 'sess-b');
        $this->seedSession($user, 'sess-c');

        app(SessionService::class)->logoutOtherDevices($user);

        $this->assertDatabaseHas('sessions', ['id' => $currentId]);
        $this->assertDatabaseMissing('sessions', ['id' => 'sess-b']);
        $this->assertDatabaseMissing('sessions', ['id' => 'sess-c']);
    }

    public function test_logout_user_everywhere_deletes_all_and_rotates_token(): void
    {
        $user = User::factory()->create(['remember_token' => 'original-token']);
        $this->seedSession($user, Session::getId());
        $this->seedSession($user, 'sess-b');

        app(SessionService::class)->logoutUserEverywhere($user);

        $this->assertSame(0, DB::table('sessions')->where('user_id', $user->id)->count());
        $this->assertNotSame('original-token', $user->fresh()->remember_token);
        $this->assertNotNull($user->fresh()->remember_token);
    }

    public function test_logout_everyone_truncates_and_rotates_all_tokens(): void
    {
        $a = User::factory()->create(['remember_token' => 'token-a']);
        $b = User::factory()->create(['remember_token' => 'token-b']);
        $this->seedSession($a, 'sess-a');
        $this->seedSession($b, 'sess-b');

        app(SessionService::class)->logoutEveryone();

        $this->assertSame(0, DB::table('sessions')->count());
        $this->assertNotSame('token-a', $a->fresh()->remember_token);
        $this->assertNotSame('token-b', $b->fresh()->remember_token);
    }

    public function test_component_lists_the_users_sessions(): void
    {
        $user = User::factory()->create();
        $this->seedSession($user, Session::getId());

        Livewire::actingAs($user)->test(Sessions::class)
            ->assertOk()
            ->assertViewHas('sessions', fn ($sessions) => count($sessions) === 1);
    }

    public function test_logout_others_action_confirms_then_deletes(): void
    {
        $user = User::factory()->create();
        $current = Session::getId();
        $this->seedSession($user, $current);
        $this->seedSession($user, 'sess-other');

        // The button opens the R5 confirm modal carrying the apply event + audit descriptor.
        Livewire::actingAs($user)->test(Sessions::class)
            ->call('confirmLogoutOthers')
            ->assertDispatched('openModal');

        // The confirm modal re-dispatches the apply event with the signed token; simulate that.
        $this->actingAs($user);
        $token = ConfirmToken::issue('sessionsLogoutOthers');
        ConfirmToken::confirm($token);
        Livewire::actingAs($user)->test(Sessions::class)
            ->call('logoutOthers', $token);

        $this->assertDatabaseHas('sessions', ['id' => $current]);
        $this->assertDatabaseMissing('sessions', ['id' => 'sess-other']);
    }

    public function test_logout_all_action_truncates_and_redirects_to_login(): void
    {
        $user = User::factory()->create(['remember_token' => 'tok']);
        $this->seedSession($user, Session::getId());

        Livewire::actingAs($user)->test(Sessions::class)
            ->call('confirmLogoutAll')
            ->assertDispatched('openModal');

        $this->actingAs($user);
        $token = ConfirmToken::issue('sessionsLogoutAll');
        ConfirmToken::confirm($token);
        Livewire::actingAs($user)->test(Sessions::class)
            ->call('logoutAll', $token)
            ->assertRedirect(route('login'));

        $this->assertSame(0, DB::table('sessions')->count());
        $this->assertNotSame('tok', $user->fresh()->remember_token);
    }

    public function test_confirm_modal_writes_audit_row_on_confirm(): void
    {
        // The generic confirm modal persists the audit row from the SEALED descriptor
        // in the signed token (mirrors Settings\Security). Exercise it directly.
        $user = User::factory()->create();
        $this->actingAs($user);

        $token = ConfirmToken::issue('sessionsLogoutOthers', [], 'session.logout_others', $user->email);

        Livewire::actingAs($user)->test(ConfirmAction::class, ['token' => $token])->call('confirm');

        $this->assertDatabaseHas('audit_events', [
            'action' => 'session.logout_others',
            'target' => $user->email,
        ]);
        $this->assertSame(1, AuditEvent::where('action', 'session.logout_others')->count());
    }
}
