<?php

namespace Velis\Test\Dto\Validators;

use DateTimeImmutable;
use Velis\Dto\Validators\DateAfterValidator;
use Velis\TestCase;

final class DateAfterValidatorTest extends TestCase
{
    /**
     * @dataProvider provideDateTimeComparisonData
     */
    public function testDateTimeComparison(
        DateTimeImmutable $afterDate,
        string $testValue,
        bool $expected,
        string $testCase,
    ): void {
        // Given a DateAfterValidator with a specific after date
        $validator = new DateAfterValidator(after: $afterDate);

        // When validating a date string
        $result = $validator->validate($testValue);

        // Then the result matches expectations
        self::assertEquals($expected, $result, "Failed test case: $testCase");

        if (!$expected) {
            // And the error message is properly formatted
            $expectedMessage = sprintf('Date should be after %s.', $afterDate->format('c'));
            self::assertEquals($expectedMessage, $validator->getMessage());
        }
    }

    /**
     * @return array<string<array<mixed>>>
     */
    public function provideDateTimeComparisonData(): array
    {
        return [
            'date after reference should pass' => [
                new DateTimeImmutable('2023-01-01'),
                '2023-01-02',
                true,
                'value after reference date',
            ],
            'date same as reference should pass' => [
                new DateTimeImmutable('2023-01-01'),
                '2023-01-01',
                true,
                'value same as reference date',
            ],
            'date before reference should fail' => [
                new DateTimeImmutable('2023-01-02'),
                '2023-01-01',
                false,
                'value before reference date',
            ],
            'future date comparison should work' => [
                new DateTimeImmutable('2025-12-31'),
                '2026-01-01',
                true,
                'future date comparison',
            ],
        ];
    }

    /**
     * @param array<string, mixed> $dtoData
     * @dataProvider provideFieldReferenceData
     */
    public function testFieldReferenceValidation(
        array $dtoData,
        string $fieldReference,
        string $testValue,
        bool $expected,
        string $testCase,
    ): void {
        // Given a DateAfterValidator that references another field
        $validator = new DateAfterValidator(after: [$fieldReference]);
        $validator->setDtoData($dtoData);

        // When validating against the referenced field
        $result = $validator->validate($testValue);

        // Then the result matches expectations
        self::assertEquals($expected, $result, "Failed test case: {$testCase}");
    }

    /**
     * @return array<string<array<mixed>>>
     */
    public function provideFieldReferenceData(): array
    {
        return [
            'date after referenced field should pass' => [
                ['start_date' => '2023-01-01'],
                'start_date',
                '2023-01-02',
                true,
                'value after referenced field',
            ],
            'date same as referenced field should pass' => [
                ['start_date' => '2023-01-01'],
                'start_date',
                '2023-01-01',
                true,
                'value same as referenced field',
            ],
            'date before referenced field should fail' => [
                ['start_date' => '2023-01-02'],
                'start_date',
                '2023-01-01',
                false,
                'value before referenced field',
            ],
            'empty referenced field should pass' => [
                ['start_date' => ''],
                'start_date',
                '2023-01-01',
                true,
                'empty referenced field',
            ],
            'null referenced field should pass' => [
                ['start_date' => null],
                'start_date',
                '2023-01-01',
                true,
                'null referenced field',
            ],
        ];
    }

    /**
     * @param array<mixed> $afterDates
     * @dataProvider provideMultipleAfterDatesData
     */
    public function testMultipleAfterDates(
        array $afterDates,
        string $testValue,
        bool $expected,
        string $testCase,
    ): void {
        // Given a DateAfterValidator with multiple after dates
        $validator = new DateAfterValidator(after: $afterDates);

        // When validating against multiple dates
        $result = $validator->validate($testValue);

        // Then the result matches expectations
        self::assertEquals($expected, $result, "Failed test case: {$testCase}");
    }

    /**
     * @return array<string<array<mixed>>>
     */
    public function provideMultipleAfterDatesData(): array
    {
        return [
            'date after all reference dates should pass' => [
                [new DateTimeImmutable('2023-01-01'), new DateTimeImmutable('2023-01-02')],
                '2023-01-03',
                true,
                'value after all reference dates',
            ],
            'date before first reference should fail' => [
                [new DateTimeImmutable('2023-01-02'), new DateTimeImmutable('2023-01-01')],
                '2023-01-01',
                false,
                'value before first reference date',
            ],
        ];
    }

    /**
     * @param array<string, mixed> $dtoData
     * @dataProvider provideErrorHandlingData
     */
    public function testErrorHandling(
        mixed $after,
        array $dtoData,
        mixed $testValue,
        bool $expected,
        ?string $expectedMessage,
        string $testCase
    ): void {
        // Given a DateAfterValidator with potential error conditions
        $validator = new DateAfterValidator(after: $after);
        if (!empty($dtoData)) {
            $validator->setDtoData($dtoData);
        }

        // When validating with invalid data
        $result = $validator->validate($testValue);

        // Then the result matches expectations
        self::assertEquals($expected, $result, "Failed test case: $testCase");

        if ($expectedMessage !== null) {
            // And the error message is properly set
            self::assertEquals($expectedMessage, $validator->getMessage());
        }
    }

    /**
     * @return array<string<array<mixed>>>
     */
    public function provideErrorHandlingData(): array
    {
        return [
            'invalid date string should fail' => [
                new DateTimeImmutable('2023-01-01'),
                [],
                'invalid-date-string',
                false,
                null,
                'invalid date string input',
            ],
            'malformed referenced field should fail' => [
                'start_date',
                ['start_date' => 'invalid-date-string'],
                '2023-01-02',
                false,
                'Invalid datetime value in referenced field: `start_date`',
                'malformed referenced field date',
            ],
            'non-string non-null value should fail' => [
                new DateTimeImmutable('2023-01-01'),
                [],
                123,
                false,
                null,
                'numeric value input',
            ],
        ];
    }

    public function testCustomErrorMessage(): void
    {
        // Given a DateAfterValidator with custom error message
        $customMessage = 'Custom error: date must be after %s';
        $afterDate = new DateTimeImmutable('2023-01-01');
        $validator = new DateAfterValidator(
            after: $afterDate,
            message: $customMessage
        );

        // When validation fails
        $result = $validator->validate('2022-12-31');

        // Then the custom message is used
        self::assertFalse($result);
        $expectedMessage = sprintf($customMessage, $afterDate->format('c'));
        self::assertEquals($expectedMessage, $validator->getMessage());
    }

    public function testMixedAfterTypesWithFieldReference(): void
    {
        // Given a DateAfterValidator with mixed after types (DateTime and field reference)
        $afterDate = new DateTimeImmutable('2023-01-01');
        $validator = new DateAfterValidator(after: [$afterDate, 'reference_date']);
        $validator->setDtoData(['reference_date' => '2023-01-15']);

        // When
        $result = $validator->validate('2023-01-16');

        // Then validation should pass
        self::assertTrue($result);
    }
}
