<?php

namespace Tests\Unit;

use App\Domain\Training\Support\BillingCycle;
use Illuminate\Support\Carbon;
use PHPUnit\Framework\TestCase;

/**
 * The one rule: every renewal is due on the 1st of a month, and lateness never
 * moves it.
 *
 * Before this class existed the rule was hand-written in five places and three
 * of them were wrong in different ways. These are the cases that were wrong.
 */
class BillingCycleTest extends TestCase
{
    protected function tearDown(): void
    {
        Carbon::setTestNow();
        parent::tearDown();
    }

    public function test_every_answer_is_the_first_of_a_month(): void
    {
        foreach (['2026-09-01', '2026-09-14', '2026-09-30', '2026-02-28'] as $date) {
            $this->assertSame('01', BillingCycle::startOf($date)->format('d'));
            $this->assertSame('01', BillingCycle::next($date)->format('d'));
        }
    }

    public function test_a_late_run_does_not_move_the_billing_day(): void
    {
        // The old advanceNextBillingDate() did $current->addMonth(). Billing a
        // cycle on the 5th because the 1st was missed pushed the anchor to the
        // 5th of the next month, and the next lateness pushed it again — the
        // academy's "everyone renews on the 1st" rule eroded a day at a time.
        $this->assertSame('2026-09-01', BillingCycle::next('2026-08-05')->toDateString());
        $this->assertSame('2026-09-01', BillingCycle::next('2026-08-01')->toDateString());
        $this->assertSame('2026-09-01', BillingCycle::next('2026-08-31')->toDateString());
    }

    public function test_a_join_date_mid_month_still_renews_on_the_first(): void
    {
        Carbon::setTestNow('2026-09-14 16:00:00');

        $this->assertSame('2026-10-01', BillingCycle::next()->toDateString());
    }

    public function test_cycle_lengths_follow_the_programme(): void
    {
        $this->assertSame(1, BillingCycle::lengthInMonths('monthly'));
        $this->assertSame(3, BillingCycle::lengthInMonths('quarterly'));
        $this->assertSame(6, BillingCycle::lengthInMonths('semi_annual'));
        $this->assertSame(12, BillingCycle::lengthInMonths('annual'));
        // A 12-week course bills quarterly; weeks round UP to whole months so a
        // player is never billed before the period they paid for has run out.
        $this->assertSame(3, BillingCycle::lengthInMonths('per_duration', 12));
        $this->assertSame(2, BillingCycle::lengthInMonths('per_duration', 5));
        $this->assertSame(1, BillingCycle::lengthInMonths('per_duration', null));
        // An unknown value must never silently become "never bill again".
        $this->assertSame(1, BillingCycle::lengthInMonths(null));
    }

    public function test_a_quarterly_cycle_lands_on_the_first_three_months_out(): void
    {
        $this->assertSame('2026-12-01', BillingCycle::next('2026-09-01', 'quarterly')->toDateString());
        $this->assertSame('2027-09-01', BillingCycle::next('2026-09-20', 'annual')->toDateString());
    }

    public function test_nothing_is_due_when_the_next_cycle_is_still_ahead(): void
    {
        $this->assertSame([], BillingCycle::dueCycles('2026-10-01', '2026-09-01'));
    }

    public function test_the_current_cycle_is_due_on_its_own_first_day(): void
    {
        $due = BillingCycle::dueCycles('2026-09-01', '2026-09-01');

        $this->assertCount(1, $due);
        $this->assertSame('2026-09-01', $due[0]->toDateString());
    }

    public function test_a_missed_month_is_caught_up_dated_to_its_own_first(): void
    {
        // The scheduler runs at 07:00. If the container is redeploying then, the
        // run is simply lost — this is what today's outage looked like. Running
        // on the 20th must still produce September's invoice dated 1 September,
        // not a September invoice dated the 20th and an August that never comes.
        $due = BillingCycle::dueCycles('2026-08-01', '2026-09-20');

        $this->assertSame(['2026-08-01', '2026-09-01'], array_map(fn ($d) => $d->toDateString(), $due));
    }

    public function test_a_drifted_anchor_is_repaired_to_the_first(): void
    {
        // A legacy row left mid-month by the old addMonth() drift still bills
        // its month, dated the 1st.
        $due = BillingCycle::dueCycles('2026-08-15', '2026-09-05');

        $this->assertSame(['2026-08-01', '2026-09-01'], array_map(fn ($d) => $d->toDateString(), $due));
    }

    public function test_one_run_is_capped_but_the_backlog_is_not_lost(): void
    {
        $due = BillingCycle::dueCycles('2026-01-01', '2026-09-01', 'monthly', null, 3);
        $this->assertCount(3, $due);
        $this->assertSame('2026-01-01', $due[0]->toDateString());

        // Six cycles are still owed after this run takes three. The command
        // reports that number rather than letting the cap read as "all done".
        $this->assertSame(6, BillingCycle::remainingAfter('2026-01-01', '2026-09-01', 'monthly', null, 3));
        $this->assertSame(0, BillingCycle::remainingAfter('2026-08-01', '2026-09-01', 'monthly', null, 3));
    }

    public function test_the_month_key_is_what_subscription_line_reads(): void
    {
        // SubscriptionLine::monthsFromMetadata() requires exactly 'YYYY-MM'.
        $this->assertSame('2026-09', BillingCycle::monthKey('2026-09-20'));
        $this->assertMatchesRegularExpression('/^\d{4}-\d{2}$/', BillingCycle::monthKey('2026-01-31'));
    }

    public function test_the_month_label_is_arabic_and_readable_by_subscription_line(): void
    {
        $this->assertSame('سبتمبر 2026', BillingCycle::monthLabel('2026-09-20'));
        $this->assertSame('يناير 2027', BillingCycle::monthLabel('2027-01-01'));
    }
}
