<?php

namespace Tests\Feature\Identity;

use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;

class UserManagementTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);

        $this->company = Company::factory()->create();
        $this->branch = Branch::factory()->for($this->company)->create();
    }

    private function userWithRole(string $role, array $attributes = []): User
    {
        return tap(User::factory()->create(array_merge([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ], $attributes)))->assignRole($role);
    }

    public function test_an_administrator_can_create_a_staff_account(): void
    {
        $response = $this->actingAs($this->userWithRole('super-admin'))
            ->postJson('/api/v1/users', [
                'name' => 'Nadia Islam',
                'email' => 'nadia@asiatictalents.test',
                'roles' => ['document-verification-officer'],
            ])
            ->assertCreated();

        $this->assertNotEmpty($response->json('data.temporary_password'));

        $created = User::where('email', 'nadia@asiatictalents.test')->firstOrFail();

        // An administrator never chooses someone else's password.
        $this->assertTrue($created->must_change_password);
        $this->assertSame('staff', $created->user_type);
        $this->assertSame($this->company->id, $created->company_id);
        $this->assertTrue($created->hasRole('document-verification-officer'));
    }

    public function test_an_account_must_be_created_with_at_least_one_role(): void
    {
        $this->actingAs($this->userWithRole('super-admin'))
            ->postJson('/api/v1/users', [
                'name' => 'Roleless',
                'email' => 'roleless@asiatictalents.test',
                'roles' => [],
            ])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['roles']]);
    }

    public function test_nobody_can_grant_a_role_carrying_permissions_they_lack(): void
    {
        // HR managers may create users, but hold nothing like super-admin.
        $hr = $this->userWithRole('hr-manager');

        $this->actingAs($hr)
            ->postJson('/api/v1/users', [
                'name' => 'Escalated',
                'email' => 'escalated@asiatictalents.test',
                'roles' => ['super-admin'],
            ])
            ->assertStatus(403);

        $this->assertNull(User::where('email', 'escalated@asiatictalents.test')->first());
    }

    public function test_a_role_within_the_granter_permissions_is_allowed(): void
    {
        $this->actingAs($this->userWithRole('super-admin'))
            ->postJson('/api/v1/users', [
                'name' => 'Officer',
                'email' => 'officer@asiatictalents.test',
                'roles' => ['cashier'],
            ])
            ->assertCreated();
    }

    public function test_suspending_requires_a_reason_and_cannot_be_done_to_oneself(): void
    {
        $admin = $this->userWithRole('super-admin');
        $target = $this->userWithRole('cashier');

        $this->actingAs($admin)
            ->postJson("/api/v1/users/{$target->uuid}/status", ['status' => 'suspended'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['reason']]);

        $this->actingAs($admin)
            ->postJson("/api/v1/users/{$target->uuid}/status", [
                'status' => 'suspended',
                'reason' => 'Left the company at short notice.',
            ])
            ->assertOk()
            ->assertJsonPath('data.status', 'suspended');

        // Suspending yourself locks you out of the system you administer.
        $this->actingAs($admin)
            ->postJson("/api/v1/users/{$admin->uuid}/status", [
                'status' => 'suspended',
                'reason' => 'Testing',
            ])
            ->assertStatus(403);
    }

    public function test_a_suspended_user_cannot_sign_in(): void
    {
        $admin = $this->userWithRole('super-admin');
        $target = $this->userWithRole('cashier', [
            'email' => 'leaver@asiatictalents.test',
            'password' => Hash::make('secret-password'),
        ]);

        $this->actingAs($admin)->postJson("/api/v1/users/{$target->uuid}/status", [
            'status' => 'suspended',
            'reason' => 'Left the company.',
        ])->assertOk();

        $this->withHeader('Origin', config('app.url'))
            ->postJson('/api/v1/auth/login', [
                'email' => 'leaver@asiatictalents.test',
                'password' => 'secret-password',
            ])
            ->assertStatus(403);
    }

    public function test_a_password_reset_issues_a_one_time_password(): void
    {
        $admin = $this->userWithRole('super-admin');
        $target = $this->userWithRole('cashier', ['password' => Hash::make('old-password')]);

        $response = $this->actingAs($admin)
            ->postJson("/api/v1/users/{$target->uuid}/reset-password")
            ->assertOk();

        $password = $response->json('data.temporary_password');
        $this->assertNotEmpty($password);

        $target->refresh();
        $this->assertTrue($target->must_change_password);
        $this->assertTrue(Hash::check($password, $target->password));
        $this->assertFalse(Hash::check('old-password', $target->password));
    }

    public function test_roles_can_be_changed_but_never_on_your_own_account(): void
    {
        $admin = $this->userWithRole('super-admin');
        $target = $this->userWithRole('cashier');

        $this->actingAs($admin)
            ->postJson("/api/v1/users/{$target->uuid}/roles", ['roles' => ['accounts-officer']])
            ->assertOk();

        $this->assertTrue($target->fresh()->hasRole('accounts-officer'));
        $this->assertFalse($target->fresh()->hasRole('cashier'));

        // Nobody edits their own roles, however senior.
        $this->actingAs($admin)
            ->postJson("/api/v1/users/{$admin->uuid}/roles", ['roles' => ['auditor']])
            ->assertStatus(403);
    }

    public function test_a_user_cannot_be_left_without_any_role(): void
    {
        $admin = $this->userWithRole('super-admin');
        $target = $this->userWithRole('cashier');

        $this->actingAs($admin)
            ->postJson("/api/v1/users/{$target->uuid}/roles", ['roles' => []])
            ->assertStatus(422);
    }

    public function test_account_changes_are_written_to_the_activity_log(): void
    {
        $admin = $this->userWithRole('super-admin');

        $this->actingAs($admin)->postJson('/api/v1/users', [
            'name' => 'Logged User',
            'email' => 'logged@asiatictalents.test',
            'roles' => ['cashier'],
        ])->assertCreated();

        $this->assertDatabaseHas('activities', [
            'event_action' => 'user.created',
            'event_category' => 'security',
            'actor_id' => $admin->id,
        ]);
    }

    public function test_the_listing_is_limited_to_staff_within_scope(): void
    {
        $this->userWithRole('cashier');
        User::factory()->create(['user_type' => 'staff']); // another company
        User::factory()->create([
            'company_id' => $this->company->id,
            'user_type' => 'applicant',
        ]);

        // HR managers hold user.view at company scope, which is what is being
        // tested here; a super admin holds it globally and would see everyone.
        $response = $this->actingAs($this->userWithRole('hr-manager'))
            ->getJson('/api/v1/users')
            ->assertOk();

        $emails = collect($response->json('data'))->pluck('email');

        // Staff of this company only; applicant portal logins are not staff.
        $this->assertCount(2, $emails);
    }

    public function test_user_administration_requires_permission(): void
    {
        $this->getJson('/api/v1/users')->assertStatus(401);

        // Cashiers hold no user permissions.
        $this->actingAs($this->userWithRole('cashier'))
            ->getJson('/api/v1/users')
            ->assertStatus(403);
    }
}
