<?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\Models\Pipeline;
use App\Domain\Workflow\Models\PipelineStage;
use App\Domain\Workflow\Models\PipelineTransition;
use App\Domain\Workflow\Services\PipelineService;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;

class PipelineEngineTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    private Pipeline $pipeline;

    /** @var array<string, PipelineStage> */
    private array $stages = [];

    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();

        $this->pipeline = Pipeline::create([
            'company_id' => $this->company->id,
            'code' => 'test-flow',
            'name' => 'Test Flow',
            'is_default' => true,
            'is_active' => true,
        ]);

        $this->stages['start'] = $this->stage('start', 'Start', 10, ['is_initial' => true]);
        $this->stages['verified'] = $this->stage('verified', 'Verified', 20, [
            'entry_requirements' => [['type' => 'profile_verified', 'params' => []]],
        ]);
        $this->stages['visa'] = $this->stage('visa', 'Visa Processing', 30, [
            'sla_days' => 5,
            'entry_requirements' => [
                ['type' => 'passport_valid', 'params' => ['minimum_months' => 6]],
                ['type' => 'staff_assigned', 'params' => []],
            ],
        ]);
        $this->stages['on_hold'] = $this->stage('on_hold', 'On Hold', 90, ['is_exception' => true]);

        $this->transition($this->stages['start'], $this->stages['verified']);
        $this->transition($this->stages['verified'], $this->stages['visa']);
        // Reachable from anywhere, and always explained.
        $this->transition(null, $this->stages['on_hold'], requiresReason: true);
    }

    private function stage(string $code, string $name, int $sequence, array $extra = []): PipelineStage
    {
        return PipelineStage::create(array_merge([
            'pipeline_id' => $this->pipeline->id,
            'code' => $code,
            'name' => $name,
            'sequence' => $sequence,
            'applicant_visible_label' => "Public {$name}",
            'is_active' => true,
        ], $extra));
    }

    private function transition(
        ?PipelineStage $from,
        PipelineStage $to,
        ?array $roles = null,
        bool $requiresReason = false,
    ): PipelineTransition {
        return PipelineTransition::create([
            'pipeline_id' => $this->pipeline->id,
            'from_stage_id' => $from?->id,
            'to_stage_id' => $to->id,
            'allowed_roles' => $roles,
            'requires_reason' => $requiresReason,
            'is_active' => true,
        ]);
    }

    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(array $attributes = []): Applicant
    {
        return Applicant::factory()->for($this->company)->create(array_merge([
            'branch_id' => $this->branch->id,
        ], $attributes));
    }

    private function service(): PipelineService
    {
        return app(PipelineService::class);
    }

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

        $this->service()->assignPipeline($applicant, $this->pipeline, $this->userWithRole('recruitment-manager'));

        $applicant->refresh();
        $this->assertSame($this->stages['start']->id, $applicant->current_stage_id);
        $this->assertNotNull($applicant->stage_entered_at);
        $this->assertDatabaseHas('applicant_stage_histories', [
            'applicant_id' => $applicant->id,
            'to_stage_id' => $this->stages['start']->id,
        ]);
    }

    public function test_a_pipeline_from_another_company_is_refused(): void
    {
        $foreign = Pipeline::create([
            'company_id' => Company::factory()->create()->id,
            'code' => 'foreign',
            'name' => 'Foreign',
        ]);

        $this->expectException(ValidationException::class);
        $this->service()->assignPipeline($this->applicant(), $foreign);
    }

    public function test_an_undeclared_transition_is_refused(): void
    {
        $applicant = $this->applicant(['verified_at' => now()]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        // Start → Visa was never declared; only Start → Verified exists.
        $this->expectException(ValidationException::class);
        $this->service()->moveTo($applicant->fresh(), $this->stages['visa'], $user);
    }

    public function test_a_stage_with_unmet_requirements_blocks_the_move(): void
    {
        $applicant = $this->applicant(['verified_at' => null]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        try {
            $this->service()->moveTo($applicant->fresh(), $this->stages['verified'], $user);
            $this->fail('The move should have been refused.');
        } catch (ValidationException $exception) {
            $this->assertArrayHasKey('requirements', $exception->errors());
            $this->assertStringContainsString('verified by staff', $exception->errors()['requirements'][0]);
        }

        $this->assertSame($this->stages['start']->id, $applicant->fresh()->current_stage_id);
    }

    public function test_a_satisfied_requirement_allows_the_move(): void
    {
        $applicant = $this->applicant(['verified_at' => now()]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        $this->service()->moveTo($applicant->fresh(), $this->stages['verified'], $user);

        $this->assertSame($this->stages['verified']->id, $applicant->fresh()->current_stage_id);
    }

    public function test_an_override_records_what_was_bypassed_and_needs_a_reason(): void
    {
        $applicant = $this->applicant(['verified_at' => null]);
        $user = $this->userWithRole('super-admin');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        // Override without a reason is still refused.
        try {
            $this->service()->moveTo($applicant->fresh(), $this->stages['verified'], $user, null, null, true);
            $this->fail('An override without a reason should have been refused.');
        } catch (ValidationException $exception) {
            $this->assertArrayHasKey('reason', $exception->errors());
        }

        $this->service()->moveTo(
            $applicant->fresh(),
            $this->stages['verified'],
            $user,
            'Director approved; paper file checked in person.',
            null,
            true,
        );

        $history = $applicant->fresh()->stageHistories()->first();
        $this->assertTrue($history->was_overridden);
        $this->assertNotEmpty($history->unmet_requirements);
        $this->assertSame('Director approved; paper file checked in person.', $history->reason);
    }

    public function test_a_transition_restricted_by_role_refuses_other_roles(): void
    {
        $applicant = $this->applicant(['verified_at' => now()]);
        $manager = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $manager);

        PipelineTransition::where('to_stage_id', $this->stages['verified']->id)
            ->update(['allowed_roles' => json_encode(['document-verification-officer'])]);

        $this->expectException(ValidationException::class);
        $this->service()->moveTo($applicant->fresh(), $this->stages['verified'], $manager);
    }

    public function test_a_transition_marked_as_requiring_a_reason_enforces_it(): void
    {
        $applicant = $this->applicant();
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        try {
            $this->service()->moveTo($applicant->fresh(), $this->stages['on_hold'], $user);
            $this->fail('A reason should have been required.');
        } catch (ValidationException $exception) {
            $this->assertArrayHasKey('reason', $exception->errors());
        }

        $this->service()->moveTo(
            $applicant->fresh(),
            $this->stages['on_hold'],
            $user,
            'Applicant travelling; unreachable for three weeks.',
        );

        $this->assertSame($this->stages['on_hold']->id, $applicant->fresh()->current_stage_id);
    }

    public function test_an_exception_stage_is_reachable_from_any_stage(): void
    {
        $applicant = $this->applicant(['verified_at' => now()]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);
        $this->service()->moveTo($applicant->fresh(), $this->stages['verified'], $user);

        // The catch-all transition applies from Verified too.
        $this->service()->moveTo($applicant->fresh(), $this->stages['on_hold'], $user, 'Documents lost in transit.');

        $this->assertSame($this->stages['on_hold']->id, $applicant->fresh()->current_stage_id);
    }

    public function test_moving_to_the_current_stage_is_refused(): void
    {
        $applicant = $this->applicant();
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        $this->expectException(ValidationException::class);
        $this->service()->moveTo($applicant->fresh(), $this->stages['start'], $user);
    }

    public function test_available_transitions_report_what_blocks_each_option(): void
    {
        $applicant = $this->applicant(['verified_at' => null]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        $options = $this->service()->availableTransitions($applicant->fresh(), $user);
        $byCode = collect($options)->keyBy(fn (array $option) => $option['stage']->code);

        $this->assertTrue($byCode->has('verified'));
        $this->assertTrue($byCode->has('on_hold'));
        $this->assertCount(1, $byCode['verified']['unmet_requirements']);
        $this->assertSame([], $byCode['on_hold']['unmet_requirements']);
        $this->assertTrue($byCode['on_hold']['requires_reason']);
    }

    public function test_an_unknown_requirement_type_blocks_rather_than_passes(): void
    {
        $applicant = $this->applicant(['verified_at' => now()]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);

        $this->stages['verified']->update([
            'entry_requirements' => [['type' => 'not_a_real_check', 'params' => []]],
        ]);

        $this->expectException(ValidationException::class);
        $this->service()->moveTo($applicant->fresh(), $this->stages['verified']->fresh(), $user);
    }

    public function test_sla_breach_is_reported_once_the_target_is_passed(): void
    {
        $applicant = $this->applicant(['verified_at' => now()]);
        $user = $this->userWithRole('recruitment-manager');
        $this->service()->assignPipeline($applicant, $this->pipeline, $user);
        $this->service()->moveTo($applicant->fresh(), $this->stages['verified'], $user);

        $applicant->refresh();
        $this->assertFalse($applicant->isBreachingStageSla());

        // Verified has no SLA; move to a stage that does, then age it.
        $applicant->forceFill([
            'current_stage_id' => $this->stages['visa']->id,
            'stage_entered_at' => now()->subDays(9),
        ])->save();

        $this->assertTrue($applicant->fresh()->isBreachingStageSla());
    }
}
