<?php

namespace Tests\Feature;

use App\Actions\Admissions\AdmitApplicant;
use App\Enums\ApplicationStatus;
use App\Enums\VerificationStatus;
use App\Models\Application;
use App\Models\DocumentType;
use App\Models\GeneratedDocument;
use App\Models\Programme;
use App\Notifications\ApplicationAdmitted;
use App\Services\Admissions\ApplicationStatusService;
use App\Services\Payments\PaymentService;
use App\Services\Pdf\DocumentGenerator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;

class AdmissionsTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_the_status_machine_allows_a_legal_transition(): void
    {
        $application = $this->submittedApplication();
        $officer = $this->staff('admissions-officer');

        app(ApplicationStatusService::class)->transition(
            $application, ApplicationStatus::UnderReview, $officer, 'Starting review'
        );

        $application->refresh();

        $this->assertSame(ApplicationStatus::UnderReview, $application->status);
        $this->assertNotNull($application->reviewed_at);
        $this->assertSame($officer->id, $application->reviewed_by);

        $this->assertDatabaseHas('application_status_history', [
            'application_id' => $application->id,
            'from_status' => 'submitted',
            'to_status' => 'under_review',
            'changed_by' => $officer->id,
        ]);
    }

    public function test_the_status_machine_refuses_an_illegal_transition(): void
    {
        $application = $this->submittedApplication();

        $this->expectException(\RuntimeException::class);

        app(ApplicationStatusService::class)->transition(
            $application, ApplicationStatus::Admitted, $this->staff('admissions-manager')
        );
    }

    public function test_every_transition_is_written_to_the_audit_log(): void
    {
        $application = $this->submittedApplication();

        app(ApplicationStatusService::class)->transition(
            $application, ApplicationStatus::UnderReview, $this->staff('admissions-officer')
        );

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

    public function test_history_and_audit_rows_are_timestamped(): void
    {
        $application = $this->submittedApplication();

        app(ApplicationStatusService::class)->transition(
            $application, ApplicationStatus::UnderReview, $this->staff('admissions-officer')
        );

        // Both tables set UPDATED_AT = null, so created_at must still be stamped
        // by Eloquent rather than silently left empty.
        $this->assertSame(0, \App\Models\ApplicationStatusHistory::whereNull('created_at')->count());
        $this->assertSame(0, \App\Models\AuditLog::whereNull('created_at')->count());

        $this->assertNotNull(
            \App\Models\ApplicationStatusHistory::latest('id')->first()->created_at
        );
    }

    public function test_a_recommended_applicant_can_be_admitted(): void
    {
        Notification::fake();
        Storage::fake('private');

        $application = $this->recommendedApplication();
        $manager = $this->staff('admissions-manager');

        $record = app(AdmitApplicant::class)->execute($application, $manager);

        $application->refresh();

        $this->assertSame(ApplicationStatus::Admitted, $application->status);
        $this->assertMatchesRegularExpression('#^KCU/\d{4}/\d{6}$#', $record->admission_number);
        $this->assertSame($manager->id, $record->admitted_by);
        $this->assertNotEmpty($record->verification_code);
        $this->assertNotNull($record->acceptance_deadline);

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

    public function test_admission_generates_the_letter_and_joining_instructions(): void
    {
        Notification::fake();
        Storage::fake('private');

        $application = $this->recommendedApplication();

        app(AdmitApplicant::class)->execute($application, $this->staff('admissions-manager'));

        $documents = $application->fresh()->generatedDocuments;

        $this->assertCount(2, $documents);
        $this->assertTrue($documents->contains('document_type', GeneratedDocument::TYPE_ADMISSION_LETTER));
        $this->assertTrue($documents->contains('document_type', GeneratedDocument::TYPE_JOINING_INSTRUCTIONS));

        foreach ($documents as $document) {
            Storage::disk('private')->assertExists($document->storage_path);
            $this->assertSame('private', $document->disk);
        }
    }

    public function test_the_applicant_is_told_they_have_been_admitted(): void
    {
        Notification::fake();
        Storage::fake('private');

        $application = $this->recommendedApplication();

        app(AdmitApplicant::class)->execute($application, $this->staff('admissions-manager'));

        Notification::assertSentTo($application->applicant->user, ApplicationAdmitted::class);
    }

    public function test_an_applicant_cannot_be_admitted_twice(): void
    {
        Notification::fake();
        Storage::fake('private');

        $application = $this->recommendedApplication();
        $manager = $this->staff('admissions-manager');

        app(AdmitApplicant::class)->execute($application, $manager);

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

        app(AdmitApplicant::class)->execute($application->fresh(), $manager);
    }

    public function test_admission_numbers_are_unique_and_sequential(): void
    {
        Notification::fake();
        Storage::fake('private');

        $manager = $this->staff('admissions-manager');

        $numbers = collect(range(1, 3))->map(function () use ($manager) {
            return app(AdmitApplicant::class)
                ->execute($this->recommendedApplication(), $manager)
                ->admission_number;
        });

        $this->assertCount(3, $numbers->unique());
        $this->assertSame('KCU/'.now()->year.'/000001', $numbers->first());
    }

    public function test_regenerating_a_letter_creates_a_new_version(): void
    {
        Notification::fake();
        Storage::fake('private');

        $application = $this->recommendedApplication();
        app(AdmitApplicant::class)->execute($application, $this->staff('admissions-manager'));

        app(DocumentGenerator::class)->admissionLetter($application->fresh());

        $versions = $application->fresh()
            ->generatedDocuments()
            ->where('document_type', GeneratedDocument::TYPE_ADMISSION_LETTER)
            ->pluck('version');

        $this->assertEquals([1, 2], $versions->sort()->values()->all());
    }

    public function test_an_admitted_applicant_can_download_their_letter(): void
    {
        Notification::fake();
        Storage::fake('private');

        $application = $this->recommendedApplication();
        app(AdmitApplicant::class)->execute($application, $this->staff('admissions-manager'));

        $this->actingAs($application->applicant->user)
            ->get(route('applicant.applications.letter', $application->fresh()))
            ->assertOk()
            ->assertHeader('content-type', 'application/pdf');
    }

    public function test_an_applicant_who_is_not_admitted_cannot_download_a_letter(): void
    {
        $application = $this->submittedApplication();

        $this->actingAs($application->applicant->user)
            ->get(route('applicant.applications.letter', $application))
            ->assertForbidden();
    }

    public function test_a_document_can_be_verified_by_an_officer(): void
    {
        $application = $this->submittedApplication();
        $document = $application->documents->first();
        $officer = $this->staff('admissions-officer');

        $this->actingAs($officer)
            ->post(route('admin.documents.verify', $document))
            ->assertRedirect();

        $document->refresh();

        $this->assertSame(VerificationStatus::Verified, $document->verification_status);
        $this->assertSame($officer->id, $document->verified_by);
        $this->assertDatabaseHas('audit_logs', ['action' => 'document.verified']);
    }

    public function test_a_document_can_be_rejected_with_a_reason(): void
    {
        $application = $this->submittedApplication();
        $document = $application->documents->first();

        $this->actingAs($this->staff('admissions-officer'))
            ->post(route('admin.documents.reject', $document), ['reason' => 'The scan is unreadable.'])
            ->assertRedirect();

        $document->refresh();

        $this->assertSame(VerificationStatus::Rejected, $document->verification_status);
        $this->assertSame('The scan is unreadable.', $document->rejection_reason);
    }

    public function test_requesting_information_moves_the_application_and_notifies_the_applicant(): void
    {
        Notification::fake();

        $application = $this->submittedApplication();

        $this->actingAs($this->staff('admissions-officer'))
            ->post(route('admin.applications.request-information', $application), [
                'message' => 'Please upload a clearer copy of your UACE certificate.',
            ])
            ->assertRedirect();

        $application->refresh();

        $this->assertSame(ApplicationStatus::NeedsInformation, $application->status);
        $this->assertDatabaseHas('application_notes', [
            'application_id' => $application->id,
            'visibility' => 'applicant',
            'requires_response' => true,
        ]);
    }

    /** A submitted application with documents attached. */
    protected function submittedApplication(): Application
    {
        $applicant = $this->completeApplicant();
        $application = $this->startApplication($applicant);

        $application->choices()->create([
            'programme_id' => Programme::where('code', 'BSN')->value('id'),
            'choice_number' => 1,
        ]);

        foreach (DocumentType::required()->get() as $type) {
            $application->documents()->create([
                'document_type_id' => $type->id,
                'document_type' => $type->slug,
                'original_filename' => $type->slug.'.pdf',
                'stored_path' => 'applications/'.$application->id.'/documents/'.$type->slug.'.pdf',
                'disk' => 'private',
                'mime_type' => 'application/pdf',
                'file_size' => 2048,
            ]);
        }

        app(PaymentService::class)->markPaid(
            app(PaymentService::class)->forApplication($application)
        );

        $application->forceFill([
            'declaration_accepted' => true,
            'declaration_accepted_at' => now(),
            'declaration_version' => '1.0',
            'submitted_at' => now(),
            'status' => ApplicationStatus::Submitted,
        ])->save();

        return $application->fresh(['applicant.user', 'documents', 'choices', 'intake']);
    }

    /** An application that has progressed to Recommended, ready to admit. */
    protected function recommendedApplication(): Application
    {
        $application = $this->submittedApplication();
        $officer = $this->staff('admissions-officer');
        $service = app(ApplicationStatusService::class);

        $service->transition($application, ApplicationStatus::UnderReview, $officer, notify: false);
        $service->transition($application->fresh(), ApplicationStatus::Recommended, $officer, notify: false);

        return $application->fresh(['applicant.user', 'intake', 'choices.programme']);
    }
}
