<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Event
 * @subpackage UnitTests
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

/**
 * @category   Core
 * @package    Core_Event
 * @subpackage UnitTests
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 * @group      Core_Event
 */
class Core_Event_DispatcherTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var Core_Event_Dispatcher
     */
    protected $_object;

    protected function setUp()
    {
        $this->_object = new Core_Event_Dispatcher;
    }

    /**
     * @covers Core_Event_Dispatcher_Interface
     */
    public function testCoversInterface()
    {
        $this->assertTrue(true);
    }

    /**
     * @covers Core_Event_Dispatcher::setEventSubscribers
     * @covers Core_Event_Dispatcher::getEventSubscribers
     */
    public function testCanSetAndGetEventSubscribers()
    {
        $this->_object->setEventSubscribers('eventName', array(1, 2, 3));
        $this->assertEquals(array(1, 2, 3), $this->_object->getEventSubscribers('eventName'));
    }

    /**
     * @covers Core_Event_Dispatcher::hasEventSubscribers
     */
    public function testCanTestIfThereAreEventSubscribers()
    {
        $this->_object->setEventSubscribers('foo', array('aaa'));
        $this->assertTrue($this->_object->hasEventSubscribers('foo'));
    }


    /**
     * @covers Core_Event_Dispatcher::hasEventSubscribers
     */
    public function testCanTestIfThereAreParentEventSubscribers()
    {
        $this->_object->setEventSubscribers('foo', array('aaa'));
        $this->assertTrue($this->_object->hasEventSubscribers('foo.bar'));
    }

    /**
     * @covers Core_Event_Dispatcher::getEventSubscribers
     */
    public function testNoEventSubscribersIsAnEmptyArray()
    {
        $this->assertEquals(array(), $this->_object->getEventSubscribers('noSuchEvent'));
    }

    /**
     * @covers Core_Event_Dispatcher::subscribe
     */
    public function testCanSubscribeACallbackToAnEvent()
    {
        $this->_object->subscribe('my.event', array('some', 'callback'));
        $subscriber = array('filter' => null, 'callback' => array('some', 'callback'));
        $this->assertContains($subscriber, $this->_object->getEventSubscribers('my.event'));
    }

     /**
     * @covers Core_Event_Dispatcher::subscribe
     */
    public function testOrderOfSubscriptionWillBeRetained()
    {
        $callback1 = array('some', 'callback1');
        $callback2 = array('some', 'callback2');

        $this->_object->subscribe('my.event', $callback1);
        $this->_object->subscribe('my.event', $callback2);

        // callback1 is in front of callback2
        $this->assertSame(
            array(
                array('filter' => null, 'callback' => $callback1),
                array('filter' => null, 'callback' => $callback2),
            ),
            $this->_object->getEventSubscribers('my.event')
        );
    }

    /**
     * @covers Core_Event_Dispatcher::subscribe
     */
    public function testCanChangeOrderBySettingPriority()
    {
        $callback1 = array('some', 'callback1');
        $callback2 = array('some', 'callback2');

        $this->_object->subscribe('my.event', $callback1, 2);
        $this->_object->subscribe('my.event', $callback2, 1);

        // callback1 is behind callback2
        $this->assertSame(
            array(
                array('filter' => null, 'callback' => $callback2),
                array('filter' => null, 'callback' => $callback1),
            ),
            $this->_object->getEventSubscribers('my.event')
        );
    }

    /**
     * @covers Core_Event_Dispatcher::subscribe
     */
    public function testNegativePriorityIsHigherThanPositive()
    {
        $callback1 = array('some', 'callback1');
        $callback2 = array('some', 'callback2');

        $this->_object->subscribe('my.event', $callback1, 1);
        $this->_object->subscribe('my.event', $callback2, -1);

        // callback1 is behind callback2
        $this->assertSame(
            array(
                array('filter' => null, 'callback' => $callback2),
                array('filter' => null, 'callback' => $callback1),
            ),
            $this->_object->getEventSubscribers('my.event')
        );
    }

    /**
     * @covers Core_Event_Dispatcher::subscribe
     */
    public function testDefaultPriorityIsZero()
    {
        $callback1 = array('some', 'callback1');
        $callback2 = array('some', 'callback2');
        $callback3 = array('some', 'callback3');

        $this->_object->subscribe('my.event', $callback1,  1);
        $this->_object->subscribe('my.event', $callback2, -1);
        $this->_object->subscribe('my.event', $callback3);   // implicit, 0

        // keys are -1, 0, 1
        $this->assertSame(
            array(
                array('filter' => null, 'callback' => $callback2),
                array('filter' => null, 'callback' => $callback3),
                array('filter' => null, 'callback' => $callback1),
            ),
            $this->_object->getEventSubscribers('my.event')
        );
    }

    /**
     * @covers Core_Event_Dispatcher::unsubscribe
     */
    public function testUnsubscribingFromUnknownEventsFailsSilently()
    {
        /// XXX: this is a code smell, it doesn't test anything, it's added only for code coverage
        $this->assertSame($this->_object, $this->_object->unsubscribe('no.such.event'));
    }

    /**
     * @covers Core_Event_Dispatcher::unsubscribe
     */
    public function testIfUnsubscribeCallbackNotSetUnsubscribeAllCallbacks()
    {
        $callback1 = array('some', 'callback1');
        $callback2 = array('some', 'callback2');

        $this->_object->subscribe('my.event', $callback1);
        $this->_object->subscribe('my.event', $callback2);

        $this->_object->unsubscribe('my.event');
        $this->assertEquals(array(), $this->_object->getEventSubscribers('my.event'));
    }

    /**
     * @covers Core_Event_Dispatcher::unsubscribe
     */
    public function testCanUnsubscribeSpecificCallback()
    {
        $callback1 = array('some', 'callback1');
        $callback2 = array('some', 'callback2');

        $this->_object->subscribe('my.event', $callback1);
        $this->_object->subscribe('my.event', $callback2);

        $this->_object->unsubscribe('my.event', $callback2);

        $this->assertContains(
            array('filter' => null, 'callback' => $callback1),
            $this->_object->getEventSubscribers('my.event')
        );
        $this->assertNotContains(
            array('filter' => null, 'callback' => $callback2),
            $this->_object->getEventSubscribers('my.event')
        );
    }

    /**
     * @covers Core_Event_Dispatcher::dispatch
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testCanDispatchEventIfNoSubscribersExists()
    {
        $event = $this->getMock('Core_Event', null, array($this, 'my.event'));

        /// XXX: this is a code smell, it doesn't test anything, it's added only for code coverage
        $this->assertSame($this->_object, $this->_object->dispatch($event));
    }

    /**
     * @covers Core_Event_Dispatcher::dispatch
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testCanDispatchEventToSubscribers()
    {
        $event    = $this->getMock('Core_Event', null, array($this, 'my.event'));
        $listener = new Invalid_Event_Listener;
        $this->_object
            ->subscribe('my.event', array($listener, 'eventHandler'))
            ->dispatch($event);
        $this->assertSame($event, $listener->event);
    }

    /**
     * @covers Core_Event_Dispatcher::dispatchUntil
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testWillDispatchEventUntilMarkedAsProcessed()
    {
        $event     = $this->getMock('Core_Event', null, array($this, 'my.event'));
        $listener  = new Invalid_Event_Listener;
        $processor = new Invalid_Event_Processor;
        $this->_object
            ->subscribe('my.event', array($processor, 'eventHandler'))
            ->subscribe('my.event', array($listener,  'eventHandler'))
            ->dispatchUntil($event);

        $this->assertNull($listener->event);
    }

    /**
     * @covers Core_Event_Dispatcher::dispatchUntil
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testWillDispatchToAllListenersIfNotMarkedAsProcessed()
    {
        $event     = $this->getMock('Core_Event', null, array($this, 'my.event'));
        $listener1 = new Invalid_Event_Listener;
        $listener2 = new Invalid_Event_Listener;
        $this->_object
            ->subscribe('my.event', array($listener1, 'eventHandler'))
            ->subscribe('my.event', array($listener2, 'eventHandler'))
            ->dispatchUntil($event);

        $this->assertSame($event, $listener1->event);
        $this->assertSame($event, $listener2->event);
    }

    /**
     * @covers Core_Event_Dispatcher::filter
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testSubscribersCanFilterValue()
    {
        $event       = $this->getMock('Core_Event', null, array($this, 'my.event'));
        $incrementor = new Invalid_Event_Incrementor();
        $filtered    = $this->_object
            ->subscribe('my.event', array($incrementor, 'eventHandler'))
            ->filter($event, 1);

        $this->assertEquals(2, $filtered);
    }

    /**
     * @covers Core_Event_Dispatcher::filterUntil
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testSubscribersWillFilterValueUntilEventProcessed()
    {
        $event        = $this->getMock('Core_Event', null, array($this, 'my.event'));
        $incrementor1 = new Invalid_Event_Incrementor();
        $incrementor2 = new Invalid_Event_Incrementor();
        $processor    = new Invalid_Event_Processor();
        $filtered     = $this->_object
            ->subscribe('my.event', array($incrementor1, 'eventHandler'))   // increments to 2
            ->subscribe('my.event', array($processor, 'eventHandler'))      // this one stops the processing
            ->subscribe('my.event', array($incrementor2, 'eventHandler'))   // never fired
            ->filterUntil($event, 1);

        // 2, not 3
        $this->assertEquals(2, $filtered);
    }

    /**
     * @covers Core_Event_Dispatcher::dispatch
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testFilterWillStopEventDispatch()
    {
        $event    = $this->getMock('Core_Event', null, array($this, 'my.event', array('foo' => 23)));
        $listener = new Invalid_Event_Listener;
        $this->_object
             // this should NOT get triggered as the filter will return false
            ->subscribe('my.event', array($listener, 'eventHandler'), 0, 'params.foo != 23')
            ->dispatch($event);

        // listener was not triggered
        $this->assertNull($listener->event);
    }

    /**
     * @covers Core_Event_Dispatcher::dispatch
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testCanHandleInvalidFilters()
    {
        $event     = $this->getMock('Core_Event', null, array($this, 'my.event', array('bla' => 23)));
        $listener1 = new Invalid_Event_Listener;
        $listener2 = new Invalid_Event_Listener;
        $this->_object
             // invalid filter
            ->subscribe('my.event', array($listener1, 'eventHandler'), 0, 'hm')

             // no such property as foo
            ->subscribe('my.event', array($listener2, 'eventHandler'), 0, 'params.foo != 23')
            ->dispatch($event);

        // listener was not filtered (and thus, triggered)
        $this->assertNotNull($listener1->event);
        $this->assertNotNull($listener2->event);
    }

    /**
     * @covers Core_Event_Dispatcher::dispatch
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testWillInvokeHandlersSubscribedToParentEvents()
    {
        $event    = $this->getMock('Core_Event', null, array($this, 'namespaced.event'));
        $listener = new Invalid_Event_Listener;
        $this->_object
            ->subscribe('namespaced', array($listener, 'eventHandler'))
            ->dispatch($event);

        $this->assertNotNull($listener->event);
    }

    /**
     * @covers Core_Event_Dispatcher::dispatch
     * @covers Core_Event_Dispatcher::<protected>
     */
    public function testCanStopEventFromBeingPropagatedToParentEventSubscribers()
    {
        $event     = $this->getMock('Core_Event', null, array($this, 'namespaced.event'));
        $listener1 = new Invalid_Event_Unpropagator;
        $listener2 = new Invalid_Event_Listener;
        $this->_object
            ->subscribe('namespaced.event', array($listener1, 'eventHandler'))
            ->subscribe('namespaced',       array($listener2, 'eventHandler'))
            ->dispatch($event);

        // listened2 is not invoked because listener1 stopped propagation
        $this->assertNull($listener2->event);
    }
}

class Invalid_Event_Listener
{
    public $event;

    public function eventHandler(Core_Event_Interface $event, $value = null)
    {
        $this->event = $event;
        return $value;
    }
}

class Invalid_Event_Incrementor extends Invalid_Event_Listener
{
    public function eventHandler(Core_Event_Interface $event, $value = null)
    {
        parent::eventHandler($event, $value);
        return ++$value;
    }
}

class Invalid_Event_Processor
{
    public function eventHandler(Core_Event_Interface $event, $value = null)
    {
        $event->setIsProcessed(true);
        return $value;
    }
}

class Invalid_Event_Unpropagator
{
    public function eventHandler(Core_Event_Interface $event, $value = null)
    {
        $event->setIsPropagated(false);
        return $value;
    }
}