<?php

namespace Tests\Feature;

use App\Models\Document;
use App\Models\DocumentType;
use App\Models\Role;
use App\Services\Documents\DocumentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;

class SecurityTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seedPlatform();
        Storage::fake('private');
    }

    // ------------------------------------------------------- Document access

    public function test_an_applicant_cannot_download_another_applicants_document(): void
    {
        $mine = $this->applicant();
        $document = $this->uploadFor($this->applicant());

        $this->actingAs($mine->user)
            ->get(route('applicant.documents.download', $document))
            ->assertForbidden();

        $this->actingAs($mine->user)
            ->get(route('applicant.documents.preview', $document))
            ->assertForbidden();
    }

    public function test_a_guest_cannot_download_a_document(): void
    {
        $document = $this->uploadFor($this->applicant());

        $this->get(route('applicant.documents.download', $document))
            ->assertRedirect(route('login'));
    }

    public function test_an_applicant_can_download_their_own_document(): void
    {
        $applicant = $this->applicant();
        $document = $this->uploadFor($applicant);

        $this->actingAs($applicant->user)
            ->get(route('applicant.documents.download', $document))
            ->assertOk();
    }

    public function test_staff_with_permission_can_read_a_document_and_the_access_is_logged(): void
    {
        $document = $this->uploadFor($this->applicant());

        $this->actingAs($this->staff(Role::ADMISSIONS_OFFICER))
            ->get(route('applicant.documents.download', $document))
            ->assertOk();

        $this->assertDatabaseHas('audit_logs', ['action' => 'document.downloaded']);
    }

    public function test_staff_without_document_permission_are_refused(): void
    {
        $document = $this->uploadFor($this->applicant());

        $this->actingAs($this->staff(Role::CONTENT_EDITOR))
            ->get(route('applicant.documents.download', $document))
            ->assertForbidden();
    }

    public function test_documents_are_stored_outside_the_public_web_root(): void
    {
        $document = $this->uploadFor($this->applicant());

        $this->assertSame('private', $document->disk);
        $this->assertStringNotContainsString('public', $document->stored_path);

        // The stored name is randomised, so the path cannot be guessed.
        $this->assertStringNotContainsString($document->original_filename, $document->stored_path);
        $this->assertMatchesRegularExpression('#^applications/\d+/documents/[a-z0-9-]+-[A-Za-z0-9]{24}\.\w+$#', $document->stored_path);
    }

    // ------------------------------------------------------- Upload validation

    public function test_an_executable_disguised_as_a_pdf_is_rejected(): void
    {
        $application = $this->startApplication($this->applicant());
        $type = DocumentType::where('slug', 'academic-certificate')->firstOrFail();

        $file = UploadedFile::fake()->createWithContent('payload.pdf', '<?php echo "pwned";');

        $this->expectException(ValidationException::class);

        app(DocumentService::class)->store($application, $type, $file);
    }

    public function test_a_file_with_a_disallowed_extension_is_rejected(): void
    {
        $application = $this->startApplication($this->applicant());
        $type = DocumentType::where('slug', 'academic-certificate')->firstOrFail();

        $this->expectException(ValidationException::class);

        app(DocumentService::class)->store($application, $type, UploadedFile::fake()->create('notes.docx', 100));
    }

    public function test_an_oversized_file_is_rejected(): void
    {
        $application = $this->startApplication($this->applicant());
        $type = DocumentType::where('slug', 'passport-photo')->firstOrFail();

        // The passport photo type caps uploads at 2 MB.
        $file = UploadedFile::fake()->image('huge.jpg')->size(4096);

        $this->expectException(ValidationException::class);

        app(DocumentService::class)->store($application, $type, $file);
    }

    public function test_replacing_a_document_removes_the_previous_file(): void
    {
        $applicant = $this->applicant();
        $application = $this->startApplication($applicant);
        $type = DocumentType::where('slug', 'passport-photo')->firstOrFail();
        $service = app(DocumentService::class);

        $first = $service->store($application, $type, UploadedFile::fake()->image('one.jpg'));
        $firstPath = $first->stored_path;

        $service->store($application->fresh(), $type, UploadedFile::fake()->image('two.jpg'));

        Storage::disk('private')->assertMissing($firstPath);
        $this->assertDatabaseMissing('documents', ['id' => $first->id]);
        $this->assertSame(1, $application->fresh()->documents()->count());
    }

    // ----------------------------------------------------- Application access

    public function test_an_applicant_cannot_open_another_applicants_application(): void
    {
        $mine = $this->applicant();
        $theirs = $this->startApplication($this->applicant());

        $this->actingAs($mine->user)
            ->get(route('applicant.applications.show', $theirs))
            ->assertForbidden();

        $this->actingAs($mine->user)
            ->get(route('applicant.applications.wizard', [$theirs, 'personal']))
            ->assertForbidden();
    }

    public function test_an_applicant_cannot_drive_another_applicants_wizard(): void
    {
        $mine = $this->applicant();
        $theirs = $this->startApplication($this->applicant());

        \Livewire\Livewire::actingAs($mine->user)
            ->test(\App\Livewire\Applicant\PersonalInformationForm::class, ['application' => $theirs])
            ->assertStatus(403);
    }

    // -------------------------------------------------- Privilege escalation

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

        // status and email_verified_at are guarded. Outside production, strict
        // mode turns an attempt to set them into a hard failure rather than a
        // silent no-op, so this can never pass unnoticed.
        foreach (['status' => 'suspended', 'email_verified_at' => null] as $field => $value) {
            try {
                $user->update([$field => $value]);
                $this->fail("Expected [{$field}] to be guarded against mass assignment.");
            } catch (\Illuminate\Database\Eloquent\MassAssignmentException) {
                // Expected.
            }
        }

        $this->assertTrue($user->fresh()->isActive());
        $this->assertNotNull($user->fresh()->email_verified_at);
    }

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

        $this->assertFalse($applicant->user->isStaff());
        $this->assertFalse($applicant->user->hasPermission('applications.approve'));

        $this->actingAs($applicant->user)
            ->get(route('admin.settings.index'))
            ->assertForbidden();
    }

    public function test_an_applicant_cannot_forge_their_application_status(): void
    {
        $applicant = $this->applicant();
        $application = $this->startApplication($applicant);

        // A submitted status can only come from the SubmitApplication action.
        $this->actingAs($applicant->user)
            ->post(route('applicant.applications.store'), [
                'status' => 'admitted',
                'application_number' => 'KCU-2026-999999',
            ]);

        $this->assertSame('draft', $application->fresh()->status->value);
        $this->assertNotSame('KCU-2026-999999', $application->fresh()->application_number);
    }

    public function test_an_applicant_cannot_reach_admin_endpoints_by_posting_directly(): void
    {
        $applicant = $this->applicant();
        $application = $this->startApplication($applicant);

        $this->actingAs($applicant->user)
            ->post(route('admin.applications.status', $application), ['status' => 'admitted'])
            ->assertForbidden();
    }

    // -------------------------------------------------------------- CSRF etc.

    public function test_web_routes_are_protected_by_csrf_and_webhooks_are_excluded(): void
    {
        // Laravel disables the CSRF check while tests run, so the guarantee is
        // asserted against the configured middleware rather than a live request.
        $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
        $web = $kernel->getMiddlewareGroups()['web'];

        $this->assertContains(\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class, $web);

        // Providers post server-to-server and authenticate by signature instead,
        // so the webhook path is registered as never-verified.
        $property = new \ReflectionProperty(
            \Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class,
            'neverVerify',
        );

        $this->assertContains(
            'webhooks/payments/*',
            $property->getValue(),
            'The payment webhook route should be excluded from CSRF verification.',
        );
    }

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

        for ($i = 0; $i < 6; $i++) {
            $response = $this->post('/applicant/login', [
                'email' => $applicant->user->email,
                'password' => 'wrong-password',
            ]);
        }

        $response->assertSessionHasErrors('email');
        $this->assertStringContainsString(
            'Too many login attempts',
            session('errors')->first('email'),
        );
    }

    public function test_the_contact_form_is_rate_limited(): void
    {
        $payload = [
            'name' => 'Spammer',
            'email' => 'spam@example.test',
            'subject' => 'Hello',
            'message' => 'This is a long enough message to pass validation.',
        ];

        for ($i = 0; $i < 7; $i++) {
            $response = $this->post('/contact', $payload);
        }

        $response->assertStatus(429);
    }

    /** Store a real uploaded file against a fresh application. */
    protected function uploadFor(\App\Models\Applicant $applicant): Document
    {
        $application = $this->startApplication($applicant);
        $type = DocumentType::where('slug', 'passport-photo')->firstOrFail();

        return app(DocumentService::class)->store(
            $application,
            $type,
            UploadedFile::fake()->image('photo.jpg', 400, 400),
        );
    }
}
