<?php

namespace Tests\Feature\Processing;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use App\Domain\Processing\Enums\MedicalStatus;
use App\Domain\Processing\Enums\VisaStatus;
use App\Domain\Processing\Models\MedicalRecord;
use App\Domain\Processing\Models\PassportCustodyEvent;
use App\Domain\Processing\Models\TravelRecord;
use App\Domain\Processing\Models\VisaRecord;
use App\Domain\Recruitment\Enums\JobDemandStatus;
use App\Domain\Recruitment\Models\Employer;
use App\Domain\Recruitment\Models\JobDemand;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Tests\TestCase;

class ProcessingTest 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 officer(): User
    {
        return tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole('processing-officer');
    }

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

    /** Puts an applicant through medical so a visa can be submitted. */
    private function clearMedically(Applicant $applicant, User $officer): MedicalRecord
    {
        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/medical", [
            'center' => 'Gulf Medical Centre',
            'appointment_date' => now()->addDay()->toDateString(),
        ])->assertCreated();

        $record = MedicalRecord::where('applicant_id', $applicant->id)->current()->firstOrFail();

        $this->actingAs($officer)->postJson("/api/v1/medical-records/{$record->uuid}/result", [
            'status' => 'fit',
            'expiry_date' => now()->addMonths(6)->toDateString(),
        ])->assertOk();

        return $record->fresh();
    }

    // ---- Medical (§21) ----

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

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/medical", [
            'appointment_date' => now()->addDay()->toDateString(),
        ])->assertCreated();

        $first = MedicalRecord::where('applicant_id', $applicant->id)->current()->firstOrFail();

        $this->actingAs($officer)->postJson("/api/v1/medical-records/{$first->uuid}/result", [
            'status' => 'unfit',
            'findings' => 'Blood pressure outside the accepted range.',
        ])->assertOk();

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/medical", [
            'appointment_date' => now()->addWeek()->toDateString(),
        ])->assertCreated()->assertJsonPath('data.attempt', 2);

        // A history of unfit results matters; it is not overwritten.
        $this->assertSame(2, MedicalRecord::where('applicant_id', $applicant->id)->count());
        $this->assertFalse($first->fresh()->is_current);
        $this->assertSame(MedicalStatus::Unfit, $first->fresh()->status);
    }

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

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/medical", [
            'appointment_date' => now()->addDay()->toDateString(),
        ])->assertCreated();

        $record = MedicalRecord::where('applicant_id', $applicant->id)->current()->firstOrFail();

        // A pass with no expiry would clear someone indefinitely.
        $this->actingAs($officer)
            ->postJson("/api/v1/medical-records/{$record->uuid}/result", ['status' => 'fit'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['expiry_date']]);
    }

    public function test_an_expired_fit_result_stops_clearing_the_applicant(): void
    {
        $applicant = $this->applicant();
        $record = $this->clearMedically($applicant, $this->officer());

        $this->assertTrue($record->fresh()->clearsForDeployment());

        $record->fresh()->update(['expiry_date' => now()->subDay()->toDateString()]);

        $this->assertFalse($record->fresh()->clearsForDeployment());
        $this->assertSame(MedicalStatus::Expired, $record->fresh()->effectiveStatus());
    }

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

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/medical", [
            'appointment_date' => now()->addDay()->toDateString(),
        ])->assertCreated();

        $record = MedicalRecord::where('applicant_id', $applicant->id)->current()->firstOrFail();
        $this->actingAs($officer)->postJson("/api/v1/medical-records/{$record->uuid}/result", [
            'status' => 'unfit',
            'findings' => 'Hepatitis B surface antigen reactive.',
        ])->assertOk();

        // Branch managers hold processing.view but not processing.medical_manage,
        // so they see that a medical happened without the clinical detail.
        $manager = tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole('branch-manager');

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

        $this->assertNull($response->json('data.medical.current.findings'));
        $this->assertStringNotContainsString('Hepatitis', $response->getContent());
    }

    // ---- Visa (§23) ----

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

        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA'])
            ->assertCreated();

        $visa = VisaRecord::firstOrFail();

        // Submitting without a medical wastes the fee and the applicant's time.
        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", ['status' => 'submitted'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['status']]);

        $this->clearMedically($applicant, $officer);

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", ['status' => 'submitted'])
            ->assertOk()
            ->assertJsonPath('data.status', 'submitted');
    }

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

        $this->clearMedically($applicant, $officer);
        MedicalRecord::where('applicant_id', $applicant->id)
            ->current()
            ->update(['expiry_date' => now()->addDays(10)->toDateString()]);

        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA'])
            ->assertCreated();

        $visa = VisaRecord::firstOrFail();

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", ['status' => 'submitted'])
            ->assertStatus(422);
    }

    public function test_approval_requires_the_visa_number(): void
    {
        $applicant = $this->applicant();
        $officer = $this->officer();
        $this->clearMedically($applicant, $officer);

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA']);
        $visa = VisaRecord::firstOrFail();

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", ['status' => 'approved'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['visa_number']]);

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", [
                'status' => 'approved',
                'visa_number' => 'SA-99887766',
                'valid_until' => now()->addYear()->toDateString(),
            ])
            ->assertOk()
            ->assertJsonPath('data.is_usable', true);
    }

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

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA']);
        $visa = VisaRecord::firstOrFail();

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", ['status' => 'rejected'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['reason']]);

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", [
                'status' => 'rejected',
                'reason' => 'Employer quota exhausted for this trade.',
            ])
            ->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/visa-records/{$visa->uuid}/status", ['status' => 'under_processing'])
            ->assertStatus(422);
    }

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

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA'])
            ->assertCreated();

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA'])
            ->assertStatus(422);
    }

    // ---- Passport custody (§24) ----

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

        $event = PassportCustodyEvent::create([
            'company_id' => $this->company->id,
            'applicant_id' => $applicant->id,
            'event_type' => 'received',
            'passport_number' => 'A1234567',
            'occurred_at' => now(),
        ]);

        // A passport the agency cannot account for is a serious matter.
        $this->expectException(RuntimeException::class);
        $event->delete();
    }

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

        // Nothing is held yet, so it cannot be handed over.
        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
                'event_type' => 'handed_over',
                'acknowledgement' => 'Embassy courier',
            ])
            ->assertStatus(422);

        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
                'event_type' => 'received',
                'stored_location' => 'Safe 2, Dhaka office',
                'reason' => 'Visa submission',
            ])
            ->assertCreated()
            ->assertJsonPath('data.is_held', true);

        // Taking it in twice means the register has lost track of a real document.
        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
                'event_type' => 'received',
            ])
            ->assertStatus(422);
    }

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

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
            'event_type' => 'received',
        ])->assertCreated();

        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
                'event_type' => 'returned',
            ])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['acknowledgement']]);
    }

    // ---- Travel (§25) ----

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

        // Money spent on a trip that cannot legally happen.
        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/travel", [
                'departure_at' => now()->addWeeks(2)->toDateTimeString(),
            ])
            ->assertStatus(422);

        $this->clearMedically($applicant, $officer);
        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA']);
        $visa = VisaRecord::firstOrFail();
        $this->actingAs($officer)->postJson("/api/v1/visa-records/{$visa->uuid}/status", [
            'status' => 'approved',
            'visa_number' => 'SA-1234',
            'valid_until' => now()->addYear()->toDateString(),
        ])->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/travel", [
                'airline' => 'Biman',
                'flight_number' => 'BG-047',
                'departure_at' => now()->addWeeks(2)->toDateTimeString(),
            ])
            ->assertCreated()
            ->assertJsonPath('data.status', 'booked');
    }

    public function test_departure_is_refused_while_the_agency_holds_the_passport(): void
    {
        $applicant = $this->applicant();
        $officer = $this->officer();
        $this->readyForTravel($applicant, $officer);

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
            'event_type' => 'received',
        ])->assertCreated();

        $travel = TravelRecord::firstOrFail();

        $this->actingAs($officer)
            ->postJson("/api/v1/travel-records/{$travel->uuid}/status", ['status' => 'departed'])
            ->assertStatus(422);

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/passport-custody", [
            'event_type' => 'returned',
            'acknowledgement' => 'Signed for by the applicant.',
        ])->assertCreated();

        $this->actingAs($officer)
            ->postJson("/api/v1/travel-records/{$travel->uuid}/status", ['status' => 'departed'])
            ->assertOk();
    }

    public function test_joining_the_employer_advances_the_demand_count_once(): void
    {
        $employer = Employer::factory()->for($this->company)->active()->create();
        $demand = JobDemand::factory()->for($this->company)->create([
            'employer_id' => $employer->id,
            'status' => JobDemandStatus::Open->value,
            'quantity_required' => 10,
            'deployed_count' => 0,
        ]);

        $applicant = $this->applicant(['job_demand_id' => $demand->id]);
        $officer = $this->officer();
        $this->readyForTravel($applicant, $officer);

        $travel = TravelRecord::firstOrFail();

        $this->actingAs($officer)->postJson("/api/v1/travel-records/{$travel->uuid}/status", [
            'status' => 'joined',
        ])->assertOk();

        $this->assertSame(1, $demand->fresh()->deployed_count);

        // A repeated confirmation must not inflate the count.
        $this->actingAs($officer)->postJson("/api/v1/travel-records/{$travel->uuid}/status", [
            'status' => 'joined',
        ])->assertOk();

        $this->assertSame(1, $demand->fresh()->deployed_count);
    }

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

        $this->getJson("/api/v1/applicants/{$applicant->uuid}/processing")->assertStatus(401);

        // Cashiers hold no processing permission.
        $cashier = tap(User::factory()->create(['company_id' => $this->company->id]))
            ->assignRole('cashier');

        $this->actingAs($cashier)
            ->getJson("/api/v1/applicants/{$applicant->uuid}/processing")
            ->assertStatus(403);
    }

    /** Medical → visa approved → ticket booked. */
    private function readyForTravel(Applicant $applicant, User $officer): void
    {
        $this->clearMedically($applicant, $officer);

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/visa", ['country' => 'SA']);
        $visa = VisaRecord::where('applicant_id', $applicant->id)->firstOrFail();

        $this->actingAs($officer)->postJson("/api/v1/visa-records/{$visa->uuid}/status", [
            'status' => 'approved',
            'visa_number' => 'SA-'.$applicant->id,
            'valid_until' => now()->addYear()->toDateString(),
        ])->assertOk();

        $this->actingAs($officer)->postJson("/api/v1/applicants/{$applicant->uuid}/travel", [
            'departure_at' => now()->addWeeks(2)->toDateTimeString(),
        ])->assertCreated();
    }
}
