<?php

namespace Tests\Feature;

use App\Actions\Applications\ApplicationValidator;
use App\Actions\Applications\StartApplication;
use App\Actions\Applications\SubmitApplication;
use App\Enums\ApplicationStatus;
use App\Enums\ApplicationStep;
use App\Livewire\Applicant\AcademicHistoryForm;
use App\Livewire\Applicant\DeclarationForm;
use App\Livewire\Applicant\PersonalInformationForm;
use App\Livewire\Applicant\ProgrammeSelectionForm;
use App\Models\Application;
use App\Models\DocumentType;
use App\Models\Intake;
use App\Models\Programme;
use App\Notifications\ApplicationSubmitted;
use App\Services\Payments\PaymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Illuminate\Validation\ValidationException;
use Livewire\Livewire;
use Tests\TestCase;

class ApplicationLifecycleTest extends TestCase
{
    use RefreshDatabase;

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

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

        $application = app(StartApplication::class)->execute($applicant, Intake::current());

        $this->assertSame(ApplicationStatus::Draft, $application->status);
        $this->assertMatchesRegularExpression('/^KCU-\d{4}-\d{6}$/', $application->application_number);
        $this->assertDatabaseHas('application_status_history', [
            'application_id' => $application->id,
            'to_status' => 'draft',
        ]);
    }

    public function test_application_numbers_are_sequential_and_unique(): void
    {
        $numbers = collect(range(1, 5))
            ->map(fn () => $this->startApplication($this->applicant())->application_number);

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

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

        $first = $this->startApplication($applicant);
        $second = $this->startApplication($applicant);

        $this->assertTrue($first->is($second));
        $this->assertSame(1, Application::where('applicant_id', $applicant->id)->count());
    }

    public function test_an_application_cannot_be_started_for_a_closed_intake(): void
    {
        $intake = Intake::current();
        $intake->update(['status' => 'closed']);

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

        app(StartApplication::class)->execute($this->applicant(), $intake->fresh());
    }

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

        Livewire::actingAs($applicant->user)
            ->test(PersonalInformationForm::class, ['application' => $application])
            ->set('first_name', 'Amina')
            ->set('last_name', 'Nakato')
            ->set('date_of_birth', now()->subYears(20)->toDateString())
            ->set('gender', 'female')
            ->set('nationality', 'Ugandan')
            ->set('identity_type', 'national_id')
            ->set('national_id', 'CM90123456ABCD')
            ->call('save')
            ->assertHasNoErrors();

        $application->refresh();

        $this->assertSame('Amina', $applicant->fresh()->first_name);
        $this->assertContains(ApplicationStep::Personal->value, $application->completed_steps);
        $this->assertSame(ApplicationStep::Contact, $application->current_step);
    }

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

        Livewire::actingAs($applicant->user)
            ->test(PersonalInformationForm::class, ['application' => $application])
            ->set('first_name', 'Amina')
            ->set('last_name', 'Nakato')
            ->set('date_of_birth', now()->subYears(3)->toDateString())
            ->set('gender', 'female')
            ->set('nationality', 'Ugandan')
            ->set('national_id', 'CM90123456ABCD')
            ->call('save')
            ->assertHasErrors('date_of_birth');
    }

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

        Livewire::actingAs($applicant->user)
            ->test(AcademicHistoryForm::class, ['application' => $application])
            ->set('qualifications.0.qualification_type', 'uace')
            ->set('qualifications.0.institution', 'Kampala SS')
            ->set('qualifications.0.year', (string) (now()->year - 1))
            ->set('qualifications.0.index_number', 'U0123/501')
            ->set('qualifications.0.subjects.0.subject', 'Biology')
            ->set('qualifications.0.subjects.0.grade', 'B')
            ->call('save')
            ->assertHasNoErrors();

        $qualification = $applicant->fresh()->qualifications()->with('subjects')->first();

        $this->assertNotNull($qualification);
        $this->assertSame('Kampala SS', $qualification->institution);
        $this->assertSame('Biology', $qualification->subjects->first()->subject);
        // UACE points are cached for the eligibility engine.
        $this->assertSame(5, $qualification->total_points);
    }

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

        Livewire::actingAs($applicant->user)
            ->test(AcademicHistoryForm::class, ['application' => $application])
            ->set('qualifications.0.qualification_type', 'uace')
            ->set('qualifications.0.institution', 'Kampala SS')
            ->set('qualifications.0.year', (string) (now()->year - 1))
            ->call('save')
            ->assertHasErrors('qualifications.0.subjects');
    }

    public function test_programme_choices_are_saved_with_an_eligibility_report(): void
    {
        $applicant = $this->completeApplicant();
        $application = $this->startApplication($applicant);
        $programme = Programme::where('code', 'BSN')->firstOrFail();

        Livewire::actingAs($applicant->user)
            ->test(ProgrammeSelectionForm::class, ['application' => $application])
            ->set('choices.1', (string) $programme->id)
            ->call('save')
            ->assertHasNoErrors();

        $choice = $application->fresh()->choices()->first();

        $this->assertSame($programme->id, $choice->programme_id);
        $this->assertSame(1, $choice->choice_number);
        $this->assertIsArray($choice->eligibility_report);
    }

    public function test_the_same_programme_cannot_be_chosen_twice(): void
    {
        $applicant = $this->completeApplicant();
        $application = $this->startApplication($applicant);
        $programme = Programme::where('code', 'BSN')->firstOrFail();

        Livewire::actingAs($applicant->user)
            ->test(ProgrammeSelectionForm::class, ['application' => $application])
            ->set('choices.1', (string) $programme->id)
            ->set('choices.2', (string) $programme->id)
            ->call('save')
            ->assertHasErrors('choices');
    }

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

        Livewire::actingAs($applicant->user)
            ->test(DeclarationForm::class, ['application' => $application])
            ->set('accepted', true)
            ->call('save')
            ->assertHasNoErrors();

        $application->refresh();

        $this->assertTrue($application->declaration_accepted);
        $this->assertNotNull($application->declaration_accepted_at);
        $this->assertNotNull($application->declaration_ip);
        $this->assertSame(config('kcu.declaration.version'), $application->declaration_version);
    }

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

        Livewire::actingAs($applicant->user)
            ->test(DeclarationForm::class, ['application' => $application])
            ->set('accepted', false)
            ->call('save')
            ->assertHasErrors('accepted');

        $this->assertFalse($application->fresh()->declaration_accepted);
    }

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

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

        app(SubmitApplication::class)->execute($application);
    }

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

        $application = $this->readyToSubmitApplication();

        app(SubmitApplication::class)->execute($application);

        $application->refresh();

        $this->assertSame(ApplicationStatus::Submitted, $application->status);
        $this->assertNotNull($application->submitted_at);

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

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

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

    public function test_an_application_cannot_be_submitted_twice(): void
    {
        $application = $this->readyToSubmitApplication();

        app(SubmitApplication::class)->execute($application);

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

        app(SubmitApplication::class)->execute($application->fresh());
    }

    public function test_a_submitted_application_can_no_longer_be_edited(): void
    {
        $application = $this->readyToSubmitApplication();
        app(SubmitApplication::class)->execute($application);

        $this->actingAs($application->applicant->user)
            ->get(route('applicant.applications.wizard', [$application->fresh(), 'personal']))
            ->assertRedirect(route('applicant.applications.show', $application));
    }

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

        Livewire::actingAs($applicant->user)
            ->test(PersonalInformationForm::class, ['application' => $application])
            ->set('first_name', 'Amina')
            ->set('last_name', 'Nakato')
            ->set('date_of_birth', now()->subYears(20)->toDateString())
            ->set('gender', 'female')
            ->set('nationality', 'Ugandan')
            ->set('national_id', 'CM90123456ABCD')
            ->call('save');

        // A later visit finds the saved values already populated.
        Livewire::actingAs($applicant->user)
            ->test(PersonalInformationForm::class, ['application' => $application->fresh()])
            ->assertSet('first_name', 'Amina')
            ->assertSet('national_id', 'CM90123456ABCD');
    }

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

        $this->actingAs($applicant->user)
            ->get(route('applicant.dashboard'))
            ->assertOk()
            ->assertSee('Continue Application');
    }

    /** Build an application that satisfies every submission requirement. */
    protected function readyToSubmitApplication(): 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' => 1024,
            ]);
        }

        $application->forceFill([
            'declaration_accepted' => true,
            'declaration_accepted_at' => now(),
            'declaration_ip' => '127.0.0.1',
            'declaration_version' => config('kcu.declaration.version'),
        ])->save();

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

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