<?php

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

namespace yiiunit\framework\widgets;

use yiiunit\TestCase;
use Exception;
use yii\validators\Validator;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Yii;
use yii\base\DynamicModel;
use yii\web\AssetManager;
use yii\web\View;
use yii\widgets\ActiveField;
use yii\widgets\ActiveForm;
use yii\widgets\InputWidget;
use yii\widgets\MaskedInput;

/**
 * @author Nelson J Morais <njmorais@gmail.com>
 *
 * @group widgets
 */
class ActiveFieldTest extends TestCase
{
    use ArraySubsetAsserts;

    /**
     * @var ActiveFieldExtend
     */
    private $activeField;
    /**
     * @var DynamicModel
     */
    private $helperModel;
    /**
     * @var ActiveForm
     */
    private $helperForm;
    private $attributeName = 'attributeName';

    protected function setUp(): void
    {
        parent::setUp();
        // dirty way to have Request object not throwing exception when running testHomeLinkNull()
        $_SERVER['SCRIPT_FILENAME'] = 'index.php';
        $_SERVER['SCRIPT_NAME'] = 'index.php';

        $this->mockWebApplication();

        Yii::setAlias('@testWeb', '/');
        Yii::setAlias('@testWebRoot', '@yiiunit/data/web');

        $this->helperModel = new ActiveFieldTestModel(['attributeName']);
        ob_start();
        $this->helperForm = ActiveForm::begin(['action' => '/something', 'enableClientScript' => false]);
        ActiveForm::end();
        ob_end_clean();

        $this->activeField = new ActiveFieldExtend(true);
        $this->activeField->form = $this->helperForm;
        $this->activeField->form->setView($this->getView());
        $this->activeField->model = $this->helperModel;
        $this->activeField->attribute = $this->attributeName;
    }

    public function testRenderNoContent(): void
    {
        $expectedValue = <<<EOD
<div class="form-group field-activefieldtestmodel-attributename">
<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[{$this->attributeName}]">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    /**
     * @todo discuss|review Expected HTML shouldn't be wrapped only by the $content?
     */
    public function testRenderWithCallableContent(): void
    {
        // field will be the html of the model's attribute wrapped with the return string below.
        $field = $this->attributeName;
        $content = function ($field) {
            return "<div class=\"custom-container\"> $field </div>";
        };

        $expectedValue = <<<EOD
<div class="form-group field-activefieldtestmodel-attributename">
<div class="custom-container"> <div class="form-group field-activefieldtestmodel-attributename">
<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[{$this->attributeName}]">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div> </div>
</div>
EOD;

        $actualValue = $this->activeField->render($content);
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    /**
     * @link https://github.com/yiisoft/yii2/issues/7627
     */
    public function testRenderWithCustomInputId(): void
    {
        $expectedValue = <<<EOD
<div class="form-group field-custom-input-id">
<label class="control-label" for="custom-input-id">Attribute Name</label>
<input type="text" id="custom-input-id" class="form-control" name="ActiveFieldTestModel[{$this->attributeName}]">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;

        $this->activeField->inputOptions['id'] = 'custom-input-id';

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testBeginHasErrors(): void
    {
        $this->helperModel->addError($this->attributeName, 'Error Message');

        $expectedValue = '<div class="form-group field-activefieldtestmodel-attributename has-error">';
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);
    }

    public function testBeginAttributeIsRequired(): void
    {
        $this->helperModel->addRule($this->attributeName, 'required');

        $expectedValue = '<div class="form-group field-activefieldtestmodel-attributename required">';
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);
    }

    public function testBeginHasErrorAndRequired(): void
    {
        $this->helperModel->addError($this->attributeName, 'Error Message');
        $this->helperModel->addRule($this->attributeName, 'required');

        $expectedValue = '<div class="form-group field-activefieldtestmodel-attributename required has-error">';
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);
    }

    public function testBegin(): void
    {
        $expectedValue = '<article class="form-group field-activefieldtestmodel-attributename">';
        $this->activeField->options['tag'] = 'article';
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);

        $expectedValue = '';
        $this->activeField->options['tag'] = null;
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);

        $expectedValue = '';
        $this->activeField->options['tag'] = false;
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);
    }

    public function testEnd(): void
    {
        $expectedValue = '</div>';
        $actualValue = $this->activeField->end();

        $this->assertEquals($expectedValue, $actualValue);

        // other tag
        $expectedValue = '</article>';
        $this->activeField->options['tag'] = 'article';
        $actualValue = $this->activeField->end();

        $this->assertEquals($expectedValue, $actualValue);

        $expectedValue = '';
        $this->activeField->options['tag'] = false;
        $actualValue = $this->activeField->end();

        $this->assertEquals($expectedValue, $actualValue);

        $expectedValue = '';
        $this->activeField->options['tag'] = null;
        $actualValue = $this->activeField->end();

        $this->assertEquals($expectedValue, $actualValue);
    }

    public function testLabel(): void
    {
        $expectedValue = '<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>';
        $this->activeField->label();

        $this->assertEquals($expectedValue, $this->activeField->parts['{label}']);

        // label = false
        $expectedValue = '';
        $this->activeField->label(false);

        $this->assertEquals($expectedValue, $this->activeField->parts['{label}']);

        // $label = 'Label Name'
        $label = 'Label Name';
        $expectedValue = <<<EOT
<label class="control-label" for="activefieldtestmodel-attributename">{$label}</label>
EOT;
        $this->activeField->label($label);

        $this->assertEquals($expectedValue, $this->activeField->parts['{label}']);
    }

    public function testError(): void
    {
        $expectedValue = '<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>';
        $this->activeField->label();

        $this->assertEquals($expectedValue, $this->activeField->parts['{label}']);

        // label = false
        $expectedValue = '';
        $this->activeField->label(false);

        $this->assertEquals($expectedValue, $this->activeField->parts['{label}']);

        // $label = 'Label Name'
        $label = 'Label Name';
        $expectedValue = <<<EOT
<label class="control-label" for="activefieldtestmodel-attributename">{$label}</label>
EOT;
        $this->activeField->label($label);

        $this->assertEquals($expectedValue, $this->activeField->parts['{label}']);
    }

    public function testTabularInputErrors(): void
    {
        $this->activeField->attribute = '[0]' . $this->attributeName;
        $this->helperModel->addError($this->attributeName, 'Error Message');

        $expectedValue = '<div class="form-group field-activefieldtestmodel-0-attributename has-error">';
        $actualValue = $this->activeField->begin();

        $this->assertEquals($expectedValue, $actualValue);
    }

    public static function hintDataProvider(): array
    {
        return [
            ['Hint Content', '<div class="hint-block">Hint Content</div>'],
            [false, ''],
            [null, '<div class="hint-block">Hint for attributeName attribute</div>'],
        ];
    }

    /**
     * @dataProvider hintDataProvider
     * @param mixed $hint
     * @param string $expectedHtml
     */
    public function testHint($hint, $expectedHtml): void
    {
        $this->activeField->hint($hint);

        $this->assertEquals($expectedHtml, $this->activeField->parts['{hint}']);
    }

    public function testInput(): void
    {
        $expectedValue = <<<'EOD'
<input type="password" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">
EOD;
        $this->activeField->input('password');

        $this->assertEquals($expectedValue, $this->activeField->parts['{input}']);

        // with options
        $expectedValue = <<<'EOD'
<input type="password" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]" weird="value">
EOD;
        $this->activeField->input('password', ['weird' => 'value']);

        $this->assertEquals($expectedValue, $this->activeField->parts['{input}']);
    }

    public function testTextInput(): void
    {
        $expectedValue = <<<'EOD'
<input type="text" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">
EOD;
        $this->activeField->textInput();
        $this->assertEquals($expectedValue, $this->activeField->parts['{input}']);
    }

    public function testHiddenInput(): void
    {
        $expectedValue = <<<'EOD'
<input type="hidden" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">
EOD;
        $this->activeField->hiddenInput();
        $this->assertEquals($expectedValue, $this->activeField->parts['{input}']);
    }

    public function testListBox(): void
    {
        $expectedValue = <<<'EOD'
<input type="hidden" name="ActiveFieldTestModel[attributeName]" value=""><select id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]" size="4">
<option value="1">Item One</option>
<option value="2">Item 2</option>
</select>
EOD;
        $this->activeField->listBox(['1' => 'Item One', '2' => 'Item 2']);
        $this->assertEqualsWithoutLE($expectedValue, $this->activeField->parts['{input}']);

        // https://github.com/yiisoft/yii2/issues/8848
        $expectedValue = <<<'EOD'
<input type="hidden" name="ActiveFieldTestModel[attributeName]" value=""><select id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]" size="4">
<option value="value1" disabled>Item One</option>
<option value="value2" label="value 2">Item 2</option>
</select>
EOD;
        $this->activeField->listBox(['value1' => 'Item One', 'value2' => 'Item 2'], ['options' => [
            'value1' => ['disabled' => true],
            'value2' => ['label' => 'value 2'],
        ]]);
        $this->assertEqualsWithoutLE($expectedValue, $this->activeField->parts['{input}']);

        $expectedValue = <<<'EOD'
<input type="hidden" name="ActiveFieldTestModel[attributeName]" value=""><select id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]" size="4">
<option value="value1" disabled>Item One</option>
<option value="value2" selected label="value 2">Item 2</option>
</select>
EOD;
        $this->activeField->model->{$this->attributeName} = 'value2';
        $this->activeField->listBox(['value1' => 'Item One', 'value2' => 'Item 2'], ['options' => [
            'value1' => ['disabled' => true],
            'value2' => ['label' => 'value 2'],
        ]]);
        $this->assertEqualsWithoutLE($expectedValue, $this->activeField->parts['{input}']);
    }

    public function testRadioList(): void
    {
        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-attributename">
<label class="control-label">Attribute Name</label>
<input type="hidden" name="ActiveFieldTestModel[attributeName]" value=""><div id="activefieldtestmodel-attributename" role="radiogroup"><label><input type="radio" name="ActiveFieldTestModel[attributeName]" value="1"> Item One</label></div>
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;
        $this->activeField->radioList(['1' => 'Item One']);
        $this->assertEqualsWithoutLE($expectedValue, $this->activeField->render());
    }

    public function testGetClientOptionsReturnEmpty(): void
    {
        // setup: we want the real deal here!
        $this->activeField->setClientOptionsEmpty(false);

        // expected empty
        $actualValue = $this->activeField->getClientOptions();
        $this->assertEmpty($actualValue);
    }

    public function testGetClientOptionsWithActiveAttributeInScenario(): void
    {
        $this->activeField->setClientOptionsEmpty(false);

        $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
        $this->activeField->form->enableClientValidation = false;

        // expected empty
        $actualValue = $this->activeField->getClientOptions();
        $this->assertEmpty($actualValue);
    }

    public function testGetClientOptionsClientValidation(): void
    {
        $this->activeField->setClientOptionsEmpty(false);

        $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
        $this->activeField->enableClientValidation = true;
        $actualValue = $this->activeField->getClientOptions();
        $expectedJsExpression = 'function (attribute, value, messages, deferred, $form) {return true;}';
        $this->assertEquals($expectedJsExpression, $actualValue['validate']);

        $this->assertNotTrue(isset($actualValue['validateOnChange']));
        $this->assertNotTrue(isset($actualValue['validateOnBlur']));
        $this->assertNotTrue(isset($actualValue['validateOnType']));
        $this->assertNotTrue(isset($actualValue['validationDelay']));
        $this->assertNotTrue(isset($actualValue['enableAjaxValidation']));

        $this->activeField->validateOnChange = $expectedValidateOnChange = false;
        $this->activeField->validateOnBlur = $expectedValidateOnBlur = false;
        $this->activeField->validateOnType = $expectedValidateOnType = true;
        $this->activeField->validationDelay = $expectedValidationDelay = 100;
        $this->activeField->enableAjaxValidation = $expectedEnableAjaxValidation = true;

        $actualValue = $this->activeField->getClientOptions();

        $this->assertSame($expectedValidateOnChange, $actualValue['validateOnChange']);
        $this->assertSame($expectedValidateOnBlur, $actualValue['validateOnBlur']);
        $this->assertSame($expectedValidateOnType, $actualValue['validateOnType']);
        $this->assertSame($expectedValidationDelay, $actualValue['validationDelay']);
        $this->assertSame($expectedEnableAjaxValidation, $actualValue['enableAjaxValidation']);
    }

    public function testGetClientOptionsValidatorWhenClientSet(): void
    {
        $this->activeField->setClientOptionsEmpty(false);
        $this->activeField->enableAjaxValidation = true;
        $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');

        foreach ($this->activeField->model->validators as $validator) {
            $validator->whenClient = "function (attribute, value) { return 'yii2' == 'yii2'; }"; // js
        }

        $actualValue = $this->activeField->getClientOptions();
        $expectedJsExpression = 'function (attribute, value, messages, deferred, $form) {if ((function (attribute, value) '
            . "{ return 'yii2' == 'yii2'; })(attribute, value)) { return true; }}";

        $this->assertEquals($expectedJsExpression, $actualValue['validate']->expression);
    }

    /**
     * @see https://github.com/yiisoft/yii2/issues/8779
     */
    public function testEnctype(): void
    {
        $this->activeField->fileInput();
        $this->assertEquals('multipart/form-data', $this->activeField->form->options['enctype']);
    }

    /**
     * @link https://github.com/yiisoft/yii2/issues/7627
     */
    public function testGetClientOptionsWithCustomInputId(): void
    {
        $this->activeField->setClientOptionsEmpty(false);

        $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
        $this->activeField->inputOptions['id'] = 'custom-input-id';
        $this->activeField->textInput();
        $actualValue = $this->activeField->getClientOptions();

        $this->assertArraySubset([
            'id' => 'custom-input-id',
            'name' => $this->attributeName,
            'container' => '.field-custom-input-id',
            'input' => '#custom-input-id',
        ], $actualValue);

        $this->activeField->textInput(['id' => 'custom-textinput-id']);
        $actualValue = $this->activeField->getClientOptions();

        $this->assertArraySubset([
            'id' => 'custom-textinput-id',
            'name' => $this->attributeName,
            'container' => '.field-custom-textinput-id',
            'input' => '#custom-textinput-id',
        ], $actualValue);
    }

    public function testAriaAttributes(): void
    {
        $this->activeField->addAriaAttributes = true;

        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-attributename">
<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testAriaRequiredAttribute(): void
    {
        $this->activeField->addAriaAttributes = true;
        $this->helperModel->addRule([$this->attributeName], 'required');

        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-attributename required">
<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]" aria-required="true">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testAriaInvalidAttribute(): void
    {
        $this->activeField->addAriaAttributes = true;
        $this->helperModel->addError($this->attributeName, 'Some error');

        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-attributename has-error">
<label class="control-label" for="activefieldtestmodel-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]" aria-invalid="true">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block">Some error</div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testTabularAriaAttributes(): void
    {
        $this->activeField->attribute = '[0]' . $this->attributeName;
        $this->activeField->addAriaAttributes = true;

        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-0-attributename">
<label class="control-label" for="activefieldtestmodel-0-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-0-attributename" class="form-control" name="ActiveFieldTestModel[0][attributeName]">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testTabularAriaRequiredAttribute(): void
    {
        $this->activeField->attribute = '[0]' . $this->attributeName;
        $this->activeField->addAriaAttributes = true;
        $this->helperModel->addRule([$this->attributeName], 'required');

        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-0-attributename required">
<label class="control-label" for="activefieldtestmodel-0-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-0-attributename" class="form-control" name="ActiveFieldTestModel[0][attributeName]" aria-required="true">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testTabularAriaInvalidAttribute(): void
    {
        $this->activeField->attribute = '[0]' . $this->attributeName;
        $this->activeField->addAriaAttributes = true;
        $this->helperModel->addError($this->attributeName, 'Some error');

        $expectedValue = <<<'EOD'
<div class="form-group field-activefieldtestmodel-0-attributename has-error">
<label class="control-label" for="activefieldtestmodel-0-attributename">Attribute Name</label>
<input type="text" id="activefieldtestmodel-0-attributename" class="form-control" name="ActiveFieldTestModel[0][attributeName]" aria-invalid="true">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block">Some error</div>
</div>
EOD;

        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

    public function testEmptyTag(): void
    {
        $this->activeField->options = ['tag' => false];
        $expectedValue = '<input type="hidden" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">';
        $actualValue = $this->activeField->hiddenInput()->label(false)->error(false)->hint(false)->render();
        $this->assertEqualsWithoutLE($expectedValue, trim($actualValue));
    }

    public function testWidget(): void
    {
        $this->activeField->widget(TestInputWidget::class);
        $this->assertEquals('Render: ' . TestInputWidget::class, $this->activeField->parts['{input}']);
        $widget = TestInputWidget::$lastInstance;

        $this->assertSame($this->activeField->model, $widget->model);
        $this->assertEquals($this->activeField->attribute, $widget->attribute);
        $this->assertSame($this->activeField->form->view, $widget->view);
        $this->assertSame($this->activeField, $widget->field);

        $this->activeField->widget(TestInputWidget::class, ['options' => ['id' => 'test-id']]);
        $this->assertEquals('test-id', $this->activeField->labelOptions['for']);
    }

    public function testWidgetOptions(): void
    {
        $this->activeField->form->validationStateOn = ActiveForm::VALIDATION_STATE_ON_INPUT;
        $this->activeField->model->addError('attributeName', 'error');

        $this->activeField->widget(TestInputWidget::class);
        $widget = TestInputWidget::$lastInstance;
        $expectedOptions = [
            'class' => 'form-control has-error',
            'aria-invalid' => 'true',
            'id' => 'activefieldtestmodel-attributename',
        ];
        $this->assertEquals($expectedOptions, $widget->options);

        $this->activeField->inputOptions = [];
        $this->activeField->widget(TestInputWidget::class);
        $widget = TestInputWidget::$lastInstance;
        $expectedOptions = [
            'class' => 'has-error',
            'aria-invalid' => 'true',
            'id' => 'activefieldtestmodel-attributename',
        ];
        $this->assertEquals($expectedOptions, $widget->options);
    }

    /**
     * @depends testHiddenInput
     *
     * @see https://github.com/yiisoft/yii2/issues/14773
     */
    public function testOptionsClass(): void
    {
        $this->activeField->options = ['class' => 'test-wrapper'];
        $expectedValue = <<<'HTML'
<div class="test-wrapper field-activefieldtestmodel-attributename">

<input type="hidden" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">


</div>
HTML;
        $actualValue = $this->activeField->hiddenInput()->label(false)->error(false)->hint(false)->render();
        $this->assertEqualsWithoutLE($expectedValue, trim($actualValue));

        $this->activeField->options = ['class' => ['test-wrapper', 'test-add']];
        $expectedValue = <<<'HTML'
<div class="test-wrapper test-add field-activefieldtestmodel-attributename">

<input type="hidden" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">


</div>
HTML;
        $actualValue = $this->activeField->hiddenInput()->label(false)->error(false)->hint(false)->render();
        $this->assertEqualsWithoutLE($expectedValue, trim($actualValue));
    }

    public function testInputOptionsTransferToWidget(): void
    {
        $widget = $this->activeField->widget(TestMaskedInput::class, [
            'mask' => '999-999-9999',
            'options' => ['placeholder' => 'pholder_direct'],
        ]);
        $this->assertStringContainsString('placeholder="pholder_direct"', (string) $widget);

        // use regex clientOptions instead mask
        $widget = $this->activeField->widget(TestMaskedInput::class, [
            'options' => ['placeholder' => 'pholder_direct'],
            'clientOptions' => ['regex' => '^.*$'],
        ]);
        $this->assertStringContainsString('placeholder="pholder_direct"', (string) $widget);

        // transfer options from ActiveField to widget
        $this->activeField->inputOptions = ['placeholder' => 'pholder_input'];
        $widget = $this->activeField->widget(TestMaskedInput::class, [
            'mask' => '999-999-9999',
        ]);
        $this->assertStringContainsString('placeholder="pholder_input"', (string) $widget);

        // set both AF and widget options (second one takes precedence)
        $this->activeField->inputOptions = ['placeholder' => 'pholder_both_input'];
        $widget = $this->activeField->widget(TestMaskedInput::class, [
            'mask' => '999-999-9999',
            'options' => ['placeholder' => 'pholder_both_direct']
        ]);
        $this->assertStringContainsString('placeholder="pholder_both_direct"', (string) $widget);
    }

    public function testExceptionToString(): void
    {
        if (PHP_VERSION_ID < 70400) {
            $this->markTestSkipped('This test is for PHP 7.4+ only');
        }

        $field = new TestActiveFieldWithException();

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Test exception in toString.');

        (string) $field;
    }

    public function testExceptionToStringLegacy(): void
    {
        if (PHP_VERSION_ID >= 70400) {
            $this->markTestSkipped('This test is for PHP < 7.4 only');
        }

        $field = new TestActiveFieldWithException();

        $errorTriggered = false;
        $errorMessage = '';

        set_error_handler(
            function ($severity, $message, $file, $line) use (&$errorTriggered, &$errorMessage) {
                if ($severity === E_USER_ERROR) {
                    $errorTriggered = true;
                    $errorMessage = $message;

                    return true;
                }

                return false;
            },
            E_USER_ERROR,
        );

        $result = (string) $field;

        restore_error_handler();

        $this->assertTrue($errorTriggered, 'E_USER_ERROR should have been triggered');
        $this->assertStringContainsString('Test exception in toString.', $errorMessage);
        $this->assertSame('', $result, 'Result should be an empty string');
    }

    /**
     * Helper methods.
     */
    protected function getView()
    {
        $view = new View();
        $view->setAssetManager(new AssetManager([
            'basePath' => '@testWebRoot/assets',
            'baseUrl' => '@testWeb/assets',
        ]));

        return $view;
    }
}

class ActiveFieldTestModel extends DynamicModel
{
    public function attributeHints()
    {
        return [
            'attributeName' => 'Hint for attributeName attribute',
        ];
    }
}

/**
 * Helper Classes.
 */
class ActiveFieldExtend extends ActiveField
{
    private $getClientOptionsEmpty;

    public function __construct($getClientOptionsEmpty = true)
    {
        $this->getClientOptionsEmpty = $getClientOptionsEmpty;
    }

    public function setClientOptionsEmpty($value): void
    {
        $this->getClientOptionsEmpty = (bool) $value;
    }

    /**
     * Useful to test other methods from ActiveField, that call ActiveField::getClientOptions()
     * but it's return value is not relevant for the test being run.
     */
    public function getClientOptions()
    {
        return ($this->getClientOptionsEmpty) ? [] : parent::getClientOptions();
    }
}

class TestValidator extends Validator
{
    public function clientValidateAttribute($object, $attribute, $view)
    {
        return 'return true;';
    }

    public function setWhenClient($js): void
    {
        $this->whenClient = $js;
    }
}

class TestInputWidget extends InputWidget
{
    /**
     * @var static
     */
    public static $lastInstance;

    public function init(): void
    {
        parent::init();
        self::$lastInstance = $this;
    }

    public function run()
    {
        return 'Render: ' . get_class($this);
    }
}

class TestMaskedInput extends MaskedInput
{
    /**
     * @var static
     */
    public static $lastInstance;

    public function init(): void
    {
        parent::init();
        self::$lastInstance = $this;
    }

    public function getOptions()
    {
        return $this->options;
    }

    public function run()
    {
        return 'Options: ' . implode(', ', array_map(
            function ($v, $k) {
                return sprintf('%s="%s"', $k, $v);
            },
            $this->options,
            array_keys($this->options)
        ));
    }
}

class TestActiveFieldWithException extends ActiveField
{
    public function render($content = null)
    {
        throw new Exception('Test exception in toString.');
    }
}
