<?php

namespace Tests\Feature;

use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Services\EnrollmentService;
use App\Models\User;
use Tests\TestCase;

/**
 * Moving a player back into a group they used to be in.
 *
 * A club shuffles children between the parallel groups of one age band all
 * season — 2015 A to 2015 B and back again a month later. transfer() used to
 * answer the return leg with a raw
 * `enrollments_participant_id_training_group_id_unique` violation, because it
 * always INSERTs the replacement enrolment while the cancelled one it made on
 * the way out is still sitting in the table. The desk saw "خطأ غير متوقع" and
 * had no way to know what it had done wrong.
 *
 * The row is unique per (participant, group) for good reason — one player is in
 * a group once — so the fix is to revive that row rather than to loosen the
 * constraint.
 *
 * Runs against a restored Postgres tenant; skips on the default SQLite suite:
 *
 *   DB_CONNECTION=pgsql DB_DATABASE=oc_sport_test \
 *     ./vendor/bin/phpunit --filter EnrollmentTransferReturnsToFormerGroupTest
 */
class EnrollmentTransferReturnsToFormerGroupTest extends TestCase
{
    private EnrollmentService $service;

    private User $actor;

    private TrainingGroup $home;

    private TrainingGroup $away;

    private Enrollment $enrollment;

    /** Everything this test touched, restored in teardown. */
    private array $restore = [];

    protected function setUp(): void
    {
        parent::setUp();

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

        $academy = \App\Domain\Shared\Models\Academy::query()->first();

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

        app()->instance('current_academy', $academy);

        $this->actor = User::withoutGlobalScopes()->firstOrFail();
        $this->service = app(EnrollmentService::class);

        // Two groups in the same branch with room in both — the shape a club
        // actually shuffles players between.
        $enrollment = Enrollment::withoutGlobalScopes()
            ->where('status', 'active')
            ->whereHas('group', fn ($q) => $q->whereColumn('current_count', '<', 'max_capacity'))
            // Ordered: an UPDATE moves a row in the Postgres heap, so an
            // unordered first() hands the two test methods different fixtures.
            ->orderBy('id')
            ->first();

        if (! $enrollment) {
            $this->markTestSkipped('No active enrolment with room in its group.');
        }

        $home = TrainingGroup::withoutGlobalScopes()->find($enrollment->training_group_id);

        $away = TrainingGroup::withoutGlobalScopes()
            ->where('id', '!=', $home->id)
            ->where('branch_id', $home->branch_id)
            ->where('training_program_id', '!=', $home->training_program_id)
            ->whereIn('status', ['forming', 'active'])
            ->whereColumn('current_count', '<', 'max_capacity')
            ->orderBy('id')
            ->first();

        if (! $away) {
            $this->markTestSkipped('No second group in the same branch with room.');
        }

        // Nothing may already occupy the seat we are about to move into and back
        // out of, or teardown could not tell its rows from ours.
        $priorAway = Enrollment::withoutGlobalScopes()
            ->where('participant_id', $enrollment->participant_id)
            ->where('training_group_id', $away->id)
            ->exists();

        if ($priorAway) {
            $this->markTestSkipped('That player has already been in the destination group.');
        }

        $this->home = $home;
        $this->away = $away;
        $this->enrollment = $enrollment;

        $this->restore['enrollment'] = $enrollment->only([
            'status', 'withdrawal_date', 'withdrawal_reason', 'transferred_to_id',
            'training_group_id', 'training_program_id', 'enrollment_date', 'start_date',
            'end_date', 'payment_status', 'enrolled_by',
        ]);
        $this->restore['home_count'] = $home->current_count;
        $this->restore['home_status'] = $home->status->value;
        $this->restore['away_count'] = $away->current_count;
        $this->restore['away_status'] = $away->status->value;
    }

    public function test_a_player_can_be_moved_back_into_a_group_they_have_left(): void
    {
        $participantId = $this->enrollment->participant_id;

        // Out …
        $moved = $this->service->transfer($this->enrollment, $this->away, $this->actor);

        $this->assertSame($this->away->id, $moved->training_group_id);
        $this->assertSame(
            $this->away->training_program_id,
            $moved->training_program_id,
            'the subscription must follow the player to the destination group\'s programme'
        );

        // … and back. This is the leg that used to die on the unique constraint.
        $returned = $this->service->transfer($moved->fresh(), $this->home->fresh(), $this->actor);

        $this->assertSame($this->home->id, $returned->training_group_id);
        $this->assertSame($this->home->training_program_id, $returned->training_program_id);
        $this->assertSame('active', $returned->status->value);

        // Revived, not duplicated: the constraint still holds one row per
        // (player, group), and it is the row the player originally had.
        $this->assertSame($this->enrollment->id, $returned->id);

        $this->assertSame(1, Enrollment::withoutGlobalScopes()
            ->where('participant_id', $participantId)
            ->where('training_group_id', $this->home->id)
            ->count());

        // The outbound leg is closed off, not left looking active.
        $this->assertSame('cancelled', $moved->fresh()->status->value);

        // A revived enrolment must not still carry the withdrawal that ended it.
        $this->assertNull($returned->withdrawal_date);
        $this->assertNull($returned->transferred_to_id);
    }

    public function test_transferring_into_the_group_the_player_is_already_in_is_refused(): void
    {
        $this->expectException(DomainException::class);

        $this->service->transfer($this->enrollment, $this->home, $this->actor);
    }

    protected function tearDown(): void
    {
        if (isset($this->enrollment)) {
            Enrollment::withoutGlobalScopes()
                ->where('participant_id', $this->enrollment->participant_id)
                ->where('training_group_id', $this->away->id)
                ->delete();

            Enrollment::withoutGlobalScopes()
                ->where('id', $this->enrollment->id)
                ->update($this->restore['enrollment']);

            TrainingGroup::withoutGlobalScopes()->where('id', $this->home->id)->update([
                'current_count' => $this->restore['home_count'],
                'status' => $this->restore['home_status'],
            ]);

            TrainingGroup::withoutGlobalScopes()->where('id', $this->away->id)->update([
                'current_count' => $this->restore['away_count'],
                'status' => $this->restore['away_status'],
            ]);
        }

        parent::tearDown();
    }
}
