<?php

namespace Tests\Feature;

use App\Domain\Shared\Models\Academy;
use App\Domain\Training\Events\EnrollmentCancelled;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Services\EnrollmentService;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

/**
 * Cancelling an enrolment.
 *
 * `EnrollmentCancelled` takes (enrolment, reason, actor); the service dispatched
 * (enrolment, actor). PHP put the User where the string reason was expected and
 * found no third argument, so dispatch() threw an ArgumentCountError — inside
 * the service's own DB::transaction, which then rolled the cancellation back.
 *
 * Every cancellation on every tenant failed this way, and the only trace was an
 * error page: the row stayed active, the group count stayed high, and the
 * enrolment kept being billed by the nightly renewal run.
 *
 * Nothing caught it because the arity mismatch is only reachable at runtime, on
 * the one line that constructs the event.
 *
 *   DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_DATABASE=oc_sport_test \
 *     ./vendor/bin/phpunit --filter EnrollmentCancellationTest
 */
class EnrollmentCancellationTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        if (config('database.default') !== 'pgsql') {
            $this->markTestSkipped('Needs a restored Postgres tenant.');
        }

        if (! $academy = Academy::first()) {
            $this->markTestSkipped('No academy in the restored tenant.');
        }

        app()->instance('current_academy', $academy);
        DB::beginTransaction();
    }

    protected function tearDown(): void
    {
        DB::rollBack();
        parent::tearDown();
    }

    public function test_an_active_enrolment_can_be_cancelled(): void
    {
        $enrollment = Enrollment::withoutGlobalScopes()
            ->where('status', 'active')
            ->whereHas('group')
            ->first();

        $actor = User::first();

        if (! $enrollment || ! $actor) {
            $this->markTestSkipped('Tenant has no active enrolment to cancel.');
        }

        // No Event::fake here, deliberately. dispatch() constructs the event
        // before the dispatcher ever sees it, so a wrong argument list throws on
        // this line whether events are faked or not — and the throw happens
        // inside the service's transaction, so a cancellation that reaches
        // 'cancelled' at all is the proof the signature matches.
        //
        // Asserting on the dispatched event instead would prove nothing: the
        // event is ShouldDispatchAfterCommit and this test rolls back, so it is
        // correctly never delivered.
        $cancelled = app(EnrollmentService::class)
            ->cancel($enrollment, 'اختبار الإلغاء', $actor);

        $this->assertSame('cancelled', $cancelled->status->value);
        $this->assertSame('اختبار الإلغاء', $cancelled->withdrawal_reason);
        $this->assertNotNull($cancelled->withdrawal_date);
    }

    public function test_the_cancellation_event_still_takes_a_reason(): void
    {
        // The contract the service has to satisfy, pinned separately so a change
        // to the event is caught even without a tenant to cancel against.
        $params = (new \ReflectionMethod(EnrollmentCancelled::class, '__construct'))->getParameters();

        $this->assertSame(['enrollment', 'reason', 'actor'], array_map(
            fn (\ReflectionParameter $p) => $p->getName(),
            $params,
        ));
    }

    public function test_an_already_cancelled_enrolment_is_refused(): void
    {
        $enrollment = Enrollment::withoutGlobalScopes()->where('status', 'cancelled')->first();
        $actor = User::first();

        if (! $enrollment || ! $actor) {
            $this->markTestSkipped('Tenant has no cancelled enrolment to check.');
        }

        $this->expectExceptionMessage('لا يمكن إلغاء هذا التسجيل');
        app(EnrollmentService::class)->cancel($enrollment, 'مرة أخرى', $actor);
    }
}
