<?php

namespace Laminas\EventManager\Test;

use Laminas\EventManager\EventManager;
use PHPUnit\Framework\Assert;
use ReflectionProperty;
use Traversable;

use function array_keys;
use function array_merge;
use function iterator_to_array;
use function krsort;
use function sprintf;

use const SORT_NUMERIC;

/**
 * Trait providing utility methods and assertions for use in PHPUnit test cases.
 *
 * This trait may be composed into a test case, and provides:
 *
 * - methods for introspecting events and listeners
 * - methods for asserting listeners are attached at a specific priority
 *
 * Some functionality in this trait duplicates functionality present in the
 * version 2 EventManagerInterface and/or EventManager implementation, but
 * abstracts that functionality for use in v3. As such, components or code
 * that is testing for listener registration should use the methods in this
 * trait to ensure tests are forwards-compatible between laminas-eventmanager
 * versions.
 */
trait EventListenerIntrospectionTrait
{
    /**
     * Retrieve a list of event names from an event manager.
     *
     * @return string[]
     */
    private function getEventsFromEventManager(EventManager $events)
    {
        $r = new ReflectionProperty($events, 'events');
        $r->setAccessible(true);
        $listeners = $r->getValue($events);
        return array_keys($listeners);
    }

    /**
     * Retrieve an interable list of listeners for an event.
     *
     * Given an event and an event manager, returns an iterator with the
     * listeners for that event, in priority order.
     *
     * If $withPriority is true, the key values will be the priority at which
     * the given listener is attached.
     *
     * Do not pass $withPriority if you want to cast the iterator to an array,
     * as many listeners will likely have the same priority, and thus casting
     * will collapse to the last added.
     *
     * @param string $event
     * @param bool $withPriority
     * @return Traversable
     */
    private function getListenersForEvent($event, EventManager $events, $withPriority = false)
    {
        $r = new ReflectionProperty($events, 'events');
        $r->setAccessible(true);
        $internal = $r->getValue($events);

        $listeners = [];
        foreach ($internal[$event] ?? [] as $p => $listOfListeners) {
            foreach ($listOfListeners as $l) {
                $listeners[$p] = isset($listeners[$p]) ? array_merge($listeners[$p], $l) : $l;
            }
        }

        return $this->traverseListeners($listeners, $withPriority);
    }

    /**
     * Assert that a given listener exists at the specified priority.
     *
     * @param int $expectedPriority
     * @param string $event
     * @param string $message Failure message to use, if any.
     */
    private function assertListenerAtPriority(
        callable $expectedListener,
        $expectedPriority,
        $event,
        EventManager $events,
        $message = ''
    ) {
        $message   = $message ?: sprintf(
            'Listener not found for event "%s" and priority %d',
            $event,
            $expectedPriority
        );
        $listeners = $this->getListenersForEvent($event, $events, true);
        $found     = false;
        foreach ($listeners as $priority => $listener) {
            if (
                $listener === $expectedListener
                && $priority === $expectedPriority
            ) {
                $found = true;
                break;
            }
        }
        Assert::assertTrue($found, $message);
    }

    /**
     * Returns an indexed array of listeners for an event.
     *
     * Returns an indexed array of listeners for an event, in priority order.
     * Priority values will not be included; use this only for testing if
     * specific listeners are present, or for a count of listeners.
     *
     * @param string $event
     * @return callable[]
     */
    private function getArrayOfListenersForEvent($event, EventManager $events)
    {
        return iterator_to_array($this->getListenersForEvent($event, $events));
    }

    /**
     * Generator for traversing listeners in priority order.
     *
     * @param array $queue
     * @param bool $withPriority When true, yields priority as key.
     * @return iterable
     */
    public function traverseListeners(array $queue, $withPriority = false)
    {
        krsort($queue, SORT_NUMERIC);

        foreach ($queue as $priority => $listeners) {
            $priority = (int) $priority;
            foreach ($listeners as $listener) {
                if ($withPriority) {
                    yield $priority => $listener;
                } else {
                    yield $listener;
                }
            }
        }
    }
}
