<?php

declare(strict_types=1);

namespace Tests\Feature\Auth;

use App\Enums\AuditEvent;
use App\Models\AuditLog;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class PasswordChangeTest extends TestCase
{
    use RefreshDatabase;

    private const CURRENT = 'correct-horse-battery';

    private const REPLACEMENT = 'a-different-strong-password';

    #[Test]
    public function the_change_screen_requires_a_session(): void
    {
        $this->get(route('password.change'))->assertRedirect(route('login'));
    }

    #[Test]
    public function a_user_can_change_their_own_password(): void
    {
        $user = $this->makeUser();

        $this->actingAs($user)
            ->put(route('password.update'), [
                'current_password' => self::CURRENT,
                'password' => self::REPLACEMENT,
                'password_confirmation' => self::REPLACEMENT,
            ])
            ->assertRedirect(config('ams.auth.home_path'))
            ->assertSessionHas('success');

        $this->assertTrue(Hash::check(self::REPLACEMENT, $user->fresh()->password));
    }

    #[Test]
    public function the_current_password_must_be_correct(): void
    {
        $user = $this->makeUser();

        $this->actingAs($user)
            ->put(route('password.update'), [
                'current_password' => 'not-the-current-password',
                'password' => self::REPLACEMENT,
                'password_confirmation' => self::REPLACEMENT,
            ])
            ->assertSessionHasErrors('current_password');

        $this->assertTrue(Hash::check(self::CURRENT, $user->fresh()->password));
    }

    #[Test]
    public function the_confirmation_must_match(): void
    {
        $user = $this->makeUser();

        $this->actingAs($user)
            ->put(route('password.update'), [
                'current_password' => self::CURRENT,
                'password' => self::REPLACEMENT,
                'password_confirmation' => 'something-else-entirely',
            ])
            ->assertSessionHasErrors('password');

        $this->assertTrue(Hash::check(self::CURRENT, $user->fresh()->password));
    }

    #[Test]
    public function the_new_password_must_meet_the_minimum_length(): void
    {
        $user = $this->makeUser();

        $this->actingAs($user)
            ->put(route('password.update'), [
                'current_password' => self::CURRENT,
                'password' => 'short',
                'password_confirmation' => 'short',
            ])
            ->assertSessionHasErrors('password');
    }

    #[Test]
    public function the_new_password_must_differ_from_the_current_one(): void
    {
        $user = $this->makeUser();

        $this->actingAs($user)
            ->put(route('password.update'), [
                'current_password' => self::CURRENT,
                'password' => self::CURRENT,
                'password_confirmation' => self::CURRENT,
            ])
            ->assertSessionHasErrors('password');
    }

    #[Test]
    public function changing_a_password_is_audited_once_and_masks_the_value(): void
    {
        $user = $this->makeUser(['name' => 'Priya Sharma']);
        AuditLog::query()->delete();

        $this->actingAs($user)->put(route('password.update'), [
            'current_password' => self::CURRENT,
            'password' => self::REPLACEMENT,
            'password_confirmation' => self::REPLACEMENT,
        ]);

        // Exactly one entry: the explicit event, not also a generic "updated".
        $log = AuditLog::query()->sole();

        $this->assertSame(AuditEvent::PasswordChanged, $log->event);
        $this->assertSame('Priya Sharma changed their password', $log->description);
        $this->assertStringNotContainsString(
            self::REPLACEMENT,
            (string) json_encode($log->toArray()),
        );
    }

    #[Test]
    public function the_password_changed_timestamp_is_recorded(): void
    {
        $user = $this->makeUser(['password_changed_at' => now()->subYear()]);

        $this->actingAs($user)->put(route('password.update'), [
            'current_password' => self::CURRENT,
            'password' => self::REPLACEMENT,
            'password_confirmation' => self::REPLACEMENT,
        ]);

        $this->assertTrue($user->fresh()->password_changed_at->isToday());
    }

    // ---------------------------------------------------------------- forced

    #[Test]
    public function a_forced_change_does_not_ask_for_the_current_password(): void
    {
        // The master who issued the temporary password chose it, so asking the
        // user to repeat it adds friction without adding security.
        $user = $this->makeUser(['must_change_password' => true]);

        $this->actingAs($user)
            ->put(route('password.update'), [
                'password' => self::REPLACEMENT,
                'password_confirmation' => self::REPLACEMENT,
            ])
            ->assertRedirect(config('ams.auth.home_path'));

        $this->assertTrue(Hash::check(self::REPLACEMENT, $user->fresh()->password));
    }

    #[Test]
    public function a_completed_forced_change_clears_the_flag(): void
    {
        $user = $this->makeUser(['must_change_password' => true]);

        $this->actingAs($user)->put(route('password.update'), [
            'password' => self::REPLACEMENT,
            'password_confirmation' => self::REPLACEMENT,
        ]);

        $this->assertFalse($user->fresh()->must_change_password);
    }

    #[Test]
    public function a_forced_user_is_bounced_back_from_every_other_screen(): void
    {
        $user = $this->makeUser(['must_change_password' => true]);

        $this->actingAs($user)
            ->get(route('dashboard'))
            ->assertRedirect(config('ams.auth.password_change_path'));

        $this->actingAs($user)
            ->get(route('profile.edit'))
            ->assertRedirect(config('ams.auth.password_change_path'));
    }

    #[Test]
    public function a_forced_user_can_still_reach_the_change_screen_and_sign_out(): void
    {
        $user = $this->makeUser(['must_change_password' => true]);

        $this->actingAs($user)->get(route('password.change'))->assertOk();

        $this->actingAs($user)->post(route('logout'))->assertRedirect(config('ams.auth.login_path'));
        $this->assertGuest();
    }

    #[Test]
    public function the_forced_screen_renders_without_the_application_shell(): void
    {
        $user = $this->makeUser(['must_change_password' => true]);

        $response = $this->actingAs($user)->get(route('password.change'));

        $response->assertOk();
        $response->assertSee('Choose a new password');
        // No sidebar: there is nowhere else to go until this is done.
        $response->assertDontSee('id="amsSidebar"', escape: false);
    }

    #[Test]
    public function the_voluntary_screen_renders_inside_the_application_shell(): void
    {
        $response = $this->actingAs($this->makeUser())->get(route('password.change'));

        $response->assertOk();
        $response->assertSee('Current Password');
        $response->assertSee('id="amsSidebar"', escape: false);
    }

    // -------------------------------------------------------------- sessions

    #[Test]
    public function changing_a_password_destroys_this_accounts_other_sessions(): void
    {
        $user = $this->makeUser();

        $this->seedSession($user, 'another-device-session');

        $this->actingAs($user)->put(route('password.update'), [
            'current_password' => self::CURRENT,
            'password' => self::REPLACEMENT,
            'password_confirmation' => self::REPLACEMENT,
        ]);

        $this->assertDatabaseMissing('sessions', ['id' => 'another-device-session']);
    }

    #[Test]
    public function changing_a_password_leaves_other_accounts_sessions_alone(): void
    {
        $user = $this->makeUser();
        $colleague = $this->makeUser();

        $this->seedSession($colleague, 'colleague-session');

        $this->actingAs($user)->put(route('password.update'), [
            'current_password' => self::CURRENT,
            'password' => self::REPLACEMENT,
            'password_confirmation' => self::REPLACEMENT,
        ]);

        $this->assertDatabaseHas('sessions', ['id' => 'colleague-session']);
    }

    /**
     * @param  array<string, mixed>  $attributes
     */
    private function makeUser(array $attributes = []): User
    {
        return User::factory()->create(array_merge([
            'password' => Hash::make(self::CURRENT),
        ], $attributes));
    }

    /**
     * Insert a session row directly, standing in for a sign-in on another device.
     */
    private function seedSession(User $user, string $id): void
    {
        DB::table('sessions')->insert([
            'id' => $id,
            'user_id' => $user->getKey(),
            'ip_address' => '10.0.0.1',
            'user_agent' => 'PHPUnit',
            'payload' => base64_encode('test'),
            'last_activity' => now()->timestamp,
        ]);
    }
}
