<?php

namespace Tests\Feature\Audit;

use App\Domain\Applicants\Enums\ApplicantStatus;
use App\Domain\Applicants\Models\Applicant;
use App\Domain\Audit\Models\Activity;
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 Illuminate\Support\Facades\RateLimiter;
use RuntimeException;
use Tests\TestCase;

class ActivityLogTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);
        $this->withHeader('Origin', config('app.url'));

        $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);
    }

    private function applicant(array $attributes = []): Applicant
    {
        return Applicant::factory()->for($this->company)->create(array_merge([
            'branch_id' => $this->branch->id,
        ], $attributes));
    }

    public function test_activity_records_cannot_be_modified_or_deleted(): void
    {
        $activity = Activity::create([
            'company_id' => $this->company->id,
            'event_category' => 'system',
            'event_action' => 'test.event',
            'event_title' => 'Test',
            'created_at' => now(),
        ]);

        // Append-only is enforced in code, not merely by convention (§65).
        $this->expectException(RuntimeException::class);
        $activity->update(['event_title' => 'Rewritten']);
    }

    public function test_an_activity_record_cannot_be_deleted(): void
    {
        $activity = Activity::create([
            'company_id' => $this->company->id,
            'event_category' => 'system',
            'event_action' => 'test.event',
            'event_title' => 'Test',
            'created_at' => now(),
        ]);

        $this->expectException(RuntimeException::class);
        $activity->delete();
    }

    public function test_creating_an_applicant_is_logged_in_human_terms(): void
    {
        $manager = $this->userWithRole('recruitment-manager');

        $this->actingAs($manager)->postJson('/api/v1/applicants', [
            'full_name' => 'Abdul Karim',
            'mobile' => '01712345678',
        ])->assertCreated();

        $activity = Activity::where('event_action', 'applicant.created')->firstOrFail();

        $this->assertSame('Applicant profile created', $activity->event_title);
        $this->assertStringContainsString('Abdul Karim', $activity->event_description);
        $this->assertSame($manager->id, $activity->actor_id);
        // The name is snapshotted so a rename does not rewrite history.
        $this->assertSame($manager->name, $activity->actor_name_snapshot);
        $this->assertSame('recruitment-manager', $activity->actor_role_snapshot);
    }

    public function test_a_status_change_reads_as_names_not_identifiers(): void
    {
        $applicant = $this->applicant(['status' => ApplicantStatus::Registered->value]);

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/status", [
                'status' => ApplicantStatus::InProcess->value,
            ])->assertOk();

        $activity = Activity::where('event_action', 'applicant.status_changed')->firstOrFail();

        $this->assertSame('Applicant status changed', $activity->event_title);
        $this->assertSame('Registered → In Process', $activity->event_description);
        // The raw values are still there underneath for audit.
        $this->assertSame('registered', $activity->old_values['status']);
        $this->assertSame('in_process', $activity->new_values['status']);
    }

    public function test_identity_numbers_are_masked_in_the_log(): void
    {
        $applicant = $this->applicant(['national_id' => '1990111222']);

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->patchJson("/api/v1/applicants/{$applicant->uuid}", [
                'national_id' => '1990999888',
            ])->assertOk();

        $activity = Activity::where('event_action', 'applicant.updated')->firstOrFail();

        // Reading history must not hand someone the identity number.
        $this->assertSame('19******88', $activity->new_values['national_id']);
        $this->assertSame('19******22', $activity->old_values['national_id']);
        $this->assertStringNotContainsString('1990999888', json_encode($activity->toArray()));
    }

    public function test_a_change_is_described_with_field_labels(): void
    {
        $applicant = $this->applicant(['mobile' => '01711111111']);

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->patchJson("/api/v1/applicants/{$applicant->uuid}", [
                'mobile' => '01822222222',
            ])->assertOk();

        $activity = Activity::where('event_action', 'applicant.updated')->firstOrFail();

        $this->assertSame('Phone number: 01711111111 → 01822222222', $activity->event_description);
    }

    public function test_an_update_that_changes_nothing_writes_no_entry(): void
    {
        $applicant = $this->applicant(['occupation' => 'Driver']);

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->patchJson("/api/v1/applicants/{$applicant->uuid}", ['occupation' => 'Driver'])
            ->assertOk();

        $this->assertSame(0, Activity::where('event_action', 'applicant.updated')->count());
    }

    public function test_sign_in_and_failed_sign_in_are_both_recorded(): void
    {
        RateLimiter::clear('auditor@mams.test|127.0.0.1');

        $user = $this->userWithRole('recruitment-manager', [
            'email' => 'auditor@mams.test',
            'password' => Hash::make('secret-password'),
        ]);

        $this->postJson('/api/v1/auth/login', [
            'email' => 'auditor@mams.test',
            'password' => 'wrong',
        ])->assertStatus(422);

        $this->postJson('/api/v1/auth/login', [
            'email' => 'auditor@mams.test',
            'password' => 'secret-password',
        ])->assertOk();

        $this->assertSame(1, Activity::where('event_action', 'auth.login_failed')->count());
        $this->assertSame(1, Activity::where('event_action', 'auth.login')->count());

        // The attempted password is never written anywhere.
        $failed = Activity::where('event_action', 'auth.login_failed')->firstOrFail();
        $this->assertStringNotContainsString('wrong', json_encode($failed->toArray()));
        $this->assertSame($user->email, $failed->event_description);
    }

    public function test_the_applicant_timeline_gathers_events_from_child_records(): void
    {
        $applicant = $this->applicant(['status' => ApplicantStatus::Registered->value]);
        $manager = $this->userWithRole('recruitment-manager');

        $this->actingAs($manager)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/status", [
                'status' => ApplicantStatus::InProcess->value,
            ])->assertOk();

        $response = $this->actingAs($manager)
            ->getJson("/api/v1/applicants/{$applicant->uuid}/activities")
            ->assertOk();

        $this->assertGreaterThanOrEqual(1, count($response->json('data')));
        $this->assertSame('Applicant status changed', $response->json('data.0.title'));
    }

    public function test_the_technical_diff_is_only_shown_to_auditors(): void
    {
        $applicant = $this->applicant(['mobile' => '01711111111']);

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->patchJson("/api/v1/applicants/{$applicant->uuid}", ['mobile' => '01822222222'])
            ->assertOk();

        // Data entry officers can read the file but hold no audit permission.
        $plain = $this->actingAs($this->userWithRole('data-entry-officer'))
            ->getJson("/api/v1/applicants/{$applicant->uuid}/activities")
            ->assertOk();

        $this->assertArrayNotHasKey('changes', $plain->json('data.0'));
        $this->assertArrayNotHasKey('ip_address', $plain->json('data.0'));

        $auditor = $this->actingAs($this->userWithRole('auditor'))
            ->getJson("/api/v1/applicants/{$applicant->uuid}/activities")
            ->assertOk();

        $this->assertNotEmpty($auditor->json('data.0.changes'));
        $this->assertSame('mobile', $auditor->json('data.0.changes.0.field'));
    }

    public function test_security_events_are_hidden_from_operational_timelines(): void
    {
        $applicant = $this->applicant();

        Activity::create([
            'company_id' => $this->company->id,
            'event_category' => 'security',
            'event_action' => 'document.downloaded',
            'event_title' => 'Sensitive document downloaded',
            'subject_type' => $applicant->getMorphClass(),
            'subject_id' => $applicant->id,
            'created_at' => now(),
        ]);

        $plain = $this->actingAs($this->userWithRole('data-entry-officer'))
            ->getJson("/api/v1/applicants/{$applicant->uuid}/activities")
            ->assertOk();

        $this->assertCount(0, $plain->json('data'));

        $auditor = $this->actingAs($this->userWithRole('auditor'))
            ->getJson("/api/v1/applicants/{$applicant->uuid}/activities")
            ->assertOk();

        $this->assertCount(1, $auditor->json('data'));
    }

    public function test_the_global_feed_needs_audit_permission_and_supports_filters(): void
    {
        $applicant = $this->applicant(['status' => ApplicantStatus::Registered->value]);
        $manager = $this->userWithRole('recruitment-manager');

        $this->actingAs($manager)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/status", [
                'status' => ApplicantStatus::InProcess->value,
            ])->assertOk();

        $this->actingAs($manager)->getJson('/api/v1/activities')->assertStatus(403);

        $auditor = $this->userWithRole('auditor');

        $this->actingAs($auditor)->getJson('/api/v1/activities')
            ->assertOk()
            ->assertJsonStructure(['meta' => ['categories']]);

        $this->actingAs($auditor)->getJson('/api/v1/activities?category=status')
            ->assertOk()
            ->assertJsonPath('data.0.category', 'status');

        $this->actingAs($auditor)->getJson('/api/v1/activities?category=payroll')
            ->assertOk()
            ->assertJsonCount(0, 'data');
    }

    public function test_failed_sign_ins_reach_the_auditor_feed(): void
    {
        RateLimiter::clear('known@mams.test|127.0.0.1');

        $this->userWithRole('recruitment-manager', [
            'email' => 'known@mams.test',
            'password' => Hash::make('secret-password'),
        ]);

        // A known address, and an address nobody holds.
        $this->postJson('/api/v1/auth/login', ['email' => 'known@mams.test', 'password' => 'x'])
            ->assertStatus(422);
        $this->postJson('/api/v1/auth/login', ['email' => 'stranger@nowhere.test', 'password' => 'x'])
            ->assertStatus(422);

        $response = $this->actingAs($this->userWithRole('auditor'))
            ->getJson('/api/v1/activities?category=security')
            ->assertOk();

        $descriptions = collect($response->json('data'))->pluck('description');

        $this->assertContains('known@mams.test', $descriptions);
        // An attempt against an unknown address has no company, and must still
        // be visible rather than silently dropped.
        $this->assertContains('stranger@nowhere.test', $descriptions);
    }

    public function test_the_feed_does_not_cross_company_boundaries(): void
    {
        Activity::create([
            'company_id' => Company::factory()->create()->id,
            'event_category' => 'profile',
            'event_action' => 'applicant.created',
            'event_title' => 'Someone Else Was Created',
            'created_at' => now(),
        ]);

        $content = $this->actingAs($this->userWithRole('auditor'))
            ->getJson('/api/v1/activities')
            ->assertOk()
            ->getContent();

        $this->assertStringNotContainsString('Someone Else', $content);
    }
}
