<?php

namespace Tests\Feature\Workflow;

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\Workflow\Enums\AppointmentStatus;
use App\Domain\Workflow\Enums\TaskStatus;
use App\Domain\Workflow\Models\Appointment;
use App\Domain\Workflow\Models\Task;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TaskAndAppointmentTest 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 userWithRole(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]);
    }

    // ---- Tasks (§19) ----

    public function test_a_task_can_be_raised_against_an_applicant(): void
    {
        $applicant = $this->applicant();
        $manager = $this->userWithRole('recruitment-manager');

        $response = $this->actingAs($manager)->postJson('/api/v1/tasks', [
            'title' => 'Chase the medical report',
            'applicant_id' => $applicant->uuid,
            'due_at' => now()->addDays(2)->toDateTimeString(),
            'priority' => 'high',
        ])->assertCreated();

        $this->assertSame('Chase the medical report', $response->json('data.title'));
        $this->assertStringStartsWith('TSK-', $response->json('data.task_no'));

        $task = Task::firstOrFail();
        $this->assertSame($applicant->id, $task->subject_id);
        $this->assertSame(TaskStatus::Open, $task->status);

        $this->assertDatabaseHas('activities', ['event_action' => 'task.created']);
    }

    public function test_a_task_cannot_reach_an_applicant_the_creator_cannot_open(): void
    {
        $foreign = Applicant::factory()->create();

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->postJson('/api/v1/tasks', [
                'title' => 'Sneaky task',
                'applicant_id' => $foreign->uuid,
            ])
            ->assertStatus(403);

        $this->assertSame(0, Task::count());
    }

    public function test_blocking_a_task_requires_a_reason(): void
    {
        $manager = $this->userWithRole('recruitment-manager');
        $task = Task::factory()->for($this->company)->create();

        $this->actingAs($manager)
            ->postJson("/api/v1/tasks/{$task->uuid}/status", ['status' => 'blocked'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['reason']]);

        $this->actingAs($manager)
            ->postJson("/api/v1/tasks/{$task->uuid}/status", [
                'status' => 'blocked',
                'reason' => 'Waiting for the embassy to reopen after the holiday.',
            ])
            ->assertOk()
            ->assertJsonPath('data.status', 'blocked');
    }

    public function test_completing_a_task_records_who_and_when(): void
    {
        $officer = $this->userWithRole('processing-officer');
        $task = Task::factory()->for($this->company)->create();

        $this->actingAs($officer)
            ->postJson("/api/v1/tasks/{$task->uuid}/status", [
                'status' => 'completed',
                'note' => 'Report collected from the clinic.',
            ])
            ->assertOk()
            ->assertJsonPath('data.is_open', false);

        $task->refresh();
        $this->assertSame($officer->id, $task->completed_by);
        $this->assertNotNull($task->completed_at);
        $this->assertSame('Report collected from the clinic.', $task->completion_note);
    }

    public function test_a_task_waiting_on_someone_else_does_not_read_as_overdue(): void
    {
        $manager = $this->userWithRole('recruitment-manager');

        $ours = Task::factory()->for($this->company)->overdue()->create(['title' => 'Ours']);
        Task::factory()->for($this->company)->overdue()->create([
            'title' => 'Waiting on the embassy',
            'status' => TaskStatus::WaitingThirdParty->value,
        ]);

        $response = $this->actingAs($manager)
            ->getJson('/api/v1/tasks?overdue=1')
            ->assertOk();

        // Flagging someone else's delay as our failure hides the real ones.
        $this->assertCount(1, $response->json('data'));
        $this->assertSame($ours->uuid, $response->json('data.0.id'));
    }

    public function test_a_completed_task_is_never_overdue(): void
    {
        Task::factory()->for($this->company)->overdue()->create([
            'status' => TaskStatus::Completed->value,
        ]);

        $this->actingAs($this->userWithRole('recruitment-manager'))
            ->getJson('/api/v1/tasks?overdue=1')
            ->assertOk()
            ->assertJsonCount(0, 'data');
    }

    public function test_an_agent_only_sees_their_own_tasks(): void
    {
        $agentUser = $this->userWithRole('agent');

        $mine = Task::factory()->for($this->company)->create(['assigned_to' => $agentUser->id]);
        Task::factory()->for($this->company)->create();

        $response = $this->actingAs($agentUser)->getJson('/api/v1/tasks')->assertOk();

        $this->assertCount(1, $response->json('data'));
        $this->assertSame($mine->uuid, $response->json('data.0.id'));
    }

    public function test_tasks_require_permission(): void
    {
        $this->getJson('/api/v1/tasks')->assertStatus(401);

        // Auditors read records but hold no task permission.
        $this->actingAs($this->userWithRole('auditor'))
            ->getJson('/api/v1/tasks')
            ->assertStatus(403);
    }

    // ---- Appointments (§20) ----

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

        $response = $this->actingAs($this->userWithRole('processing-officer'))
            ->postJson('/api/v1/appointments', [
                'type' => 'medical',
                'title' => 'Medical examination',
                'scheduled_at' => now()->addWeek()->toDateTimeString(),
                'location' => 'Gulf Medical Centre, Dhaka',
                'instructions' => 'Bring your passport and fast from midnight.',
                'applicant_id' => $applicant->uuid,
            ])
            ->assertCreated();

        $this->assertStringStartsWith('APT-', $response->json('data.appointment_no'));
        $this->assertSame($applicant->id, Appointment::firstOrFail()->subject_id);
    }

    public function test_rescheduling_keeps_the_original_rather_than_overwriting_it(): void
    {
        $officer = $this->userWithRole('processing-officer');
        $original = Appointment::factory()->for($this->company)->create([
            'scheduled_at' => now()->addWeek(),
        ]);

        $newTime = now()->addWeeks(2);

        $response = $this->actingAs($officer)
            ->postJson("/api/v1/appointments/{$original->uuid}/reschedule", [
                'scheduled_at' => $newTime->toDateTimeString(),
                'reason' => 'Clinic closed for a public holiday.',
            ])
            ->assertOk();

        // The history of a moved medical date is often what a dispute turns on.
        $this->assertSame(AppointmentStatus::Rescheduled, $original->fresh()->status);
        $this->assertSame(2, Appointment::count());

        $replacement = Appointment::where('uuid', $response->json('data.id'))->firstOrFail();
        $this->assertSame($original->id, $replacement->rescheduled_from_id);
        $this->assertSame('Clinic closed for a public holiday.', $replacement->reschedule_reason);
        $this->assertNotSame($original->appointment_no, $replacement->appointment_no);
    }

    public function test_rescheduling_needs_a_reason_and_refuses_a_closed_appointment(): void
    {
        $officer = $this->userWithRole('processing-officer');
        $appointment = Appointment::factory()->for($this->company)->create();

        $this->actingAs($officer)
            ->postJson("/api/v1/appointments/{$appointment->uuid}/reschedule", [
                'scheduled_at' => now()->addWeeks(2)->toDateTimeString(),
            ])
            ->assertStatus(422);

        $appointment->update(['status' => AppointmentStatus::Cancelled->value]);

        $this->actingAs($officer)
            ->postJson("/api/v1/appointments/{$appointment->uuid}/reschedule", [
                'scheduled_at' => now()->addWeeks(2)->toDateTimeString(),
                'reason' => 'Trying anyway',
            ])
            ->assertStatus(422);
    }

    public function test_attendance_and_no_show_are_both_recordable(): void
    {
        $officer = $this->userWithRole('processing-officer');

        $attended = Appointment::factory()->for($this->company)->create();
        $missed = Appointment::factory()->for($this->company)->create();

        $this->actingAs($officer)
            ->postJson("/api/v1/appointments/{$attended->uuid}/outcome", [
                'status' => 'attended',
                'outcome' => 'fit',
            ])
            ->assertOk()
            ->assertJsonPath('data.status', 'attended');

        $this->assertNotNull($attended->fresh()->attended_at);

        $this->actingAs($officer)
            ->postJson("/api/v1/appointments/{$missed->uuid}/outcome", ['status' => 'no_show'])
            ->assertOk()
            ->assertJsonPath('data.status', 'no_show');
    }

    public function test_cancelling_an_appointment_requires_an_explanation(): void
    {
        $appointment = Appointment::factory()->for($this->company)->create();

        $this->actingAs($this->userWithRole('processing-officer'))
            ->postJson("/api/v1/appointments/{$appointment->uuid}/outcome", ['status' => 'cancelled'])
            ->assertStatus(422)
            ->assertJsonStructure(['errors' => ['notes']]);
    }

    public function test_past_appointments_without_an_outcome_are_surfaced(): void
    {
        Appointment::factory()->for($this->company)->create(['scheduled_at' => now()->subDays(2)]);
        Appointment::factory()->for($this->company)->create(['scheduled_at' => now()->addWeek()]);

        $response = $this->actingAs($this->userWithRole('processing-officer'))
            ->getJson('/api/v1/appointments?awaiting_outcome=1')
            ->assertOk();

        // A gap in the record, not just a stale row.
        $this->assertCount(1, $response->json('data'));
        $this->assertTrue($response->json('data.0.is_missing_outcome'));
    }

    public function test_the_applicant_portal_shows_only_visible_appointments(): void
    {
        $portalUser = tap(User::factory()->create([
            'company_id' => $this->company->id,
            'user_type' => 'applicant',
        ]))->assignRole('applicant');

        $applicant = $this->applicant();
        $applicant->update(['user_id' => $portalUser->id]);

        Appointment::factory()->for($this->company)->create([
            'subject_type' => $applicant->getMorphClass(),
            'subject_id' => $applicant->id,
            'title' => 'Medical examination',
            'instructions' => 'Bring your passport.',
            'is_visible_to_applicant' => true,
            'internal_notes' => 'INTERNAL: applicant missed the last two bookings.',
        ]);

        Appointment::factory()->for($this->company)->create([
            'subject_type' => $applicant->getMorphClass(),
            'subject_id' => $applicant->id,
            'type' => 'internal_review',
            'title' => 'Internal risk review',
            'is_visible_to_applicant' => false,
        ]);

        $response = $this->actingAs($portalUser)
            ->getJson('/api/v1/portal/dashboard')
            ->assertOk();

        $appointments = $response->json('data.appointments');

        $this->assertCount(1, $appointments);
        $this->assertSame('Medical examination', $appointments[0]['title']);
        $this->assertSame('Bring your passport.', $appointments[0]['instructions']);

        $content = $response->getContent();
        $this->assertStringNotContainsString('Internal risk review', $content);
        $this->assertStringNotContainsString('INTERNAL', $content);
    }

    public function test_the_dashboard_surfaces_overdue_tasks_and_missing_outcomes(): void
    {
        $officer = $this->userWithRole('processing-officer');

        Task::factory()->for($this->company)->overdue()->create(['assigned_to' => $officer->id]);
        Appointment::factory()->for($this->company)->create(['scheduled_at' => now()->subDay()]);

        $response = $this->actingAs($officer)->getJson('/api/v1/dashboard')->assertOk();

        $this->assertSame(1, $response->json('data.tasks.overdue'));

        $labels = collect($response->json('data.attention'))->pluck('label');
        $this->assertTrue($labels->contains(fn ($l) => str_contains($l, 'overdue')));
        $this->assertTrue($labels->contains(fn ($l) => str_contains($l, 'outcome recorded')));
    }
}
