<?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\base;

use yii\base\Behavior;
use yii\base\Component;
use yii\base\Event;
use yii\base\InvalidConfigException;
use yii\base\UnknownMethodException;
use yiiunit\TestCase;

function globalEventHandler($event): void
{
    $event->sender->eventHandled = true;
}

function globalEventHandler2($event): void
{
    $event->sender->eventHandled = true;
    $event->handled = true;
}

/**
 * @group base
 */
class ComponentTest extends TestCase
{
    /**
     * @var NewComponent
     */
    protected $component;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mockApplication();
        $this->component = new NewComponent();
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        $this->component = null;
        gc_enable();
        gc_collect_cycles();
    }

    public function testClone(): void
    {
        $component = new NewComponent();
        $behavior = new NewBehavior();
        $component->attachBehavior('a', $behavior);
        $this->assertSame($behavior, $component->getBehavior('a'));
        $component->on('test', 'fake');
        $this->assertTrue($component->hasEventHandlers('test'));
        $component->on('*', 'fakeWildcard');
        $this->assertTrue($component->hasEventHandlers('foo'));

        $clone = clone $component;
        $this->assertNotSame($component, $clone);
        $this->assertNull($clone->getBehavior('a'));
        $this->assertFalse($clone->hasEventHandlers('test'));
        $this->assertFalse($clone->hasEventHandlers('foo'));
        $this->assertFalse($clone->hasEventHandlers('*'));
    }

    public function testHasProperty(): void
    {
        $this->assertTrue($this->component->hasProperty('Text'));
        $this->assertTrue($this->component->hasProperty('text'));
        $this->assertFalse($this->component->hasProperty('Caption'));
        $this->assertTrue($this->component->hasProperty('content'));
        $this->assertFalse($this->component->hasProperty('content', false));
        $this->assertFalse($this->component->hasProperty('Content'));
    }

    public function testCanGetProperty(): void
    {
        $this->assertTrue($this->component->canGetProperty('Text'));
        $this->assertTrue($this->component->canGetProperty('text'));
        $this->assertFalse($this->component->canGetProperty('Caption'));
        $this->assertTrue($this->component->canGetProperty('content'));
        $this->assertFalse($this->component->canGetProperty('content', false));
        $this->assertFalse($this->component->canGetProperty('Content'));
    }

    public function testCanSetProperty(): void
    {
        $this->assertTrue($this->component->canSetProperty('Text'));
        $this->assertTrue($this->component->canSetProperty('text'));
        $this->assertFalse($this->component->canSetProperty('Object'));
        $this->assertFalse($this->component->canSetProperty('Caption'));
        $this->assertTrue($this->component->canSetProperty('content'));
        $this->assertFalse($this->component->canSetProperty('content', false));
        $this->assertFalse($this->component->canSetProperty('Content'));

        // behavior
        $this->assertFalse($this->component->canSetProperty('p2'));
        $behavior = new NewBehavior();
        $this->component->attachBehavior('a', $behavior);
        $this->assertTrue($this->component->canSetProperty('p2'));
        $this->component->detachBehavior('a');
    }

    public function testGetProperty(): void
    {
        $this->assertSame('default', $this->component->Text);
        $this->expectException('yii\base\UnknownPropertyException');
        $value2 = $this->component->Caption;
    }

    public function testSetProperty(): void
    {
        $value = 'new value';
        $this->component->Text = $value;
        $this->assertEquals($value, $this->component->Text);
        $this->expectException('yii\base\UnknownPropertyException');
        $this->component->NewMember = $value;
    }

    public function testIsset(): void
    {
        $this->assertTrue(isset($this->component->Text));
        $this->assertNotEmpty($this->component->Text);

        $this->component->Text = '';
        $this->assertTrue(isset($this->component->Text));
        $this->assertEmpty($this->component->Text);

        $this->component->Text = null;
        $this->assertFalse(isset($this->component->Text));
        $this->assertEmpty($this->component->Text);

        $this->assertFalse(isset($this->component->p2));
        $this->component->attachBehavior('a', new NewBehavior());
        $this->component->setP2('test');
        $this->assertTrue(isset($this->component->p2));
    }

    public function testCallUnknownMethod(): void
    {
        $this->expectException('yii\base\UnknownMethodException');
        $this->component->unknownMethod();
    }

    public function testUnset(): void
    {
        unset($this->component->Text);
        $this->assertFalse(isset($this->component->Text));
        $this->assertEmpty($this->component->Text);

        $this->component->attachBehavior('a', new NewBehavior());
        $this->component->setP2('test');
        $this->assertEquals('test', $this->component->getP2());

        unset($this->component->p2);
        $this->assertNull($this->component->getP2());
    }

    public function testUnsetReadonly(): void
    {
        $this->expectException('yii\base\InvalidCallException');
        unset($this->component->object);
    }

    public function testOn(): void
    {
        $this->assertFalse($this->component->hasEventHandlers('click'));
        $this->component->on('click', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('click'));

        $this->assertFalse($this->component->hasEventHandlers('click2'));
        $p = 'on click2';
        $this->component->$p = 'foo2';
        $this->assertTrue($this->component->hasEventHandlers('click2'));
    }

    /**
     * @depends testOn
     */
    public function testOff(): void
    {
        $this->assertFalse($this->component->hasEventHandlers('click'));
        $this->component->on('click', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('click'));
        $this->component->off('click', 'foo');
        $this->assertFalse($this->component->hasEventHandlers('click'));

        $this->component->on('click2', 'foo');
        $this->component->on('click2', 'foo2');
        $this->component->on('click2', 'foo3');
        $this->assertTrue($this->component->hasEventHandlers('click2'));
        $this->component->off('click2', 'foo3');
        $this->assertTrue($this->component->hasEventHandlers('click2'));
        $this->component->off('click2');
        $this->assertFalse($this->component->hasEventHandlers('click2'));
    }

    /**
     * @depends testOn
     */
    public function testTrigger(): void
    {
        $this->component->on('click', $this->component->myEventHandler(...));
        $this->assertFalse($this->component->eventHandled);
        $this->assertNull($this->component->event);
        $this->component->raiseEvent();
        $this->assertTrue($this->component->eventHandled);
        $this->assertEquals('click', $this->component->event->name);
        $this->assertEquals($this->component, $this->component->event->sender);
        $this->assertFalse($this->component->event->handled);

        $eventRaised = false;
        $this->component->on('click', function ($event) use (&$eventRaised): void {
            $eventRaised = true;
        });
        $this->component->raiseEvent();
        $this->assertTrue($eventRaised);

        // raise event w/o parameters
        $eventRaised = false;
        $this->component->on('test', function ($event) use (&$eventRaised): void {
            $eventRaised = true;
        });
        $this->component->trigger('test');
        $this->assertTrue($eventRaised);
    }

    /**
     * @depends testOn
     */
    public function testOnWildcard(): void
    {
        $this->assertFalse($this->component->hasEventHandlers('group.click'));
        $this->component->on('group.*', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('group.click'));
        $this->assertFalse($this->component->hasEventHandlers('category.click'));
    }

    /**
     * @depends testOnWildcard
     * @depends testOff
     */
    public function testOffWildcard(): void
    {
        $this->assertFalse($this->component->hasEventHandlers('group.click'));
        $this->component->on('group.*', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('group.click'));
        $this->component->off('*', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('group.click'));
        $this->component->off('group.*', 'foo');
        $this->assertFalse($this->component->hasEventHandlers('group.click'));

        $this->component->on('category.*', 'foo');
        $this->component->on('category.*', 'foo2');
        $this->component->on('category.*', 'foo3');
        $this->assertTrue($this->component->hasEventHandlers('category.click'));
        $this->component->off('category.*', 'foo3');
        $this->assertTrue($this->component->hasEventHandlers('category.click'));
        $this->component->off('category.*');
        $this->assertFalse($this->component->hasEventHandlers('category.click'));
    }

    /**
     * @depends testTrigger
     */
    public function testTriggerWildcard(): void
    {
        $this->component->on('cli*', $this->component->myEventHandler(...));
        $this->assertFalse($this->component->eventHandled);
        $this->assertNull($this->component->event);
        $this->component->raiseEvent();
        $this->assertTrue($this->component->eventHandled);
        $this->assertEquals('click', $this->component->event->name);
        $this->assertEquals($this->component, $this->component->event->sender);
        $this->assertFalse($this->component->event->handled);

        $eventRaised = false;
        $this->component->on('cli*', function ($event) use (&$eventRaised): void {
            $eventRaised = true;
        });
        $this->component->raiseEvent();
        $this->assertTrue($eventRaised);

        // raise event w/o parameters
        $eventRaised = false;
        $this->component->on('group.*', function ($event) use (&$eventRaised): void {
            $eventRaised = true;
        });
        $this->component->trigger('group.test');
        $this->assertTrue($eventRaised);
    }

    public function testHasEventHandlers(): void
    {
        $this->assertFalse($this->component->hasEventHandlers('click'));

        $this->component->on('click', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('click'));

        $this->component->on('*', 'foo');
        $this->assertTrue($this->component->hasEventHandlers('some'));
    }

    public function testStopEvent(): void
    {
        $component = new NewComponent();
        $component->on('click', 'yiiunit\framework\base\globalEventHandler2');
        $component->on('click', $this->component->myEventHandler(...));
        $component->raiseEvent();
        $this->assertTrue($component->eventHandled);
        $this->assertFalse($this->component->eventHandled);
    }

    public function testAttachBehavior(): void
    {
        $component = new NewComponent();
        $this->assertFalse($component->hasProperty('p'));
        $this->assertFalse($component->behaviorCalled);
        $this->assertNull($component->getBehavior('a'));

        $behavior = new NewBehavior();
        $component->attachBehavior('a', $behavior);
        $this->assertSame($behavior, $component->getBehavior('a'));
        $this->assertTrue($component->hasProperty('p'));
        $component->test();
        $this->assertTrue($component->behaviorCalled);

        $this->assertSame($behavior, $component->detachBehavior('a'));
        $this->assertFalse($component->hasProperty('p'));
        try {
            $component->test();
            $this->fail('Expected exception ' . UnknownMethodException::class . " wasn't thrown");
        } catch (UnknownMethodException $e) {
            // Expected
        }

        $component = new NewComponent();
        $component->{'as b'} = ['class' => NewBehavior::class];
        $this->assertInstanceOf(NewBehavior::class, $component->getBehavior('b'));
        $this->assertTrue($component->hasProperty('p'));
        $component->test();
        $this->assertTrue($component->behaviorCalled);

        $component->{'as c'} = ['__class' => NewBehavior::class];
        $this->assertNotNull($component->getBehavior('c'));

        $component->{'as d'} = [
            '__class' => NewBehavior2::class,
            'class' => NewBehavior::class,
        ];
        $this->assertInstanceOf(NewBehavior2::class, $component->getBehavior('d'));

        // CVE-2024-4990
        try {
            $component->{'as e'} = [
                '__class' => 'NotExistsBehavior',
                'class' => NewBehavior::class,
            ];
            $this->fail('Expected exception ' . InvalidConfigException::class . " wasn't thrown");
        } catch (InvalidConfigException $e) {
            $this->assertSame('Class is not of type yii\base\Behavior or its subclasses', $e->getMessage());
        }

        $component = new NewComponent();
        $component->{'as f'} = function () {
            return new NewBehavior();
        };
        $this->assertNotNull($component->getBehavior('f'));
    }

    public function testAttachBehaviors(): void
    {
        $component = new NewComponent();
        $this->assertNull($component->getBehavior('a'));
        $this->assertNull($component->getBehavior('b'));

        $behavior = new NewBehavior();

        $component->attachBehaviors([
            'a' => $behavior,
            'b' => $behavior,
        ]);

        $this->assertSame(['a' => $behavior, 'b' => $behavior], $component->getBehaviors());
    }

    public function testDetachBehavior(): void
    {
        $component = new NewComponent();
        $behavior = new NewBehavior();

        $component->attachBehavior('a', $behavior);
        $this->assertSame($behavior, $component->getBehavior('a'));

        $detachedBehavior = $component->detachBehavior('a');
        $this->assertSame($detachedBehavior, $behavior);
        $this->assertNull($component->getBehavior('a'));

        $detachedBehavior = $component->detachBehavior('z');
        $this->assertNull($detachedBehavior);
    }

    public function testDetachBehaviors(): void
    {
        $component = new NewComponent();
        $behavior = new NewBehavior();

        $component->attachBehavior('a', $behavior);
        $this->assertSame($behavior, $component->getBehavior('a'));
        $component->attachBehavior('b', $behavior);
        $this->assertSame($behavior, $component->getBehavior('b'));

        $component->detachBehaviors();
        $this->assertNull($component->getBehavior('a'));
        $this->assertNull($component->getBehavior('b'));
    }

    public function testSetReadOnlyProperty(): void
    {
        $this->expectException('\yii\base\InvalidCallException');
        $this->expectExceptionMessage('Setting read-only property: yiiunit\framework\base\NewComponent::object');
        $this->component->object = 'z';
    }

    public function testSetPropertyOfBehavior(): void
    {
        $this->assertNull($this->component->getBehavior('a'));

        $behavior = new NewBehavior();
        $this->component->attachBehaviors([
            'a' => $behavior,
        ]);
        $this->component->p = 'Yii is cool.';

        $this->assertSame('Yii is cool.', $this->component->getBehavior('a')->p);
    }

    public function testSettingBehaviorWithSetter(): void
    {
        $behaviorName = 'foo';
        $this->assertNull($this->component->getBehavior($behaviorName));
        $p = 'as ' . $behaviorName;
        $this->component->$p = __NAMESPACE__ . '\NewBehavior';
        $this->assertSame(__NAMESPACE__ . '\NewBehavior', $this->component->getBehavior($behaviorName) !== null ? $this->component->getBehavior($behaviorName)::class : self::class);
    }

    public function testWriteOnlyProperty(): void
    {
        $this->expectException('\yii\base\InvalidCallException');
        $this->expectExceptionMessage('Getting write-only property: yiiunit\framework\base\NewComponent::writeOnly');
        $this->component->writeOnly;
    }

    public function testSuccessfulMethodCheck(): void
    {
        $this->assertTrue($this->component->hasMethod('hasProperty'));
    }

    public function testTurningOffNonExistingBehavior(): void
    {
        $this->assertFalse($this->component->hasEventHandlers('foo'));
        $this->assertFalse($this->component->off('foo'));
    }

    public function testDetachNotAttachedHandler(): void
    {
        $obj = new NewComponent();

        $obj->on('test', [$this, 'handler']);
        $this->assertFalse($obj->off('test', [$this, 'handler2']), 'Trying to remove the handler that is not attached');
        $this->assertTrue($obj->off('test', [$this, 'handler']), 'Trying to remove the attached handler');
    }

    /**
     * @see https://github.com/yiisoft/yii2/issues/17223
     */
    public function testEventClosureDetachesItself(): void
    {
        $obj = require __DIR__ . '/stub/AnonymousComponentClass.php';

        $obj->trigger('barEventOnce');
        $this->assertEquals(1, $obj->foo);
        $obj->trigger('barEventOnce');
        $this->assertEquals(1, $obj->foo);
    }
}

class NewComponent extends Component
{
    private ?\yiiunit\framework\base\NewComponent $_object = null;
    private string|null $_text = 'default';
    private array $_items = [];
    public $content;

    public function getText()
    {
        return $this->_text;
    }

    public function setText($value): void
    {
        $this->_text = $value;
    }

    public function getObject()
    {
        if (!$this->_object) {
            $this->_object = new self();
            $this->_object->_text = 'object text';
        }

        return $this->_object;
    }

    public function getExecute()
    {
        return fn($param) => $param * 2;
    }

    public function getItems()
    {
        return $this->_items;
    }

    public $eventHandled = false;
    public $event;
    public $behaviorCalled = false;

    public function myEventHandler($event): void
    {
        $this->eventHandled = true;
        $this->event = $event;
    }

    public function raiseEvent(): void
    {
        $this->trigger('click', new Event());
    }

    public function setWriteOnly(): void
    {
    }
}

class NewBehavior extends Behavior
{
    public $p;
    private $p2;

    public function getP2()
    {
        return $this->p2;
    }

    public function setP2($value): void
    {
        $this->p2 = $value;
    }

    public function test()
    {
        $this->owner->behaviorCalled = true;

        return 2;
    }
}

class NewBehavior2 extends Behavior
{
}

class NewComponent2 extends Component
{
    public $a;

    public function __construct(public $b, public $c)
    {
    }
}
