<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Tests\Components;

use Doctrine\Common\Collections\ArrayCollection;
use Ferienpass\CmsBundle\Components\OfferSchedule;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferDate;
use Ferienpass\CoreBundle\Entity\PageConfig;
use Ferienpass\CoreBundle\Settings\PageConfigManager;
use PHPUnit\Framework\TestCase;

/**
 * Covers the schedule classifier + range/uniform-time helpers that drive the
 * kind-aware tile rendering. Most tests fabricate a mock Offer with a fixed
 * `OfferDate` collection so we can pin down classification rules without
 * needing the DB or Recurr expansion.
 */
final class OfferScheduleTest extends TestCase
{
    // ---- kind() ----

    public function testKindOngoingForOfferWithoutDates(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([]);

        self::assertSame('ongoing', $schedule->kind());
    }

    public function testKindSingleForOneAdHocDate(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-05-26 09:30', '2026-05-26 17:30'),
        ]);

        self::assertSame('single', $schedule->kind());
    }

    public function testKindSeriesForMultiplePlainDates(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-08-03 10:00', '2026-08-03 12:30'),
            $this->date('2026-08-04 10:00', '2026-08-04 12:30'),
            $this->date('2026-08-05 10:00', '2026-08-05 12:30'),
        ]);

        self::assertSame('series', $schedule->kind());
    }

    public function testKindRecurringWhenAllDatesHaveRecurrence(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->recurringDate('2026-05-19 16:00', '2026-05-19 19:00', 'FREQ=WEEKLY;BYDAY=TU'),
            $this->recurringDate('2026-05-21 16:00', '2026-05-21 19:00', 'FREQ=WEEKLY;BYDAY=TH'),
        ]);

        self::assertSame('recurring', $schedule->kind());
    }

    public function testKindSeriesForMixedRecurringAndAdHoc(): void
    {
        // One weekly recurrence + one one-off → can't honestly call it
        // either kind; the renderer treats it as a series.
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->recurringDate('2026-05-19 16:00', '2026-05-19 19:00', 'FREQ=WEEKLY;BYDAY=TU'),
            $this->date('2026-07-01 16:00', '2026-07-01 19:00'),
        ]);

        self::assertSame('series', $schedule->kind());
    }

    // ---- seriesIsRange() / seriesSpansMonths() ----

    public function testSeriesIsRangeForConsecutiveDaysSameMonth(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-08-03 10:00', '2026-08-03 12:30'),
            $this->date('2026-08-04 10:00', '2026-08-04 12:30'),
            $this->date('2026-08-05 10:00', '2026-08-05 12:30'),
            $this->date('2026-08-06 10:00', '2026-08-06 12:30'),
            $this->date('2026-08-07 10:00', '2026-08-07 12:30'),
        ]);

        self::assertTrue($schedule->seriesIsRange());
        self::assertFalse($schedule->seriesSpansMonths());
    }

    public function testSeriesIsRangeForConsecutiveDaysAcrossMonths(): void
    {
        // Camp Mon 30.06 → Fri 04.07 — used to fail because the old check
        // required the same Ym.
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-06-30 14:00', '2026-06-30 18:00'),
            $this->date('2026-07-01 14:00', '2026-07-01 18:00'),
            $this->date('2026-07-02 14:00', '2026-07-02 18:00'),
            $this->date('2026-07-03 14:00', '2026-07-03 18:00'),
            $this->date('2026-07-04 14:00', '2026-07-04 18:00'),
        ]);

        self::assertTrue($schedule->seriesIsRange());
        self::assertTrue($schedule->seriesSpansMonths());
    }

    public function testSeriesIsRangeFalseWhenDaysHaveGaps(): void
    {
        // Mon + Wed + Fri — not a continuous run.
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-08-03 10:00', '2026-08-03 12:00'),
            $this->date('2026-08-05 10:00', '2026-08-05 12:00'),
            $this->date('2026-08-07 10:00', '2026-08-07 12:00'),
        ]);

        self::assertFalse($schedule->seriesIsRange());
    }

    public function testSeriesIsRangeFalseForSingleOccurrence(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-05-26 09:30', '2026-05-26 17:30'),
        ]);

        self::assertFalse($schedule->seriesIsRange());
    }

    // ---- uniformTimeOfDay() ----

    public function testUniformTimeOfDayTrueWhenAllOccurrencesShareTime(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-08-03 10:00', '2026-08-03 12:30'),
            $this->date('2026-08-04 10:00', '2026-08-04 12:30'),
            $this->date('2026-08-05 10:00', '2026-08-05 12:30'),
        ]);

        self::assertTrue($schedule->uniformTimeOfDay());
    }

    public function testUniformTimeOfDayFalseWhenTimesDiffer(): void
    {
        $schedule = $this->newSchedule();
        $schedule->offer = $this->offerWithDates([
            $this->date('2026-08-03 10:00', '2026-08-03 12:30'),
            $this->date('2026-08-04 14:00', '2026-08-04 16:30'),
        ]);

        self::assertFalse($schedule->uniformTimeOfDay());
    }

    // ---- alternativesCount() ----

    public function testAlternativesCountForBaseReportsVariantCount(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getDates')->willReturn(new ArrayCollection());
        $offer->method('hasVariants')->willReturn(true);
        $offer->method('getVariants')->willReturn(new ArrayCollection([
            $this->createMock(OfferInterface::class),
            $this->createMock(OfferInterface::class),
            $this->createMock(OfferInterface::class),
        ]));
        $offer->method('getVariantBase')->willReturn(null);

        $schedule = $this->newSchedule();
        $schedule->offer = $offer;

        self::assertSame(3, $schedule->alternativesCount());
    }

    public function testAlternativesCountForVariantReportsSiblingCount(): void
    {
        // A variant: its base has 3 children (including this one) → 2 siblings.
        $base = $this->createMock(OfferInterface::class);
        $base->method('getVariants')->willReturn(new ArrayCollection([
            $this->createMock(OfferInterface::class),
            $this->createMock(OfferInterface::class),
            $this->createMock(OfferInterface::class),
        ]));

        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getDates')->willReturn(new ArrayCollection());
        $offer->method('hasVariants')->willReturn(false);
        $offer->method('getVariantBase')->willReturn($base);

        $schedule = $this->newSchedule();
        $schedule->offer = $offer;

        self::assertSame(2, $schedule->alternativesCount());
    }

    public function testAlternativesCountForStandaloneOfferIsZero(): void
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getDates')->willReturn(new ArrayCollection());
        $offer->method('hasVariants')->willReturn(false);
        $offer->method('getVariantBase')->willReturn(null);

        $schedule = $this->newSchedule();
        $schedule->offer = $offer;

        self::assertSame(0, $schedule->alternativesCount());
    }

    // ---- occurrences() memoization ----

    public function testOccurrencesIsMemoized(): void
    {
        $date = $this->createMock(OfferDate::class);
        // Expect getOccurrences() to be called at most once across many
        // helper invocations.
        $date->expects(self::once())->method('getOccurrences')->willReturn([
            ['begin' => new \DateTime('2026-05-26 09:00'), 'end' => new \DateTime('2026-05-26 11:00')],
        ]);
        $date->method('getRecurrence')->willReturn(null);

        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getDates')->willReturn(new ArrayCollection([$date]));
        $offer->method('hasVariants')->willReturn(false);
        $offer->method('getVariantBase')->willReturn(null);

        $schedule = $this->newSchedule();
        $schedule->offer = $offer;

        // Touch several methods that all flow through occurrences().
        $schedule->kind();
        $schedule->uniformTimeOfDay();
        $schedule->firstBegin();
        $schedule->lastBegin();
        $schedule->occurrences();
    }

    // ---- helpers ----

    private function newSchedule(): OfferSchedule
    {
        $manager = $this->createStub(PageConfigManager::class);
        $manager->method('getPageConfig')->willReturn(new PageConfig());

        return new OfferSchedule($manager);
    }

    private function offerWithDates(array $dates): OfferInterface
    {
        $offer = $this->createMock(OfferInterface::class);
        $offer->method('getDates')->willReturn(new ArrayCollection($dates));
        $offer->method('hasVariants')->willReturn(false);
        $offer->method('getVariantBase')->willReturn(null);

        return $offer;
    }

    private function date(string $begin, string $end): OfferDate
    {
        $b = new \DateTime($begin);
        $e = new \DateTime($end);
        $d = $this->createMock(OfferDate::class);
        $d->method('getBegin')->willReturn($b);
        $d->method('getEnd')->willReturn($e);
        $d->method('getRecurrence')->willReturn(null);
        $d->method('getOccurrences')->willReturn([['begin' => $b, 'end' => $e]]);

        return $d;
    }

    private function recurringDate(string $begin, string $end, string $rrule): OfferDate
    {
        $b = new \DateTime($begin);
        $e = new \DateTime($end);
        $d = $this->createMock(OfferDate::class);
        $d->method('getBegin')->willReturn($b);
        $d->method('getEnd')->willReturn($e);
        $d->method('getRecurrence')->willReturn($rrule);
        // Test the classifier in isolation; the expansion of recurrences is
        // OfferDate::getOccurrences's job and we mock it.
        $d->method('getOccurrences')->willReturn([['begin' => $b, 'end' => $e]]);

        return $d;
    }
}
