<?php

namespace Tests\Feature\Training;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Communication\Models\NotificationMessage;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use App\Domain\Training\Enums\ParticipationStatus;
use App\Domain\Training\Models\TrainingParticipant;
use App\Domain\Training\Models\TrainingProgram;
use Database\Seeders\NotificationTemplateSeeder;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class TrainingTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    protected function setUp(): void
    {
        parent::setUp();
        Mail::fake();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);

        $this->company = Company::factory()->create();
        $this->branch = Branch::factory()->for($this->company)->create();
        $this->seed(NotificationTemplateSeeder::class);
    }

    private function user(string $role): User
    {
        return tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole($role);
    }

    private function applicant(): Applicant
    {
        return Applicant::factory()->for($this->company)->create([
            'branch_id' => $this->branch->id,
            'email' => 'applicant'.fake()->unique()->numberBetween(1, 99999).'@example.test',
        ]);
    }

    private function program(array $overrides = []): string
    {
        return $this->actingAs($this->user('processing-officer'))
            ->postJson('/api/v1/training-programs', array_merge([
                'title' => 'Pre-departure orientation',
                'location' => 'Dhaka training centre',
                'starts_on' => now()->addWeek()->toDateString(),
                'capacity' => 2,
                'currency' => 'BDT',
                'fee_amount' => 150000,
                'certificate_required' => true,
            ], $overrides))
            ->assertCreated()
            ->json('data.id');
    }

    private function enrol(string $program, Applicant $applicant, int $expect = 201): array
    {
        return $this->actingAs($this->user('processing-officer'))
            ->postJson("/api/v1/training-programs/{$program}/enrol", [
                'applicant_id' => $applicant->uuid,
            ])
            ->assertStatus($expect)
            ->json('data') ?? [];
    }

    public function test_a_programme_refuses_to_be_over_enrolled(): void
    {
        $program = $this->program(['capacity' => 2]);

        $this->enrol($program, $this->applicant());
        $this->enrol($program, $this->applicant());

        // The third has nowhere to sit.
        $this->enrol($program, $this->applicant(), 422);

        $this->assertSame(2, (int) TrainingProgram::firstOrFail()->enrolled_count);
    }

    public function test_a_programme_with_no_capacity_is_uncapped(): void
    {
        $program = $this->program(['capacity' => 0]);

        foreach (range(1, 5) as $ignored) {
            $this->enrol($program, $this->applicant());
        }

        $this->assertSame(5, (int) TrainingProgram::firstOrFail()->enrolled_count);
        $this->assertNull(TrainingProgram::firstOrFail()->seatsLeft());
    }

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

        $this->enrol($program, $applicant);
        $this->enrol($program, $applicant, 422);

        $this->assertSame(1, (int) TrainingProgram::firstOrFail()->enrolled_count);
    }

    public function test_enrolling_tells_the_applicant_when_and_where(): void
    {
        $program = $this->program();
        $this->enrol($program, $this->applicant());

        $message = NotificationMessage::where('template_code', 'training_scheduled')
            ->where('channel', 'in_app')
            ->firstOrFail();

        $this->assertStringContainsString('Pre-departure orientation', $message->body);
        $this->assertStringContainsString('Dhaka training centre', $message->body);
    }

    public function test_withdrawing_frees_the_seat(): void
    {
        $program = $this->program(['capacity' => 1]);
        $first = $this->applicant();

        $participant = $this->enrol($program, $first);

        $this->enrol($program, $this->applicant(), 422);

        $this->actingAs($this->user('processing-officer'))
            ->postJson("/api/v1/training-participants/{$participant['id']}/outcome", [
                'status' => 'withdrawn',
            ])
            ->assertOk();

        $this->assertSame(0, (int) TrainingProgram::firstOrFail()->enrolled_count);

        // Somebody else can now take the place.
        $this->enrol($program, $this->applicant());
    }

    public function test_a_certificate_is_only_issued_to_someone_who_passed(): void
    {
        $program = $this->program();
        $participant = $this->enrol($program, $this->applicant());
        $officer = $this->user('processing-officer');

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/outcome", ['status' => 'attended'])
            ->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/certificate")
            ->assertStatus(422);

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/outcome", [
                'status' => 'passed',
                'score' => 88,
            ])
            ->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/certificate")
            ->assertOk()
            ->assertJsonPath('data.certificate_issued', true);

        $this->assertNotNull(TrainingParticipant::firstOrFail()->certificate_no);
    }

    public function test_a_certificate_is_not_issued_twice(): void
    {
        $program = $this->program();
        $participant = $this->enrol($program, $this->applicant());
        $officer = $this->user('processing-officer');

        $this->actingAs($officer)->postJson(
            "/api/v1/training-participants/{$participant['id']}/outcome",
            ['status' => 'passed'],
        )->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/certificate")
            ->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/certificate")
            ->assertStatus(422);
    }

    public function test_an_outcome_cannot_be_downgraded_under_an_issued_certificate(): void
    {
        $program = $this->program();
        $participant = $this->enrol($program, $this->applicant());
        $officer = $this->user('processing-officer');

        $this->actingAs($officer)->postJson(
            "/api/v1/training-participants/{$participant['id']}/outcome",
            ['status' => 'passed'],
        )->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/certificate")
            ->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-participants/{$participant['id']}/outcome", ['status' => 'failed'])
            ->assertStatus(422);
    }

    public function test_completing_a_programme_marks_no_shows_absent(): void
    {
        $program = $this->program(['capacity' => 3]);
        $attended = $this->enrol($program, $this->applicant());
        $this->enrol($program, $this->applicant());

        $officer = $this->user('processing-officer');

        $this->actingAs($officer)->postJson(
            "/api/v1/training-participants/{$attended['id']}/outcome",
            ['status' => 'passed'],
        )->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-programs/{$program}/status", ['status' => 'completed'])
            ->assertOk();

        $this->assertSame(
            1,
            TrainingParticipant::where('status', ParticipationStatus::Absent->value)->count(),
            'Whoever was still merely registered never turned up.',
        );
        $this->assertSame(
            1,
            TrainingParticipant::where('status', ParticipationStatus::Passed->value)->count(),
        );
    }

    public function test_a_closed_programme_takes_no_more_enrolments(): void
    {
        $program = $this->program();

        $this->actingAs($this->user('processing-officer'))
            ->postJson("/api/v1/training-programs/{$program}/status", ['status' => 'cancelled'])
            ->assertOk();

        $this->enrol($program, $this->applicant(), 422);
    }

    public function test_a_cancelled_programme_cannot_be_reopened(): void
    {
        $program = $this->program();
        $officer = $this->user('processing-officer');

        $this->actingAs($officer)
            ->postJson("/api/v1/training-programs/{$program}/status", ['status' => 'cancelled'])
            ->assertOk();

        $this->actingAs($officer)
            ->postJson("/api/v1/training-programs/{$program}/status", ['status' => 'scheduled'])
            ->assertStatus(422);
    }

    public function test_a_programme_cannot_end_before_it_starts(): void
    {
        $this->actingAs($this->user('processing-officer'))
            ->postJson('/api/v1/training-programs', [
                'title' => 'Time travel',
                'starts_on' => now()->addWeek()->toDateString(),
                'ends_on' => now()->toDateString(),
                'currency' => 'BDT',
            ])
            ->assertStatus(422);
    }

    public function test_the_applicant_file_shows_their_training_history(): void
    {
        $program = $this->program();
        $applicant = $this->applicant();
        $this->enrol($program, $applicant);

        $history = $this->actingAs($this->user('processing-officer'))
            ->getJson("/api/v1/applicants/{$applicant->uuid}/training")
            ->assertOk()
            ->json('data');

        $this->assertCount(1, $history);
        $this->assertSame('Pre-departure orientation', $history[0]['program']['title']);
        $this->assertSame('Booked', $history[0]['public_label']);
    }

    public function test_managing_training_needs_the_permission(): void
    {
        $this->actingAs($this->user('data-entry-officer'))
            ->postJson('/api/v1/training-programs', [
                'title' => 'Not my call',
                'starts_on' => now()->toDateString(),
                'currency' => 'BDT',
            ])
            ->assertForbidden();
    }
}
