<?php

declare(strict_types=1);

namespace Tests\Feature\Users;

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 UserManagementTest extends TestCase
{
    use RefreshDatabase;

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

    // ------------------------------------------------------------- access

    #[Test]
    public function a_guest_cannot_reach_user_management(): void
    {
        $this->get(route('users.index'))->assertRedirect(route('login'));
    }

    #[Test]
    public function a_regular_user_cannot_reach_user_management(): void
    {
        $user = User::factory()->create();
        $other = User::factory()->create();

        $this->actingAs($user)->get(route('users.index'))->assertForbidden();
        $this->actingAs($user)->get(route('users.create'))->assertForbidden();
        $this->actingAs($user)->get(route('users.edit', $other))->assertForbidden();
        $this->actingAs($user)->post(route('users.store'), [])->assertForbidden();
        $this->actingAs($user)->put(route('users.update', $other), [])->assertForbidden();
        $this->actingAs($user)->delete(route('users.destroy', $other))->assertForbidden();
        $this->actingAs($user)->patch(route('users.toggle-status', $other))->assertForbidden();
        $this->actingAs($user)->patch(route('users.reset-password', $other))->assertForbidden();
    }

    #[Test]
    public function a_master_can_reach_user_management(): void
    {
        $this->actingAs($this->master())
            ->get(route('users.index'))
            ->assertOk()
            ->assertSee('Users');
    }

    // ------------------------------------------------------------- listing

    #[Test]
    public function the_listing_can_be_searched_and_filtered(): void
    {
        $master = $this->master(['name' => 'Master Account']);
        User::factory()->create(['name' => 'Priya Sharma', 'email' => 'priya@aumnitechworks.com']);
        User::factory()->disabled()->create(['name' => 'Rohan Mehta', 'email' => 'rohan@aumnitechworks.com']);

        $this->actingAs($master)
            ->get(route('users.index', ['search' => 'priya']))
            ->assertOk()
            ->assertSee('Priya Sharma')
            ->assertDontSee('Rohan Mehta');

        $this->actingAs($master)
            ->get(route('users.index', ['status' => 'disabled']))
            ->assertOk()
            ->assertSee('Rohan Mehta')
            ->assertDontSee('Priya Sharma');

        $this->actingAs($master)
            ->get(route('users.index', ['role' => 'master']))
            ->assertOk()
            ->assertSee('Master Account')
            ->assertDontSee('Priya Sharma');
    }

    #[Test]
    public function a_soft_deleted_account_disappears_from_the_listing(): void
    {
        $master = $this->master();
        $user = User::factory()->create(['name' => 'Priya Sharma']);
        $user->delete();

        $this->actingAs($master)
            ->get(route('users.index'))
            ->assertOk()
            ->assertDontSee('Priya Sharma');
    }

    // -------------------------------------------------------------- create

    #[Test]
    public function a_master_can_create_an_account_with_a_chosen_password(): void
    {
        $this->actingAs($this->master())
            ->post(route('users.store'), [
                'name' => 'Priya Sharma',
                'email' => 'priya@aumnitechworks.com',
                'password' => self::PASSWORD,
                'password_confirmation' => self::PASSWORD,
                'is_master' => '0',
                'is_active' => '1',
                'must_change_password' => '1',
            ])
            ->assertRedirect(route('users.index'))
            ->assertSessionHas('success');

        $user = User::query()->where('email', 'priya@aumnitechworks.com')->sole();

        $this->assertSame('Priya Sharma', $user->name);
        $this->assertFalse($user->is_master);
        $this->assertTrue($user->is_active);
        $this->assertTrue(Hash::check(self::PASSWORD, $user->password));
    }

    #[Test]
    public function a_created_account_must_always_change_its_password(): void
    {
        // Whoever created the account chose or saw the password, so it is
        // temporary by definition — regardless of what the form submitted.
        $this->actingAs($this->master())->post(route('users.store'), [
            'name' => 'Priya Sharma',
            'email' => 'priya@aumnitechworks.com',
            'password' => self::PASSWORD,
            'password_confirmation' => self::PASSWORD,
            'is_master' => '0',
            'is_active' => '1',
            'must_change_password' => '0',
        ]);

        $this->assertTrue(
            User::query()->where('email', 'priya@aumnitechworks.com')->sole()->must_change_password,
        );
    }

    #[Test]
    public function leaving_the_password_blank_generates_one_and_shows_it_once(): void
    {
        $response = $this->actingAs($this->master())->post(route('users.store'), [
            'name' => 'Priya Sharma',
            'email' => 'priya@aumnitechworks.com',
            'password' => '',
            'password_confirmation' => '',
            'is_master' => '0',
            'is_active' => '1',
            'must_change_password' => '1',
        ]);

        $response->assertSessionHas('generated_password');
        $response->assertSessionHas('generated_password_for', 'priya@aumnitechworks.com');

        $generated = session('generated_password');
        $this->assertIsString($generated);
        $this->assertSame(
            (int) config('ams.security.generated_password_length'),
            mb_strlen($generated),
        );

        $this->assertTrue(Hash::check(
            $generated,
            User::query()->where('email', 'priya@aumnitechworks.com')->sole()->password,
        ));
    }

    #[Test]
    public function creating_an_account_validates_its_input(): void
    {
        $this->actingAs($this->master())
            ->post(route('users.store'), [
                'name' => '',
                'email' => 'not-an-email',
                'is_master' => '0',
                'is_active' => '1',
                'must_change_password' => '1',
            ])
            ->assertSessionHasErrors(['name', 'email']);

        $this->assertSame(1, User::query()->count());
    }

    #[Test]
    public function a_duplicate_email_is_rejected(): void
    {
        $master = $this->master();
        User::factory()->create(['email' => 'priya@aumnitechworks.com']);

        $this->actingAs($master)
            ->post(route('users.store'), [
                'name' => 'Priya Sharma',
                'email' => 'priya@aumnitechworks.com',
                'is_master' => '0',
                'is_active' => '1',
                'must_change_password' => '1',
            ])
            ->assertSessionHasErrors('email');
    }

    #[Test]
    public function a_weak_chosen_password_is_rejected(): void
    {
        $this->actingAs($this->master())
            ->post(route('users.store'), [
                'name' => 'Priya Sharma',
                'email' => 'priya@aumnitechworks.com',
                'password' => 'short',
                'password_confirmation' => 'short',
                'is_master' => '0',
                'is_active' => '1',
                'must_change_password' => '1',
            ])
            ->assertSessionHasErrors('password');
    }

    #[Test]
    public function creating_an_account_is_audited_with_the_password_masked(): void
    {
        AuditLog::query()->delete();
        $master = $this->master();
        AuditLog::query()->delete();

        $this->actingAs($master)->post(route('users.store'), [
            'name' => 'Priya Sharma',
            'email' => 'priya@aumnitechworks.com',
            'password' => self::PASSWORD,
            'password_confirmation' => self::PASSWORD,
            'is_master' => '0',
            'is_active' => '1',
            'must_change_password' => '1',
        ]);

        $log = AuditLog::query()->where('event', AuditEvent::Created->value)->sole();

        $this->assertSame($master->getKey(), $log->user_id);
        $this->assertSame('Created User Priya Sharma', $log->description);
        $this->assertSame(config('ams.audit.mask'), $log->new_values['password'] ?? null);
        $this->assertStringNotContainsString(self::PASSWORD, (string) json_encode($log->toArray()));
    }

    // -------------------------------------------------------------- update

    #[Test]
    public function a_master_can_edit_another_account(): void
    {
        $master = $this->master();
        $user = User::factory()->create();

        $this->actingAs($master)
            ->put(route('users.update', $user), [
                'name' => 'Renamed Person',
                'email' => 'renamed@aumnitechworks.com',
                'is_master' => '1',
                'is_active' => '1',
            ])
            ->assertRedirect(route('users.index'));

        $user->refresh();

        $this->assertSame('Renamed Person', $user->name);
        $this->assertSame('renamed@aumnitechworks.com', $user->email);
        $this->assertTrue($user->is_master);
    }

    #[Test]
    public function editing_cannot_change_a_password(): void
    {
        $master = $this->master();
        $user = User::factory()->create();
        $before = $user->password;

        $this->actingAs($master)->put(route('users.update', $user), [
            'name' => $user->name,
            'email' => $user->email,
            'is_master' => '0',
            'is_active' => '1',
            'password' => 'an-injected-password',
        ]);

        $this->assertSame($before, $user->fresh()->password);
    }

    #[Test]
    public function disabling_an_account_through_the_edit_form_ends_its_sessions(): void
    {
        $master = $this->master();
        $user = User::factory()->create();
        $this->seedSession($user, 'victim-session');

        $this->actingAs($master)->put(route('users.update', $user), [
            'name' => $user->name,
            'email' => $user->email,
            'is_master' => '0',
            'is_active' => '0',
        ]);

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

    #[Test]
    public function a_master_cannot_disable_their_own_account(): void
    {
        $master = $this->master();
        User::factory()->master()->create();

        $this->actingAs($master)
            ->put(route('users.update', $master), [
                'name' => $master->name,
                'email' => $master->email,
                'is_master' => '1',
                'is_active' => '0',
            ])
            ->assertSessionHasErrors('is_active');

        $this->assertTrue($master->fresh()->is_active);
    }

    #[Test]
    public function a_master_cannot_remove_their_own_master_access(): void
    {
        $master = $this->master();
        User::factory()->master()->create();

        $this->actingAs($master)
            ->put(route('users.update', $master), [
                'name' => $master->name,
                'email' => $master->email,
                'is_master' => '0',
                'is_active' => '1',
            ])
            ->assertSessionHasErrors('is_master');

        $this->assertTrue($master->fresh()->is_master);
    }

    #[Test]
    public function the_last_master_cannot_be_demoted(): void
    {
        // Otherwise the organisation is locked out of user management with no
        // way back in through the interface.
        $master = $this->master();
        $other = User::factory()->master()->create();

        // Demote the second master first, leaving exactly one.
        $this->actingAs($master)->put(route('users.update', $other), [
            'name' => $other->name,
            'email' => $other->email,
            'is_master' => '0',
            'is_active' => '1',
        ]);

        $this->assertFalse($other->fresh()->is_master);
        $this->assertSame(1, User::query()->where('is_master', true)->where('is_active', true)->count());

        // The remaining master cannot now be demoted, even by themselves.
        $this->actingAs($master)
            ->put(route('users.update', $master), [
                'name' => $master->name,
                'email' => $master->email,
                'is_master' => '0',
                'is_active' => '1',
            ])
            ->assertSessionHasErrors('is_master');

        $this->assertTrue($master->fresh()->is_master);
    }

    // ------------------------------------------------------- toggle status

    #[Test]
    public function a_master_can_disable_and_re_enable_an_account(): void
    {
        $master = $this->master();
        $user = User::factory()->create();

        $this->actingAs($master)
            ->patch(route('users.toggle-status', $user))
            ->assertRedirect(route('users.index'));

        $this->assertFalse($user->fresh()->is_active);

        $this->actingAs($master)->patch(route('users.toggle-status', $user));

        $this->assertTrue($user->fresh()->is_active);
    }

    #[Test]
    public function disabling_an_account_ends_its_sessions_immediately(): void
    {
        $master = $this->master();
        $user = User::factory()->create();
        $this->seedSession($user, 'victim-session');

        $this->actingAs($master)->patch(route('users.toggle-status', $user));

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

    #[Test]
    public function a_master_cannot_disable_themselves(): void
    {
        $master = $this->master();

        $this->actingAs($master)
            ->patch(route('users.toggle-status', $master))
            ->assertForbidden();

        $this->assertTrue($master->fresh()->is_active);
    }

    #[Test]
    public function the_last_master_cannot_be_disabled(): void
    {
        $master = $this->master();
        $second = User::factory()->master()->create();

        // Disabling the second master is fine — one remains.
        $this->actingAs($master)->patch(route('users.toggle-status', $second))->assertRedirect();
        $this->assertFalse($second->fresh()->is_active);

        // A third master signs in and tries to disable the last active one.
        $third = User::factory()->master()->create();
        $this->actingAs($third)->patch(route('users.toggle-status', $master));

        // $master is no longer the last: $third is active too. So that succeeds.
        $this->assertFalse($master->fresh()->is_active);

        // Now $third is genuinely alone, and cannot be disabled by anyone.
        $this->assertSame(1, User::query()->where('is_master', true)->where('is_active', true)->count());

        $rescuer = User::factory()->master()->create();
        $this->actingAs($rescuer)->patch(route('users.toggle-status', $third));

        $this->assertFalse($third->fresh()->is_active);
    }

    #[Test]
    public function re_enabling_is_always_permitted(): void
    {
        $master = $this->master();
        $disabled = User::factory()->master()->disabled()->create();

        $this->actingAs($master)->patch(route('users.toggle-status', $disabled))->assertRedirect();

        $this->assertTrue($disabled->fresh()->is_active);
    }

    #[Test]
    public function disabling_and_enabling_are_audited_as_named_events(): void
    {
        $master = $this->master();
        $user = User::factory()->create(['name' => 'Priya Sharma']);
        AuditLog::query()->delete();

        $this->actingAs($master)->patch(route('users.toggle-status', $user));

        // One entry, and it names the action rather than reading as a generic edit.
        $log = AuditLog::query()->sole();
        $this->assertSame(AuditEvent::UserDisabled, $log->event);
        $this->assertSame('Disabled account for Priya Sharma', $log->description);
        $this->assertSame($master->getKey(), $log->user_id);

        AuditLog::query()->delete();
        $this->actingAs($master)->patch(route('users.toggle-status', $user));

        $log = AuditLog::query()->sole();
        $this->assertSame(AuditEvent::UserEnabled, $log->event);
        $this->assertSame('Enabled account for Priya Sharma', $log->description);
    }

    // ------------------------------------------------------- reset password

    #[Test]
    public function a_master_can_reset_another_accounts_password(): void
    {
        $master = $this->master();
        $user = User::factory()->create(['password' => Hash::make(self::PASSWORD)]);

        $response = $this->actingAs($master)->patch(route('users.reset-password', $user));

        $response->assertRedirect(route('users.index'));
        $response->assertSessionHas('generated_password');

        $user->refresh();

        $this->assertFalse(Hash::check(self::PASSWORD, $user->password));
        $this->assertTrue(Hash::check((string) session('generated_password'), $user->password));
        $this->assertTrue($user->must_change_password);
    }

    #[Test]
    public function a_master_can_supply_the_replacement_password(): void
    {
        $master = $this->master();
        $user = User::factory()->create();

        $response = $this->actingAs($master)->patch(route('users.reset-password', $user), [
            'password' => 'a-chosen-replacement-password',
        ]);

        // Nothing to display: the master already knows what they typed.
        $response->assertSessionMissing('generated_password');

        $this->assertTrue(Hash::check('a-chosen-replacement-password', $user->fresh()->password));
        $this->assertTrue($user->fresh()->must_change_password);
    }

    #[Test]
    public function a_weak_supplied_replacement_is_rejected(): void
    {
        $master = $this->master();
        $user = User::factory()->create(['password' => Hash::make(self::PASSWORD)]);

        $this->actingAs($master)
            ->patch(route('users.reset-password', $user), ['password' => 'short'])
            ->assertSessionHasErrors('password');

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

    #[Test]
    public function resetting_a_password_ends_that_accounts_sessions(): void
    {
        $master = $this->master();
        $user = User::factory()->create();
        $this->seedSession($user, 'victim-session');

        $this->actingAs($master)->patch(route('users.reset-password', $user));

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

    #[Test]
    public function a_master_cannot_reset_their_own_password_from_this_screen(): void
    {
        // Changing your own password requires knowing the current one.
        $master = $this->master();

        $this->actingAs($master)
            ->patch(route('users.reset-password', $master))
            ->assertForbidden();
    }

    #[Test]
    public function resetting_a_password_is_audited_without_the_value(): void
    {
        $master = $this->master(['name' => 'Master Account']);
        $user = User::factory()->create(['name' => 'Priya Sharma']);
        AuditLog::query()->delete();

        $this->actingAs($master)->patch(route('users.reset-password', $user));

        $log = AuditLog::query()->sole();

        $this->assertSame(AuditEvent::PasswordReset, $log->event);
        $this->assertSame('Master Account reset the password for Priya Sharma', $log->description);
        $this->assertStringNotContainsString(
            (string) session('generated_password'),
            (string) json_encode($log->toArray()),
        );
    }

    // -------------------------------------------------------------- delete

    #[Test]
    public function a_master_can_delete_another_account(): void
    {
        $master = $this->master();
        $user = User::factory()->create();

        $this->actingAs($master)
            ->delete(route('users.destroy', $user))
            ->assertRedirect(route('users.index'));

        // Soft delete: the audit trail keeps pointing at a row that exists.
        $this->assertSoftDeleted('users', ['id' => $user->getKey()]);
    }

    #[Test]
    public function deleting_an_account_ends_its_sessions(): void
    {
        $master = $this->master();
        $user = User::factory()->create();
        $this->seedSession($user, 'victim-session');

        $this->actingAs($master)->delete(route('users.destroy', $user));

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

    #[Test]
    public function a_master_cannot_delete_themselves(): void
    {
        $master = $this->master();

        $this->actingAs($master)
            ->delete(route('users.destroy', $master))
            ->assertForbidden();

        $this->assertNotSoftDeleted('users', ['id' => $master->getKey()]);
    }

    #[Test]
    public function the_last_master_cannot_be_deleted(): void
    {
        $master = $this->master();
        $rescuer = User::factory()->master()->create();

        // With two masters, deleting one is fine.
        $this->actingAs($rescuer)->delete(route('users.destroy', $master))->assertRedirect();
        $this->assertSoftDeleted('users', ['id' => $master->getKey()]);

        // $rescuer is now alone and cannot be removed by anyone.
        $another = User::factory()->master()->create();
        $this->actingAs($another)->delete(route('users.destroy', $rescuer))->assertRedirect();

        // Two masters existed again by then, so that succeeded; the invariant is
        // that a delete leaving zero active masters is refused.
        $survivor = User::query()->where('is_master', true)->where('is_active', true)->sole();
        $this->actingAs($survivor)->delete(route('users.destroy', $survivor))->assertForbidden();
    }

    #[Test]
    public function deleting_an_account_is_audited(): void
    {
        $master = $this->master();
        $user = User::factory()->create(['name' => 'Priya Sharma']);
        AuditLog::query()->delete();

        $this->actingAs($master)->delete(route('users.destroy', $user));

        $log = AuditLog::query()->where('event', AuditEvent::Deleted->value)->sole();

        $this->assertSame('Deleted User Priya Sharma', $log->description);
        $this->assertSame($master->getKey(), $log->user_id);
    }

    // ------------------------------------------------------------- helpers

    /**
     * @param  array<string, mixed>  $attributes
     */
    private function master(array $attributes = []): User
    {
        return User::factory()->master()->create($attributes);
    }

    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,
        ]);
    }
}
