<?php

namespace Tests\Feature;

use App\Domain\Shared\Enums\ProrationMode;
use App\Domain\Shared\Services\ProrationService;
use App\Domain\Shared\Services\SettingsService;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Services\SessionCountService;
use Carbon\Carbon;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;

/**
 * What a mid-month joiner is charged.
 *
 * The old rule counted days on a hardcoded 30-day month: joining on the 22nd
 * bought "9 of 30 days", regardless of whether the programme trained nine times
 * in those days or twice. A programme meeting Sunday and Tuesday holds nine
 * sessions in September 2026 and exactly three of them fall on or after the
 * 22nd, so three ninths is what the player owes — not the nine thirtieths the
 * calendar arithmetic produced.
 *
 * September 2026 is the month under test throughout: it opens on a Tuesday and
 * carries four Sundays and five Tuesdays.
 */
class SessionProrationTest extends TestCase
{
    private const ACADEMY_ID = 1;
    private const BRANCH_ID = 1;
    private const GROUP_ID = 1;

    /** 500 EGP a month, in piasters. */
    private const MONTHLY_FEE = 50000;

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

        if (config('database.default') !== 'sqlite') {
            $this->markTestSkipped('Builds its own schema; runs on the in-memory SQLite connection.');
        }

        // SettingsService reads nothing at all without an academy in the
        // container, so proration would read as off and every price assertion
        // below would pass for the wrong reason.
        app()->instance('current_academy', (object) ['id' => self::ACADEMY_ID]);

        $this->createMinimalSchema();

        DB::table('training_groups')->insert([
            'id' => self::GROUP_ID,
            'academy_id' => self::ACADEMY_ID,
            'branch_id' => self::BRANCH_ID,
            'name_ar' => 'مجموعة الاختبار',
            'deleted_at' => null,
        ]);

        // Sunday (0) and Tuesday (2), the whole month through.
        $this->addSchedule(dayOfWeek: 0);
        $this->addSchedule(dayOfWeek: 2);
    }

    // ---- counting ---------------------------------------------------------

    public function test_a_month_holds_as_many_sessions_as_the_timetable_says(): void
    {
        $counts = $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-22'));

        $this->assertSame(9, $counts['total'], 'Four Sundays and five Tuesdays in September 2026.');
        $this->assertSame(3, $counts['remaining'], 'The 22nd, the 27th and the 29th are still to come.');
    }

    public function test_the_join_day_itself_counts_as_a_remaining_session(): void
    {
        // The 22nd IS a Tuesday. A player who signs up that afternoon and
        // trains that evening has been sold that session.
        $counts = $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-22'));

        $this->assertSame(3, $counts['remaining']);

        $dayAfter = $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-23'));

        $this->assertSame(2, $dayAfter['remaining'], 'By Wednesday that session is spent.');
    }

    public function test_a_holiday_removes_a_session_from_both_figures(): void
    {
        DB::table('holidays')->insert([
            'academy_id' => self::ACADEMY_ID,
            'branch_id' => null,
            'name' => 'Test holiday',
            'name_ar' => 'إجازة',
            'date' => '2026-09-27',
            'affects' => 'training',
        ]);

        $counts = $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-22'));

        $this->assertSame(8, $counts['total']);
        $this->assertSame(2, $counts['remaining']);
    }

    public function test_a_holiday_that_does_not_stop_training_is_ignored(): void
    {
        DB::table('holidays')->insert([
            'academy_id' => self::ACADEMY_ID,
            'branch_id' => null,
            'name' => 'Office closed',
            'name_ar' => 'إجازة إدارية',
            'date' => '2026-09-27',
            'affects' => 'staff',
        ]);

        $this->assertSame(9, $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-22'))['total']);
    }

    // ---- what it costs ----------------------------------------------------

    public function test_remaining_sessions_is_the_price_of_a_session_times_what_is_left(): void
    {
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            ProrationMode::RemainingSessions,
        );

        $this->assertTrue($result->applied);
        // 50000 × 3 / 9 = 16666.67, rounded up.
        $this->assertSame(16667, $result->proratedAmount);
        $this->assertSame(9, $result->totalSessions);
        $this->assertSame(3, $result->remainingSessions);
        $this->assertStringContainsString('متناسب', $result->description, 'ParticipantBillingService reads this word off the invoice line.');
    }

    public function test_the_old_day_based_answer_is_no_longer_what_is_charged(): void
    {
        // 1 + 30 − 22 = 9 days of 30, which the previous rule billed as 15000.
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            ProrationMode::RemainingSessions,
        );

        $this->assertNotSame(15000, $result->proratedAmount);
    }

    public function test_joining_before_the_first_session_buys_the_whole_month(): void
    {
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-01'),
            $this->group(),
            ProrationMode::RemainingSessions,
        );

        $this->assertFalse($result->applied);
        $this->assertSame(self::MONTHLY_FEE, $result->proratedAmount);
    }

    public function test_joining_after_the_last_session_owes_nothing_for_this_month(): void
    {
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-30'),
            $this->group(),
            ProrationMode::RemainingSessions,
        );

        $this->assertSame(0, $result->proratedAmount, 'The last session was the 29th.');
    }

    public function test_half_month_is_half_the_fee(): void
    {
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            ProrationMode::HalfMonth,
        );

        $this->assertTrue($result->applied);
        $this->assertSame(25000, $result->proratedAmount);
    }

    public function test_full_month_charges_the_whole_fee_whenever_it_is_chosen(): void
    {
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            ProrationMode::FullMonth,
        );

        $this->assertFalse($result->applied);
        $this->assertSame(self::MONTHLY_FEE, $result->proratedAmount);
    }

    // ---- the guards -------------------------------------------------------

    public function test_a_programme_with_no_timetable_is_charged_a_full_month_rather_than_blocked(): void
    {
        DB::table('training_schedules')->delete();

        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            ProrationMode::RemainingSessions,
        );

        $this->assertFalse($result->applied);
        $this->assertSame(self::MONTHLY_FEE, $result->proratedAmount);
        $this->assertNotSame('', $result->description, 'The desk is told why it was not prorated.');
    }

    public function test_a_mode_posted_by_hand_cannot_switch_proration_on(): void
    {
        $this->setSetting('enrollment.allow_proration', '0');

        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            ProrationMode::HalfMonth,
        );

        $this->assertFalse($result->applied);
        $this->assertSame(self::MONTHLY_FEE, $result->proratedAmount, 'The academy setting is the gate, not the posted mode.');
    }

    public function test_an_unrecognised_mode_falls_back_to_the_default(): void
    {
        $result = $this->proration()->calculate(
            self::MONTHLY_FEE,
            Carbon::parse('2026-09-22'),
            $this->group(),
            'pay_whatever_you_like',
        );

        $this->assertSame(ProrationMode::RemainingSessions, $result->mode);
        $this->assertSame(16667, $result->proratedAmount);
    }

    public function test_an_inactive_schedule_row_holds_no_sessions(): void
    {
        DB::table('training_schedules')->update(['is_active' => false]);

        $this->assertSame(0, $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-22'))['total']);
    }

    public function test_a_schedule_that_has_not_started_yet_holds_no_sessions(): void
    {
        DB::table('training_schedules')->update(['effective_from' => '2026-10-01']);

        $this->assertSame(0, $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-22'))['total']);
    }

    public function test_two_slots_on_the_same_weekday_are_two_sessions(): void
    {
        // An early group and a late one, both on Sunday.
        $this->addSchedule(dayOfWeek: 0, start: '18:00', end: '19:30');

        $counts = $this->counter()->forMonth($this->group(), Carbon::parse('2026-09-01'));

        $this->assertSame(13, $counts['total'], 'Five Tuesdays plus four Sundays twice over.');
    }

    // ---- harness ----------------------------------------------------------

    private function counter(): SessionCountService
    {
        return app(SessionCountService::class);
    }

    private function proration(): ProrationService
    {
        return app(ProrationService::class);
    }

    private function group(): TrainingGroup
    {
        return TrainingGroup::findOrFail(self::GROUP_ID);
    }

    private function addSchedule(int $dayOfWeek, string $start = '16:00', string $end = '17:30'): void
    {
        DB::table('training_schedules')->insert([
            'academy_id' => self::ACADEMY_ID,
            'training_group_id' => self::GROUP_ID,
            'facility_id' => null,
            'day_of_week' => $dayOfWeek,
            'start_time' => $start,
            'end_time' => $end,
            'effective_from' => '2026-01-01',
            'effective_until' => null,
            'is_active' => true,
        ]);
    }

    private function setSetting(string $key, string $value): void
    {
        DB::table('system_settings')->updateOrInsert(
            ['academy_id' => self::ACADEMY_ID, 'key' => $key],
            ['value' => $value, 'type' => 'boolean', 'group' => 'enrollment'],
        );

        // SettingsService memoises; a fresh instance is what a fresh request
        // would get.
        app()->forgetInstance(SettingsService::class);
    }

    private function createMinimalSchema(): void
    {
        Schema::create('training_groups', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('academy_id');
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->string('name_ar');
            $table->timestamp('deleted_at')->nullable();
        });

        Schema::create('training_schedules', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('academy_id');
            $table->unsignedBigInteger('training_group_id');
            $table->unsignedBigInteger('facility_id')->nullable();
            $table->integer('day_of_week');
            $table->string('start_time');
            $table->string('end_time');
            $table->date('effective_from')->nullable();
            $table->date('effective_until')->nullable();
            $table->boolean('is_active')->default(true);
        });

        Schema::create('holidays', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('academy_id');
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->string('name');
            $table->string('name_ar');
            $table->date('date');
            $table->string('affects')->default('all');
        });

        Schema::create('system_settings', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('academy_id')->nullable();
            $table->string('key');
            $table->text('value')->nullable();
            $table->string('type')->default('string');
            $table->string('group')->default('general');
            $table->timestamps();
        });

        // Proration on, renewing on the first — the configuration the feature
        // is being asked about.
        $this->setSetting('enrollment.allow_proration', '1');
        $this->setSetting('enrollment.renewal_day', '1');
    }
}
