<?php

namespace Tests\Feature;

use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Models\TrainingSchedule;
use App\Livewire\Programs\ProgramForm;
use App\Models\User;
use Livewire\Livewire;
use Tests\TestCase;

/**
 * Setting a programme's weekly timetable without the grid.
 *
 * A club that trains Sunday and Tuesday from four to half five should be able
 * to say so on the programme form and be done — no facility, no space, no
 * placement. The visual builder still owns anything actually placed on the
 * grid, and this form must not touch those rows: clearing a checkbox here
 * cannot be allowed to strip a space reservation off a scheduled session.
 *
 * Runs against a restored Postgres tenant; skips on the default SQLite suite:
 *
 *   DB_CONNECTION=pgsql DB_DATABASE=oc_sport_test ./vendor/bin/phpunit --filter ProgramScheduleTest
 */
class ProgramScheduleTest extends TestCase
{
    private TrainingProgram $program;

    /** Everything on the group created after this moment is this test's doing. */
    private \Carbon\CarbonInterface $startedAt;

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

        // A programme whose default group carries no timetable yet, so the test
        // adds rows rather than editing a real one, and teardown can remove
        // exactly what it made.
        $program = TrainingProgram::withoutGlobalScopes()
            ->whereHas('groups', fn ($q) => $q->whereDoesntHave('schedules'))
            ->first();

        if (! $program) {
            $this->markTestSkipped('No programme with an unscheduled group in the restored tenant.');
        }

        $this->program = $program;
        $this->startedAt = now()->subSecond();
    }

    protected function tearDown(): void
    {
        if (isset($this->program)) {
            $this->clearSchedules();
        }

        parent::tearDown();
    }

    /**
     * Remove every schedule row on the default group, and the sessions saving
     * one generated.
     *
     * The sessions are the point: TrainingSchedule::saved() generates a week of
     * them, and training_sessions.schedule_id is a hard foreign key — which is
     * precisely why the service under test retires a row instead of deleting
     * it.
     */
    private function clearSchedules(): void
    {
        $group = $this->program->defaultGroup();

        if (! $group) {
            return;
        }

        // Sessions first, and by creation time rather than by schedule id: if
        // the assertions failed part-way the schedule rows may already be gone,
        // and a session left pointing at nothing is a row on no branch that
        // shows on no screen — which the isolation sweep rightly fails on.
        \App\Domain\Training\Models\TrainingSession::withoutGlobalScopes()
            ->where('training_group_id', $group->id)
            ->where('created_at', '>=', $this->startedAt)
            ->forceDelete();

        TrainingSchedule::withoutGlobalScopes()
            ->where('training_group_id', $group->id)
            ->delete();
    }

    private function anOwner(): User
    {
        $user = User::withoutGlobalScopes()
            ->whereHas('primaryRole', fn ($q) => $q->where('slug', 'academy_owner'))
            ->first();

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

        return $user;
    }

    /** The rows this form owns: the default group's, with no facility. */
    private function managedRows()
    {
        return TrainingSchedule::withoutGlobalScopes()
            ->where('training_group_id', $this->program->defaultGroup()->id)
            ->whereNull('facility_id')
            ->get();
    }

    public function test_the_edit_screen_renders(): void
    {
        $this->actingAs($this->anOwner())
            ->get(route('programs.edit', $this->program))
            ->assertOk()
            ->assertSee('مواعيد التمرين', escape: false);
    }

    public function test_days_and_hours_alone_write_a_timetable(): void
    {
        $this->assertNotNull($this->program->defaultGroup(), 'Every programme is created with a default group.');

        app(\App\Domain\Training\Services\TrainingProgramService::class)
            ->syncDefaultGroupSchedule($this->program, [0, 2], '16:00', '17:30');

        $rows = $this->managedRows()->where('is_active', true);

        $this->assertEqualsCanonicalizing([0, 2], $rows->pluck('day_of_week')->map('intval')->all());

        foreach ($rows as $row) {
            $this->assertSame('16:00', substr((string) $row->start_time, 0, 5));
            $this->assertSame('17:30', substr((string) $row->end_time, 0, 5));
            $this->assertNull($row->facility_id, 'No facility should be demanded to record a timetable.');
        }
    }

    public function test_a_day_removed_from_the_selection_is_deactivated_not_deleted(): void
    {
        $service = app(\App\Domain\Training\Services\TrainingProgramService::class);

        $service->syncDefaultGroupSchedule($this->program, [0, 2], '16:00', '17:30');

        $tuesdayId = $this->managedRows()->firstWhere('day_of_week', 2)?->id;

        $this->assertNotNull($tuesdayId, 'Tuesday was never written.');

        $service->syncDefaultGroupSchedule($this->program, [0], '16:00', '17:30');

        $tuesday = TrainingSchedule::withoutGlobalScopes()->find($tuesdayId);

        // Sessions already generated point at this row and attendance hangs off
        // those sessions, so dropping the row would orphan them.
        $this->assertNotNull($tuesday, 'The row was deleted rather than retired.');
        $this->assertFalse((bool) $tuesday->is_active);
    }

    public function test_a_row_the_grid_placed_is_left_alone(): void
    {
        $group = $this->program->defaultGroup();

        $facility = \App\Domain\Facility\Models\Facility::withoutGlobalScopes()->first();

        if (! $facility) {
            $this->markTestSkipped('No facility in the restored tenant to place a row at.');
        }

        $placed = TrainingSchedule::withoutGlobalScopes()->create([
            'academy_id' => $group->academy_id,
            'training_group_id' => $group->id,
            'facility_id' => $facility->id,
            'day_of_week' => 4,
            'start_time' => '19:00',
            'end_time' => '20:30',
            'effective_from' => now()->toDateString(),
            'is_active' => true,
        ]);

        // Thursday is deliberately absent from the selection below.
        app(\App\Domain\Training\Services\TrainingProgramService::class)
            ->syncDefaultGroupSchedule($this->program, [0], '16:00', '17:30');

        $placed->refresh();

        $this->assertTrue((bool) $placed->is_active, 'The form retired a row belonging to the grid.');
        $this->assertSame($facility->id, $placed->facility_id);
        // tearDown() clears both rows and the sessions they generated.
    }

    public function test_a_new_programme_defaults_to_renewing_on_the_first(): void
    {
        Livewire::actingAs($this->anOwner())
            ->test(ProgramForm::class)
            ->assertSet('renewal_policy', 'auto_renew')
            ->assertSet('billing_cycle', 'monthly')
            ->assertSet('billing_day', 1);
    }
}
