<?php

/**
 * @link https://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license https://www.yiiframework.com/license/
 */

declare(strict_types=1);

namespace yiiunit\framework\validators;

use Yii;
use yii\helpers\FileHelper;
use yii\helpers\StringHelper;
use yii\validators\FileValidator;
use yii\web\UploadedFile;
use yiiunit\data\validators\models\FakedValidationModel;
use yiiunit\data\validators\models\FakedValidationTypedModel;
use yiiunit\TestCase;

/**
 * @group validators
 */
class FileValidatorTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->mockApplication();
    }

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

        $this->destroyApplication();
    }

    public function testAssureMessagesSetOnInit(): void
    {
        $val = new FileValidator();
        foreach (['message', 'uploadRequired', 'tooMany', 'wrongExtension', 'tooBig', 'tooSmall', 'wrongMimeType'] as $attr) {
            $this->assertIsString($val->$attr);
        }
    }

    public function testTypeSplitOnInit(): void
    {
        $val = new FileValidator(['extensions' => 'jpeg, jpg, gif']);
        $this->assertEquals(['jpeg', 'jpg', 'gif'], $val->extensions);

        $val = new FileValidator(['extensions' => 'jpeg']);
        $this->assertEquals(['jpeg'], $val->extensions);

        $val = new FileValidator(['extensions' => '']);
        $this->assertEquals([], $val->extensions);

        $val = new FileValidator(['extensions' => []]);
        $this->assertEquals([], $val->extensions);

        $val = new FileValidator();
        $this->assertEquals([], $val->extensions);

        $val = new FileValidator(['extensions' => ['jpeg', 'exe']]);
        $this->assertEquals(['jpeg', 'exe'], $val->extensions);
    }

    public function testMimeTypeSplitOnInit(): void
    {
        $val = new FileValidator(['mimeTypes' => 'text/plain, image/png']);
        $this->assertEquals(['text/plain', 'image/png'], $val->mimeTypes);

        $val = new FileValidator(['mimeTypes' => 'text/plain']);
        $this->assertEquals(['text/plain'], $val->mimeTypes);

        $val = new FileValidator(['mimeTypes' => '']);
        $this->assertEquals([], $val->mimeTypes);

        $val = new FileValidator(['mimeTypes' => []]);
        $this->assertEquals([], $val->mimeTypes);

        $val = new FileValidator();
        $this->assertEquals([], $val->mimeTypes);

        $val = new FileValidator(['mimeTypes' => ['text/plain', 'image/png']]);
        $this->assertEquals(['text/plain', 'image/png'], $val->mimeTypes);
    }

    public function testGetSizeLimit(): void
    {
        $size = min(
            StringHelper::convertIniSizeToBytes(ini_get('upload_max_filesize')),
            StringHelper::convertIniSizeToBytes(ini_get('post_max_size'))
        );

        $val = new FileValidator();
        $this->assertEquals($size, $val->getSizeLimit());
        $val->maxSize = $size + 1; // set and test if value is overridden
        $this->assertEquals($size, $val->getSizeLimit());
        $val->maxSize = abs($size - 1);
        $this->assertEquals($size - 1, $val->getSizeLimit());
        $_POST['MAX_FILE_SIZE'] = $size + 1;
        $this->assertEquals($size - 1, $val->getSizeLimit());
        $_POST['MAX_FILE_SIZE'] = abs($size - 2);
        $this->assertSame($_POST['MAX_FILE_SIZE'], $val->getSizeLimit());
    }

    public function testValidateAttributeMultiple(): void
    {
        $val = new FileValidator([
            'maxFiles' => 2,
        ]);
        $m = FakedValidationModel::createWithAttributes(['attr_files' => 'path']);
        $val->validateAttribute($m, 'attr_files');
        $this->assertTrue($m->hasErrors('attr_files'));
        $m = FakedValidationModel::createWithAttributes(['attr_files' => []]);
        $val->validateAttribute($m, 'attr_files');
        $this->assertTrue($m->hasErrors('attr_files'));
        $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files')));

        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_files' => $this->createTestFiles(
                    [
                        [
                            'name' => 'test_up_1.txt',
                            'size' => 1024,
                        ],
                        [
                            'error' => UPLOAD_ERR_NO_FILE,
                        ],
                    ]
                ),
            ]
        );
        $val->validateAttribute($m, 'attr_files');
        $this->assertFalse($m->hasErrors('attr_files'));
        $m = FakedValidationModel::createWithAttributes([
            'attr_files' => $this->createTestFiles([
                [''], [''], [''],
            ]),
        ]);
        $val->validateAttribute($m, 'attr_files');
        $this->assertTrue($m->hasErrors());
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_files')), 'you can upload at most'));

        $files = [
            'file_1' => [
                'name' => 'test_up_1.txt',
                'size' => 1024,
            ],
            'file_2' => [
                'name' => 'test_up_2.txt',
                'size' => 1024,
            ]
        ];
        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_files' => $this->createTestFiles(
                    $files
                ),
            ]
        );
        $val->validateAttribute($m, 'attr_files');
        $this->assertFalse($m->hasErrors());
        $this->assertEquals(array_keys($m->attr_files), array_keys($files));

        $val->maxFiles = 0;
        $m->clearErrors();
        $val->validateAttribute($m, 'attr_files');
        $this->assertFalse($m->hasErrors());

        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_images' => $this->createTestFiles(
                    [
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                        [
                            'name' => 'text.txt',
                            'size' => 1024,
                        ],
                    ]
                ),
            ]
        );
        $m->setScenario('validateMultipleFiles');
        $this->assertFalse($m->validate());
        $this->assertNotFalse(stripos(
            current($m->getErrors('attr_images')),
            'Only files with these extensions are allowed'
        ));

        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_images' => $this->createTestFiles(
                    [
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                    ]
                ),
            ]
        );
        $m->setScenario('validateMultipleFiles');
        $this->assertTrue($m->validate());

        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_image' => $this->createTestFiles(
                    [
                        [
                            'name' => 'text.txt',
                            'size' => 1024,
                        ],
                    ]
                ),
            ]
        );
        $m->setScenario('validateFile');
        $this->assertFalse($m->validate());
    }

    public function testValidateAttributeMinFilesGreaterThanOneMaxFilesUnlimitedNotError(): void
    {
        $validator = new FileValidator(['minFiles' => 2, 'maxFiles' => 0]);
        $model = FakedValidationModel::createWithAttributes(
            [
                'attr_images' => $this->createTestFiles(
                    [
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                    ]
                )
            ]
        );

        $validator->validateAttribute($model, 'attr_images');

        $this->assertFalse($model->hasErrors('attr_images'));
    }

    public function testValidateAttributeMinFilesTwoMaxFilesFourNotError(): void
    {
        $validator = new FileValidator(['minFiles' => 2, 'maxFiles' => 4]);
        $model = FakedValidationModel::createWithAttributes(
            [
                'attr_images' => $this->createTestFiles(
                    [
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                    ]
                )
            ]
        );

        $validator->validateAttribute($model, 'attr_images');

        $this->assertFalse($model->hasErrors('attr_images'));
    }

    public function testValidateAttributeMinFilesTwoMaxFilesUnlimitedHasError(): void
    {
        $validator = new FileValidator(['minFiles' => 2, 'maxFiles' => 0]);
        $model = FakedValidationModel::createWithAttributes(
            [
                'attr_images' => $this->createTestFiles(
                    [
                        [
                            'name' => 'image.png',
                            'size' => 1024,
                            'type' => 'image/png',
                        ],
                        [
                            'error' => UPLOAD_ERR_NO_FILE,
                        ],
                    ]
                )
            ]
        );

        $validator->validateAttribute($model, 'attr_images');

        $this->assertTrue($model->hasErrors('attr_images'));
    }

    /**
     * https://github.com/yiisoft/yii2/issues/19855
     */
    public function testValidateArrayAttributeWithMinMaxOneAndOneFile(): void
    {
        $validator = new FileValidator(['maxFiles' => 1, 'minFiles' => 0]);
        $files = $this->createTestFiles(
            [
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ],
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ],
            ]
        )[0]; // <-- only one file
        $model = FakedValidationModel::createWithAttributes(['attr_images' => [$files]]);

        $validator->validateAttribute($model, 'attr_images');
        $this->assertFalse($model->hasErrors('attr_images'));
    }

    /**
     * @param  array          $params
     * @return UploadedFile[]
     */
    protected function createTestFiles($params = [])
    {
        $rndString = function ($len = 10) {
            $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            $randomString = '';
            for ($i = 0; $i < $len; $i++) {
                $randomString .= $characters[random_int(0, strlen($characters) - 1)];
            }

            return $randomString;
        };
        $files = [];
        foreach ($params as $key => $param) {
            if (empty($param) && count($params) != 1) {
                $files[$key] = ['no instance of UploadedFile'];
                continue;
            }
            $name = $param['name'] ?? $rndString();
            $tempName = Yii::getAlias('@yiiunit/runtime/validators/file/tmp/') . $name;
            if (is_readable($tempName)) {
                $size = filesize($tempName);
            } else {
                $size = $param['size'] ?? random_int(
                    1,
                    StringHelper::convertIniSizeToBytes(ini_get('upload_max_filesize'))
                );
            }
            $type = $param['type'] ?? 'text/plain';
            $error = $param['error'] ?? UPLOAD_ERR_OK;
            if (count($params) == 1) {
                $error = empty($param) ? UPLOAD_ERR_NO_FILE : $error;

                return new UploadedFile([
                    'name' => $name,
                    'tempName' => $tempName,
                    'type' => $type,
                    'size' => $size,
                    'error' => $error,
                ]);
            }
            $files[$key] = new UploadedFile([
                'name' => $name,
                'tempName' => $tempName,
                'type' => $type,
                'size' => $size,
                'error' => $error,
            ]);
        }

        return $files;
    }

    /**
     * @param $fileName
     * @return UploadedFile
     */
    protected function getRealTestFile($fileName)
    {
        $filePath = Yii::getAlias('@yiiunit/framework/validators/data/mimeType/') . $fileName;

        return new UploadedFile([
            'name' => $fileName,
            'tempName' => $filePath,
            'type' => FileHelper::getMimeType($filePath),
            'size' => filesize($filePath),
            'error' => UPLOAD_ERR_OK,
        ]);
    }

    public function testValidateAttribute(): void
    {
        // single File
        $val = new FileValidator();
        $m = $this->createModelForAttributeTest();
        $val->validateAttribute($m, 'attr_files');
        $this->assertFalse($m->hasErrors());
        $val->validateAttribute($m, 'attr_files_empty');
        $this->assertTrue($m->hasErrors('attr_files_empty'));
        $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty')));

        // single File with skipOnEmpty = false
        $val = new FileValidator(['skipOnEmpty' => false]);
        $m = $this->createModelForAttributeTest();
        $val->validateAttribute($m, 'attr_files');
        $this->assertFalse($m->hasErrors());
        $val->validateAttribute($m, 'attr_files_empty');
        $this->assertTrue($m->hasErrors('attr_files_empty'));
        $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty')));
        $m = $this->createModelForAttributeTest();

        // too big
        $val = new FileValidator(['maxSize' => 128]);
        $val->validateAttribute($m, 'attr_files');
        $this->assertTrue($m->hasErrors('attr_files'));
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_files')), 'too big'));
        // to Small
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator(['minSize' => 2048]);
        $val->validateAttribute($m, 'attr_files');
        $this->assertTrue($m->hasErrors('attr_files'));
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_files')), 'too small'));
        // UPLOAD_ERR_INI_SIZE/UPLOAD_ERR_FORM_SIZE
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator();
        $val->validateAttribute($m, 'attr_err_ini');
        $this->assertTrue($m->hasErrors('attr_err_ini'));
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_err_ini')), 'too big'));
        // UPLOAD_ERR_PARTIAL
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator();
        $val->validateAttribute($m, 'attr_err_part');
        $this->assertTrue($m->hasErrors('attr_err_part'));
        $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part')));
    }

    public function testValidateAttributeType(): void
    {
        $val = new FileValidator([
            'extensions' => 'jpeg, jpg',
            'checkExtensionByMimeType' => false,
        ]);
        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_jpg' => $this->createTestFiles([['name' => 'one.jpeg']]),
                'attr_exe' => $this->createTestFiles([['name' => 'bad.exe']]),
            ]
        );
        $val->validateAttribute($m, 'attr_jpg');
        $this->assertFalse($m->hasErrors('attr_jpg'));
        $val->validateAttribute($m, 'attr_exe');
        $this->assertTrue($m->hasErrors('attr_exe'));
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_exe')), 'Only files with these extensions '));
    }

    public function testValidateEmptyExtension(): void
    {
        $val = new FileValidator([
            'extensions' => ['txt', ''],
            'checkExtensionByMimeType' => false,
        ]);
        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_txt' => $this->createTestFiles([['name' => 'one.txt']]),
                'attr_empty' => $this->createTestFiles([['name' => 'bad.']]),
                'attr_empty2' => $this->createTestFiles([['name' => 'bad']]),
            ]
        );
        $val->validateAttribute($m, 'attr_txt');
        $this->assertFalse($m->hasErrors('attr_txt'));
        $val->validateAttribute($m, 'attr_empty');
        $this->assertFalse($m->hasErrors('attr_empty'));
        $val->validateAttribute($m, 'attr_empty2');
        $this->assertFalse($m->hasErrors('attr_empty2'));
    }

    public function testValidateAttributeDoubleType(): void
    {
        $val = new FileValidator([
            'extensions' => 'tar.gz, tar.xz',
            'checkExtensionByMimeType' => false,
        ]);

        $m = FakedValidationModel::createWithAttributes(
            [
                'attr_tar' => $this->createTestFiles([['name' => 'one.tar.gz']]),
                'attr_bar' => $this->createTestFiles([['name' => 'bad.bar.xz']]),
                'attr_badtar' => $this->createTestFiles([['name' => 'badtar.xz']]),
            ]
        );
        $val->validateAttribute($m, 'attr_tar');
        $this->assertFalse($m->hasErrors('attr_tar'));

        $val->validateAttribute($m, 'attr_bar');
        $this->assertTrue($m->hasErrors('attr_bar'));
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_bar')), 'Only files with these extensions '));

        $val->validateAttribute($m, 'attr_badtar');
        $this->assertTrue($m->hasErrors('attr_badtar'));
        $this->assertNotFalse(stripos((string) current($m->getErrors('attr_badtar')), 'Only files with these extensions '));
    }

    public function testIssue11012(): void
    {
        $baseName = '飛兒樂團光茫';
        /** @var UploadedFile $file */
        $file = $this->createTestFiles([
            ['name' => $baseName . '.txt'],
        ]);
        $this->assertEquals($baseName, $file->getBaseName());
    }

    /**
     * @param string $fileName
     * @param string $mask
     * @dataProvider \yiiunit\framework\validators\providers\FileValidatorProvider::validMimeTypes
     */
    public function testValidateMimeTypeMaskValid($fileName, $mask): void
    {
        $validator = new FileValidator(['mimeTypes' => $mask]);
        $file = $this->getRealTestFile($fileName);
        $this->assertTrue($validator->validate($file));
    }

    /**
     * @dataProvider \yiiunit\framework\validators\providers\FileValidatorProvider::invalidMimeTypes
     */
    public function testValidateMimeTypeMaskInvalid(string $fileName, string $mask): void
    {
        $validator = new FileValidator(['mimeTypes' => $mask]);
        $file = $this->getRealTestFile($fileName);
        $this->assertFalse($validator->validate($file));
    }

    public static function validMimeTypes(): array
    {
        $validMimeTypes = array_filter([
            ['test.svg', 'image/*', 'svg'],
            ['test.jpg', 'image/*', 'jpg'],
            ['test.png', 'image/*', 'png'],
            ['test.png', 'IMAGE/*', 'png'],
            ['test.txt', 'text/*', 'txt'],
            ['test.xml', '*/xml', 'xml'],
            ['test.odt', 'application/vnd*', 'odt'],
            ['test.tar.xz', 'application/x-xz', 'tar.xz'],
        ]);

        # fix for bundled libmagic bug, see also https://github.com/yiisoft/yii2/issues/19925
        if ((PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80122) || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80209)) {
            $v81_zx = ['test.tar.xz', 'application/octet-stream', 'tar.xz'];
            array_pop($validMimeTypes);
            $validMimeTypes[] = $v81_zx;
        }

        return $validMimeTypes;
    }

    public static function invalidMimeTypes(): array
    {
        return [
            ['test.txt', 'image/*', 'png, jpg'],
            ['test.odt', 'text/*', 'txt'],
            ['test.xml', '*/svg+xml', 'svg'],
            ['test.png', 'image/x-iso9660-image', 'bmp'],
            ['test.svg', 'application/*', 'jpg'],
        ];
    }

    /**
     * @param string $fileName
     * @param string|array $allowedExtensions
     * @dataProvider \yiiunit\framework\validators\providers\FileValidatorProvider::validMimeTypes
     */
    public function testValidateFileByExtensionUsingMimeType($fileName, mixed $_, $allowedExtensions): void
    {
        $validator = new FileValidator(['extensions' => (array) $allowedExtensions]);
        $file = $this->getRealTestFile($fileName);
        $detectedMimeType = FileHelper::getMimeType($file->tempName, null, false);
        $this->assertTrue($validator->validate($file), "Mime type detected was \"$detectedMimeType\". Consider adding it to MimeTypeController::\$aliases.");
    }

    /**
     * @param mixed $_
     * @param string|array $allowedExtensions
     * @dataProvider \yiiunit\framework\validators\providers\FileValidatorProvider::invalidMimeTypes
     */
    public function testValidateFileByExtensionUsingMimeTypeInvalid(string $fileName, string $_, string $allowedExtensions): void
    {
        $validator = new FileValidator(['extensions' => (array) $allowedExtensions]);
        $file = $this->getRealTestFile($fileName);
        $this->assertFalse($validator->validate($file));
    }

    protected function createModelForAttributeTest()
    {
        return FakedValidationModel::createWithAttributes(
            [
                'attr_files' => $this->createTestFiles([
                    ['name' => 'abc.jpg', 'size' => 1024, 'type' => 'image/jpeg'],
                ]),
                'attr_files_empty' => $this->createTestFiles([[]]),
                'attr_err_ini' => $this->createTestFiles([['error' => UPLOAD_ERR_INI_SIZE]]),
                'attr_err_part' => $this->createTestFiles([['error' => UPLOAD_ERR_PARTIAL]]),
                'attr_err_tmp' => $this->createTestFiles([['error' => UPLOAD_ERR_NO_TMP_DIR]]),
                'attr_err_write' => $this->createTestFiles([['error' => UPLOAD_ERR_CANT_WRITE]]),
                'attr_err_ext' => $this->createTestFiles([['error' => UPLOAD_ERR_EXTENSION]]),
            ]
        );
    }

    public function testValidateAttributeErrPartial(): void
    {
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator();
        $val->validateAttribute($m, 'attr_err_part');
        $this->assertTrue($m->hasErrors('attr_err_part'));
        $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part')));
    }

    public function testValidateAttributeErrCantWrite(): void
    {
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator();
        $val->validateAttribute($m, 'attr_err_write');
        $this->assertTrue($m->hasErrors('attr_err_write'));
        $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_write')));
    }

    public function testValidateAttributeErrExtension(): void
    {
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator();
        $val->validateAttribute($m, 'attr_err_ext');
        $this->assertTrue($m->hasErrors('attr_err_ext'));
        $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_ext')));
    }

    public function testValidateAttributeErrNoTmpDir(): void
    {
        $m = $this->createModelForAttributeTest();
        $val = new FileValidator();
        $val->validateAttribute($m, 'attr_err_tmp');
        $this->assertTrue($m->hasErrors('attr_err_tmp'));
        $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_tmp')));
    }

    /**
     * @dataProvider \yiiunit\framework\validators\providers\FileValidatorProvider::mimeTypeCaseInsensitive
     */
    public function testValidateMimeTypeCaseInsensitive(string $mask, string $fileMimeType, bool $expected): void
    {
        $validator = $this->getMockBuilder(FileValidator::class)
            ->onlyMethods(['getMimeTypeByFile'])
            ->getMock();
        $validator->method('getMimeTypeByFile')->willReturn($fileMimeType);
        $validator->mimeTypes = [$mask];

        $file = $this->getRealTestFile('test.txt');
        $this->assertEquals($expected, $validator->validate($file), sprintf('Mime type validate fail: "%s" / "%s"', $mask, $fileMimeType));
    }

    public static function mimeTypeCaseInsensitive(): array
    {
        return [
            ['Image/*', 'image/jp2', true],
            ['image/*', 'Image/jp2', true],
            ['application/vnd.ms-word.document.macroEnabled.12', 'application/vnd.ms-word.document.macroenabled.12', true],
            ['image/jxra', 'image/jxrA', true],
        ];
    }

    public function testValidateTypedAttributeNoErrors(): void
    {
        if (version_compare(PHP_VERSION, '7.4', '<')) {
            $this->markTestSkipped('Requires typed properties');
        }

        $validator = new FileValidator(['minFiles' => 0, 'maxFiles' => 2]);
        $file = $this->createTestFiles(
            [
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ]
            ]
        );
        $model = new FakedValidationTypedModel();
        $model->single = $file;
        $model->multiple = [$file];
        $validator->validateAttribute($model, 'single');
        $this->assertFalse($model->hasErrors('single'));
        $validator->validateAttribute($model, 'multiple');
        $this->assertFalse($model->hasErrors('multiple'));
    }

    public function testValidateTypedAttributeExactMinNoErrors(): void
    {
        if (version_compare(PHP_VERSION, '7.4', '<')) {
            $this->markTestSkipped('Requires typed properties');
        }

        $validator = new FileValidator(['minFiles' => 1]);
        $file = $this->createTestFiles(
            [
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ]
            ]
        );
        $model = new FakedValidationTypedModel();
        $model->single = $file;
        $model->multiple = [$file];
        $validator->validateAttribute($model, 'single');
        $this->assertFalse($model->hasErrors('single'));
        $validator->validateAttribute($model, 'multiple');
        $this->assertFalse($model->hasErrors('multiple'));
    }

    public function testValidateTypedAttributeExactMaxNoErrors(): void
    {
        if (version_compare(PHP_VERSION, '7.4', '<')) {
            $this->markTestSkipped('Requires typed properties');
        }

        $validator = new FileValidator(['maxFiles' => 1]);
        $file = $this->createTestFiles(
            [
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ]
            ]
        );
        $model = new FakedValidationTypedModel();
        $model->single = $file;
        $model->multiple = [$file];
        $validator->validateAttribute($model, 'single');
        $this->assertFalse($model->hasErrors('single'));
        $validator->validateAttribute($model, 'multiple');
        $this->assertFalse($model->hasErrors('multiple'));
    }

    public function testValidateTypedAttributeMinError(): void
    {
        if (version_compare(PHP_VERSION, '7.4', '<')) {
            $this->markTestSkipped('Requires typed properties');
        }

        $validator = new FileValidator(['minFiles' => 2]);
        $file = $this->createTestFiles(
            [
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ]
            ]
        );
        $model = new FakedValidationTypedModel();
        $model->single = $file;
        $model->multiple = [$file];
        $validator->validateAttribute($model, 'single');
        $this->assertTrue($model->hasErrors('single'));
        $validator->validateAttribute($model, 'multiple');
        $this->assertTrue($model->hasErrors('multiple'));
    }

    public function testValidateTypedAttributeMaxError(): void
    {
        if (version_compare(PHP_VERSION, '7.4', '<')) {
            $this->markTestSkipped('Requires typed properties');
        }

        $validator = new FileValidator(['maxFiles' => 1]);
        $files = $this->createTestFiles(
            [
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ],
                [
                    'name' => 'image.png',
                    'size' => 1024,
                    'type' => 'image/png',
                ]
            ]
        );
        $model = new FakedValidationTypedModel();
        // single attribute cannot be checked because maxFiles = 0 === no limits
        $model->multiple = $files;
        $validator->validateAttribute($model, 'multiple');
        $this->assertTrue($model->hasErrors('multiple'));
    }
}
