<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\Services\DueDateService;
use Carbon\Carbon;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

/**
 * Tests para DueDateService.
 *
 * Verifica que las fechas de vencimiento se calculen correctamente
 * usando ISO-8601 para días de semana (1=Lun, 7=Dom).
 */
class DueDateServiceTest extends TestCase
{
    private DueDateService $service;

    protected function setUp(): void
    {
        parent::setUp();
        $this->service = new DueDateService;
    }

    /*=========================================================
    | WEEKLY - ISO-8601 (1=Monday, 7=Sunday)
    =========================================================*/

    #[DataProvider('weeklyFirstDueDateProvider')]
    public function test_weekly_first_due_date_respects_iso_weekday(
        string $startDate,
        int $dayOfWeekIso,
        int $expectedDayOfWeekIso,
        string $description
    ): void {
        $start = Carbon::parse($startDate);

        $result = $this->service->firstDueDate($start, 'weekly', $dayOfWeekIso);

        $this->assertEquals(
            $expectedDayOfWeekIso,
            $result->dayOfWeekIso,
            sprintf(
                '%s - Start: %s (%s), Expected ISO day: %d (%s), Got: %s (%s, ISO day: %d)',
                $description,
                $start->toDateString(),
                $start->englishDayOfWeek,
                $expectedDayOfWeekIso,
                $this->service->getWeekdayNameIso($expectedDayOfWeekIso),
                $result->toDateString(),
                $result->englishDayOfWeek,
                $result->dayOfWeekIso
            )
        );

        // La fecha debe ser posterior a start
        $this->assertTrue(
            $result->gt($start),
            'La primera cuota debe ser posterior a la fecha de inicio'
        );
    }

    public static function weeklyFirstDueDateProvider(): array
    {
        return [
            // [startDate, dayOfWeekIso, expectedDayOfWeekIso, description]

            // BUG ORIGINAL: due_day_1 = 3 (Miércoles) generaba Martes
            ['2024-01-15', 3, 3, 'Miércoles ISO=3 desde Lunes debe dar Miércoles'],
            ['2024-01-16', 3, 3, 'Miércoles ISO=3 desde Martes debe dar Miércoles'],
            ['2024-01-17', 3, 3, 'Miércoles ISO=3 desde Miércoles debe dar próximo Miércoles'],

            // Todos los días de la semana
            ['2024-01-15', 1, 1, 'Lunes ISO=1'],
            ['2024-01-15', 2, 2, 'Martes ISO=2'],
            ['2024-01-15', 3, 3, 'Miércoles ISO=3'],
            ['2024-01-15', 4, 4, 'Jueves ISO=4'],
            ['2024-01-15', 5, 5, 'Viernes ISO=5'],
            ['2024-01-15', 6, 6, 'Sábado ISO=6'],
            ['2024-01-15', 7, 7, 'Domingo ISO=7'],

            // Edge case: domingo en ISO es 7 (no 0)
            ['2024-01-20', 7, 7, 'Domingo ISO=7 desde Sábado'],
            ['2024-01-21', 7, 7, 'Domingo ISO=7 desde Domingo debe dar próximo Domingo'],
        ];
    }

    /**
     * Verifica que todas las cuotas semanales caigan en el mismo día de semana.
     */
    public function test_weekly_installments_maintain_same_weekday(): void
    {
        $start = Carbon::parse('2024-01-15'); // Lunes
        $dayOfWeekIso = 3; // Miércoles
        $installmentCount = 10;

        $dates = [];
        $current = $this->service->firstDueDate($start, 'weekly', $dayOfWeekIso);
        $dates[] = $current->copy();

        for ($i = 1; $i < $installmentCount; $i++) {
            $current = $this->service->nextDueDate($current, 'weekly', $dayOfWeekIso);
            $dates[] = $current->copy();
        }

        foreach ($dates as $index => $date) {
            $this->assertEquals(
                $dayOfWeekIso,
                $date->dayOfWeekIso,
                sprintf(
                    'Cuota #%d: %s debería ser %s (ISO %d), pero es %s (ISO %d)',
                    $index + 1,
                    $date->toDateString(),
                    $this->service->getWeekdayNameIso($dayOfWeekIso),
                    $dayOfWeekIso,
                    $date->englishDayOfWeek,
                    $date->dayOfWeekIso
                )
            );
        }

        // Verificar que las fechas estén separadas por exactamente 7 días
        for ($i = 1; $i < count($dates); $i++) {
            $diff = $dates[$i - 1]->diffInDays($dates[$i]);
            $this->assertEquals(7, $diff, "Cuotas #{$i} y #".($i + 1).' deben estar separadas por 7 días');
        }
    }

    /**
     * Verifica el caso específico del bug: due_day_1 = 3 generando Martes.
     */
    public function test_wednesday_is_not_generated_as_tuesday(): void
    {
        // Este test verifica específicamente el bug reportado
        $start = Carbon::parse('2024-01-15'); // Lunes 15 de enero 2024
        $wednesdayIso = 3;

        $firstDue = $this->service->firstDueDate($start, 'weekly', $wednesdayIso);

        // Debe ser Miércoles 17 de enero 2024
        $this->assertEquals('2024-01-17', $firstDue->toDateString());
        $this->assertEquals(3, $firstDue->dayOfWeekIso); // Miércoles
        $this->assertNotEquals(2, $firstDue->dayOfWeekIso); // NO debe ser Martes
    }

    /*=========================================================
    | MONTHLY
    =========================================================*/

    public function test_monthly_first_due_date(): void
    {
        $start = Carbon::parse('2024-01-15');

        // Día 20 del mes actual (después de start)
        $result = $this->service->firstDueDate($start, 'monthly', 20);
        $this->assertEquals('2024-01-20', $result->toDateString());

        // Día 10 del mes actual (antes de start) - debe ir al próximo mes
        $result = $this->service->firstDueDate($start, 'monthly', 10);
        $this->assertEquals('2024-02-10', $result->toDateString());
    }

    public function test_monthly_handles_short_months(): void
    {
        // Febrero solo tiene 28/29 días
        $start = Carbon::parse('2024-01-31');

        $result = $this->service->firstDueDate($start, 'monthly', 31);

        // Debe usar el día máximo normalizado (28) para evitar overflow
        $this->assertTrue($result->day <= 29);
    }

    /*=========================================================
    | BIWEEKLY (Quincenal)
    =========================================================*/

    public function test_biweekly_first_due_date(): void
    {
        $start = Carbon::parse('2024-01-10');

        // day1=1, day2=15 - desde el 10, primera fecha es el 15
        $result = $this->service->firstDueDate($start, 'biweekly', 1, 15);
        $this->assertEquals('2024-01-15', $result->toDateString());
    }

    public function test_biweekly_alternates_between_days(): void
    {
        $day1 = 1;
        $day2 = 15;

        $current = Carbon::parse('2024-01-01');

        // Primera cuota el día 1
        $this->assertEquals(1, $current->day);

        // Siguiente debe ser día 15
        $next = $this->service->nextDueDate($current, 'biweekly', $day1, $day2);
        $this->assertEquals(15, $next->day);
        $this->assertEquals('2024-01', $next->format('Y-m'));

        // Siguiente debe ser día 1 del próximo mes
        $next = $this->service->nextDueDate($next, 'biweekly', $day1, $day2);
        $this->assertEquals(1, $next->day);
        $this->assertEquals('2024-02', $next->format('Y-m'));
    }

    /*=========================================================
    | DAILY
    =========================================================*/

    public function test_daily_adds_one_day(): void
    {
        $start = Carbon::parse('2024-01-15');

        $result = $this->service->firstDueDate($start, 'daily', null);
        $this->assertEquals('2024-01-16', $result->toDateString());

        $result = $this->service->nextDueDate($result, 'daily', null);
        $this->assertEquals('2024-01-17', $result->toDateString());
    }

    /*=========================================================
    | EDGE CASES
    =========================================================*/

    public function test_invalid_periodicity_throws_exception(): void
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("Periodicidad 'invalid' inválida");

        $this->service->firstDueDate(Carbon::now(), 'invalid', 1);
    }

    public function test_null_day_for_weekly_uses_default(): void
    {
        $start = Carbon::parse('2024-01-15');

        $result = $this->service->firstDueDate($start, 'weekly', null);

        // Debe ser exactamente una semana después
        $this->assertEquals('2024-01-22', $result->toDateString());
    }

    public function test_invalid_day_of_week_uses_default(): void
    {
        $start = Carbon::parse('2024-01-15');

        // Día 8 no es válido en ISO-8601 (debe ser 1-7)
        $result = $this->service->firstDueDate($start, 'weekly', 8);

        // Debe usar el default (una semana después)
        $this->assertEquals('2024-01-22', $result->toDateString());
    }

    /*=========================================================
    | HELPER METHODS
    =========================================================*/

    public function test_validate_weekday(): void
    {
        $wednesday = Carbon::parse('2024-01-17'); // Miércoles

        $this->assertTrue($this->service->validateWeekday($wednesday, 3));
        $this->assertFalse($this->service->validateWeekday($wednesday, 2));
    }

    public function test_get_weekday_name_iso(): void
    {
        $this->assertEquals('Lunes', $this->service->getWeekdayNameIso(1));
        $this->assertEquals('Miércoles', $this->service->getWeekdayNameIso(3));
        $this->assertEquals('Domingo', $this->service->getWeekdayNameIso(7));
    }
}
