<?php declare(strict_types=1);

namespace IMATHUZH\Messenger\Test;

require dirname(__DIR__) . '/php/Hooks/request.php';

use IMATHUZH\Messenger\AbstractNchanRequest;
use IMATHUZH\Messenger\NchanResponse;
use PHPUnit\Framework\TestCase;

class NchanRequestMock {
    public string $nextClientId = 'client';
    public array $sendArguments = [];
    protected array $nextResponse = [];

    public function willRespond(?NchanResponse $response = null) {
        array_push($this->nextResponse, $response);
    }

    public function generateResponse(): ?NchanResponse {
        return array_shift($this->nextResponse);
    }

}

class FakeNchanRequest extends AbstractNchanRequest
{
    public NchanRequestMock $mock;

    public function __construct(
        string $baseServerUrl,
        string $requestUri,
        string $subscriberType,
        string $nchanVersion
    ) {
        parent::__construct($baseServerUrl, $requestUri, $subscriberType, $nchanVersion);
        $this->mock = new NchanRequestMock();
    }

    public function send(string $payload, string $url): ?NchanResponse {
        array_push($this->mock->sendArguments, [
            'payload' => $payload,
            'url' => $url
        ]);
        return $this->mock->generateResponse();
    }

    public function generateClientId(): string {
        return $this->mock->nextClientId;
    }
}

class NchanRequestTest extends TestCase
{
    protected static function channelProvider()
    {
        yield 'HTTP publisher' => [
            'subscriber' => 'http',
            'type' => 'publisher',
            'group' => 'group1',
            'pub' => ['ch1', 'ch2'],
            'controller' => 'group1/ctl/ch1,ch2',
            'publisher' => 'group1/pub/ch1,ch2'
        ];
        yield 'Websocket publisher' => [
            'subscriber' => 'websocket',
            'type' => 'listener',
            'client' => 'wsclient',
            'group' => 'chat',
            'pub' => ['pub1','pub2'],
            'controller' => 'chat/ctl/pub1,pub2',
            'publisher' => 'chat/pub/pub1,pub2',
            'response' => 'chat/user/wsclient'
        ];
        yield 'Websocket subscriber' => [
            'subscriber' => 'websocket',
            'type' => 'listener',
            'client' => 'wsclient',
            'group' => 'chat',
            'sub' => ['sub1','sub2'],
            'controller' => 'chat/ctl/sub1,sub2',
            'response' => 'chat/user/wsclient'
        ];
        yield 'Websocket dual channel' => [
            'subscriber' => 'websocket',
            'type' => 'listener',
            'client' => 'wsclient',
            'group' => 'chat',
            'pub' => ['pub1','pub2'],
            'sub' => ['sub1','sub2'],
            'controller' => 'chat/ctl/pub1,pub2,sub1,sub2',
            'publisher' => 'chat/pub/pub1,pub2',
            'response' => 'chat/user/wsclient'
        ];
        yield 'Websocket dual channel echo' => [
            'subscriber' => 'websocket',
            'type' => 'listener',
            'client' => 'wsclient',
            'group' => 'chat',
            'pub' => ['channel'],
            'sub' => ['channel'],
            'controller' => 'chat/ctl/channel',
            'publisher' => 'chat/pub/channel',
            'response' => 'chat/user/wsclient'
        ];
        yield 'Websocket controller' => [
            'subscriber' => 'websocket',
            'type' => 'controller',
            'client' => 'wsclient',
            'group' => 'chat',
            'sub' => ['chA'],
            'controller' => 'chat/ctl/',
            'response' => 'chat/controller/wsclient'
        ];
        yield 'Websocket controller publisher' => [
            'subscriber' => 'websocket',
            'type' => 'controller',
            'client' => 'wsclient',
            'group' => 'chat',
            'pub' => ['chA'],
            'pub' => ['chB'],
            'controller' => 'chat/ctl/',
            'publisher' => 'chat/ctl/chB',
            'response' => 'chat/controller/wsclient'
        ];
    }

    protected static function getRandomServerBase(): string {
        return 'http://localhost:' . random_int(8000,9000);
    }

    public static function channelDataProvider()
    {
        foreach (self::channelProvider() as $label => $data) {
            
            $serverBase = 'http://localhost:' . random_int(8000,9000);
            $local = '/messenger/';

            yield $label => [
                $data['type'],
                $data['subscriber'],
                $serverBase,
                $data['group'],
                $data['pub'] ?? [],
                $data['sub'] ?? [],
                $data['client'] ?? '',
                $local . $data['controller'],
                isset($data['publisher']) ?  $local . $data['publisher'] : '',
                isset($data['response']) ? $local . $data['response'] : ''
            ];
        }
    }

    public static function nonPublisherDataProvider()
    {
        foreach (self::channelDataProvider() as $label => $data) {
            if ($data[0] !== 'publisher') {
                yield $label => $data;
            }
        }
    }

    public static function channelMetaDataProvider()
    {
        // Make sure that at least one data set has an anonynous session
        $sessionId = '';
        foreach (self::channelProvider() as $label => $data) {
        
            $metaData = [
                'group' => $data['group'],
                'pub' => $data['pub'] ?? [],
                'sub' => $data['sub'] ?? [],
                'clientId' => $data['client'] ?? '',
                'sessionId' => $sessionId ?: 'anonymous'
            ];

            yield $label => [
                $data['subscriber'],
                $data['group'],
                $data['pub'] ?? [],
                $data['sub'] ?? [],
                $data['client'] ?? '',
                $sessionId,
                $metaData
            ];

            $sessionId .= bin2hex(random_bytes(2));
        }
    }


    public function createGenericRequest(
        string $subscriberType = '',
        string $clientId = '',
        string $serverBase = '',
        string $requestUri = '',
        string $nchanVersion = ''
    ): FakeNchanRequest {
        $request = new FakeNchanRequest(
            $serverBase ?: 'http://localhost:8080',
            $requestUri ?: 'http://test.com/messenger/channel',
            $subscriberType ?: 'websocket',
            $nchanVersion ?: '0.12.6'
        );
        $request->mock->nextClientId = $clientId ?: 'testclient';
        return $request;
    }

    public function testAccessingProperties(): void
    {
        $request = $this->createGenericRequest();

        $request->setRequesterType('');
        $this->assertEquals('unknown', $request->getRequesterType());
        $request->setRequesterType('set');
        $this->assertEquals('set', $request->getRequesterType());

        $request->setChannelGroup('groupname');
        $this->assertEquals('groupname', $request->getChannelGroup());

        $request->setClientId('some-client');
        $this->assertEquals('some-client', $request->getClientId());

        $request->setSessionId('');
        $this->assertEquals('anonymous', $request->getSessionId());
        $request->setSessionId('some-session');
        $this->assertEquals('some-session', $request->getSessionId());

        $request->setPubChannels(['abc', 'xyz']);
        $this->assertEquals(['abc', 'xyz'], $request->getPubChannels());
        $this->assertEquals('abc,xyz', $request->getFlattenedPubChannels());

        $request->setFlattenedPubChannels('abc,xyz');
        $this->assertEquals(['abc', 'xyz'], $request->getPubChannels());
        $this->assertEquals('abc,xyz', $request->getFlattenedPubChannels());

        $request->setSubChannels(['abc', 'xyz']);
        $this->assertEquals(['abc', 'xyz'], $request->getSubChannels());
        $this->assertEquals('abc,xyz', $request->getFlattenedSubChannels());

        $request->setFlattenedSubChannels('abc,xyz');
        $this->assertEquals(['abc', 'xyz'], $request->getSubChannels());
        $this->assertEquals('abc,xyz', $request->getFlattenedSubChannels());

        $request->setPubChannels([]);
        $this->assertEquals([], $request->getPubChannels());
        $this->assertEquals('', $request->getFlattenedPubChannels());

        $request->setFlattenedPubChannels('');
        $this->assertEquals([], $request->getPubChannels());
        $this->assertEquals('', $request->getFlattenedPubChannels());

        $request->setSubChannels([]);
        $this->assertEquals([], $request->getSubChannels());
        $this->assertEquals('', $request->getFlattenedSubChannels());

        $request->setFlattenedSubChannels('');
        $this->assertEquals([], $request->getSubChannels());
        $this->assertEquals('', $request->getFlattenedSubChannels());
    }

    /**
     * @dataProvider channelDataProvider
     */
    public function testFromServerRequest(
        string $requesterType,
        string $subscriberType,
        string $serverBase,
        string $group,
        array $pubChannels,
        array $subChannels,
        string $clientId,
        string $controllerUrl,
        string $publishUrl,
        string $responseUrl
    ) {
        $nchanVersion = "1.3.6";
        $originalUri = 'https://test.com/some/path';
        $sessionId = bin2hex(random_bytes(10));

        $_SERVER[AbstractNchanRequest::HEADER_SUBSCRIBER_TYPE] = $subscriberType;
        $_SERVER[AbstractNchanRequest::HEADER_NCHAN_VERSION] = $nchanVersion;
        $_SERVER[AbstractNchanRequest::HEADER_ORIGINAL_URI] = $originalUri;
        $_SERVER[AbstractNchanRequest::HEADER_MESSENGER_TYPE] = $requesterType;
        $_SERVER[AbstractNchanRequest::HEADER_MESSENGER_GROUP] = $group;
        $_SERVER[AbstractNchanRequest::HEADER_MESSENGER_PUB] = implode(",", $pubChannels);
        $_SERVER[AbstractNchanRequest::HEADER_MESSENGER_SUB] = implode(",", $subChannels);
        $_SERVER[AbstractNchanRequest::HEADER_MESSENGER_USER] = $clientId;
        $_SERVER[AbstractNchanRequest::HEADER_MESSENGER_SESSION] = $sessionId;

        $request = FakeNchanRequest::fromServerRequest();

        $this->assertEquals($nchanVersion, $request->nchanVersion);
        $this->assertEquals($subscriberType, $request->subscriberType);
        $this->assertEquals($originalUri, $request->requestUri);

        $this->assertEquals($request->getRequesterType(), $requesterType);
        $this->assertEquals($request->getChannelGroup(), $group);
        $this->assertEquals($request->getPubChannels(), $pubChannels);
        $this->assertEquals($request->getSubChannels(), $subChannels);
        $this->assertEquals($request->getClientId(), $clientId);
        $this->assertEquals($request->getSessionId(), $sessionId);
    }



    /**
     * @dataProvider channelDataProvider
     */
    public function testGenerateUrl(
        string $requesterType,
        string $subscriberType,
        string $serverBase,
        string $group,
        array $pubChannels,
        array $subChannels,
        string $clientId,
        string $controllerUrl,
        string $publishUrl,
        string $responseUrl
    ): void {
        $generatedClientId = $clientId . 'generated';
        $request = $this->createGenericRequest($subscriberType, $generatedClientId, $serverBase);

        $request
            ->setRequesterType($requesterType)
            ->setPubChannels($pubChannels)
            ->setSubChannels($subChannels)
            ->setChannelGroup($group)
            ->setClientId($clientId)
            ->generateUrls();

        $this->assertEquals($subscriberType, $request->subscriberType);
        $this->assertEquals($controllerUrl, $request->controllerUrl);
        if ($publishUrl) $this->assertEquals($publishUrl, $request->publishUrl);
        if ($responseUrl) $this->assertEquals($responseUrl, $request->responseUrl);
    }

    /**
     * @dataProvider channelDataProvider
     */
    public function testGenerateUrlWithNoClientId(
        string $requesterType,
        string $subscriberType,
        string $serverBase,
        string $group,
        array $pubChannels,
        array $subChannels,
        string $clientId,
        string $controllerUrl,
        string $publishUrl,
        string $responseUrl
    ): void {

        if (!$clientId) {
            // PHP unit complains if there is no assertion
            $this->assertTrue(true);
            return;
        }

        $generatedClientId = $clientId . 'generated';
        $request = $this->createGenericRequest($subscriberType, $generatedClientId, $serverBase);

        $request
            ->setRequesterType($requesterType)
            ->setPubChannels($pubChannels)
            ->setSubChannels($subChannels)
            ->setChannelGroup($group)
            ->generateUrls();

        $this->assertEquals($generatedClientId, $request->getClientId());

        $controllerUrl = strtr($controllerUrl, [ $clientId => $generatedClientId ]);
        $publishUrl = strtr($publishUrl, [ $clientId => $generatedClientId ]);
        $responseUrl = strtr($responseUrl, [ $clientId => $generatedClientId ]);

        $this->assertEquals($controllerUrl, $request->controllerUrl);
        if ($publishUrl) $this->assertEquals($publishUrl, $request->publishUrl);
        if ($responseUrl) $this->assertEquals($responseUrl, $request->responseUrl);
    }

    public function testGetRequestMetaData(): void
    {
        $serverBase = 'http://localhost:81';
        $requestUri = 'https://test.com/messenger/channel';
        $subscriberType = 'websocket';
        $nchanVersion = '0.12.8';

        $request = $this->createGenericRequest(
            $subscriberType,
            'clientname',
            $serverBase,
            $requestUri,
            $nchanVersion
        );
        $metaData = $request->getRequestMetaData();

        $this->assertEquals($requestUri, $metaData['requestUri']);
        $this->assertEquals($nchanVersion, $metaData['nchanVersion']);
        $this->assertEquals($subscriberType, $metaData['subscriberType']);
    }

    /**
     * @dataProvider channelMetaDataProvider
     */
    public function testGetChannelMetaData(
        string $subscriberType,
        string $group,
        array $pubChannels,
        array $subChannels,
        string $clientId,
        string $sessionId,
        array $expected
    ): void {

        $request = $this->createGenericRequest($subscriberType, 'testclient')
            ->setChannelGroup($group)
            ->setPubChannels($pubChannels)
            ->setSubChannels($subChannels)
            ->setClientId($clientId)
            ->setSessionId($sessionId);

        if ($subscriberType !== 'http' && !$expected['clientId']) {
            $expected['clientId'] = 'testclient';
        }

        $this->assertEquals($expected, $request->getChannelMetaData());
    }

    /**
     * @dataProvider nonPublisherDataProvider
     */
    public function testResponse(
        string $requesterType,
        string $subscriberType,
        string $serverBase,
        string $group,
        array $pubChannels,
        array $subChannels,
        string $clientId,
        string $controllerUrl,
        string $publishUrl,
        string $responseUrl
    ): void {

        $payload = [
            'action' => 'test',
            'value' => 42
        ];
        $expectedResponse = new NchanResponse([], json_encode(['key' => 'value']));

        $request = $this->createGenericRequest($subscriberType, '', $serverBase)
            ->setRequesterType($requesterType)
            ->setChannelGroup($group)
            ->setPubChannels($pubChannels)
            ->setSubChannels($subChannels)
            ->setClientId($clientId);
        $request->generateUrls();

        $request->mock->willRespond($expectedResponse);

        $response = $request->respond($payload);
        $sendArgs = $request->mock->sendArguments[0];

        # response() does not modify the result from send()
        $this->assertEquals($expectedResponse, $response);
        # response() sends to responseUrl
        $this->assertEquals($serverBase.$responseUrl, $sendArgs['url']);
        # response() sends json-encoded payload
        $this->assertIsString($sendArgs['payload']);
        $this->assertEquals($payload, json_decode($sendArgs['payload'], true));
    }


    /**
     * @dataProvider channelDataProvider
     */
    public function testNotifyController(
        string $requesterType,
        string $subscriberType,
        string $serverBase,
        string $group,
        array $pubChannels,
        array $subChannels,
        string $clientId,
        string $controllerUrl,
        string $publishUrl,
        string $responseUrl
    ): void {

        $event = 'some-event';
        $payload = [
            'action' => 'test',
            'value' => 42
        ];
        $expectedResponse = new NchanResponse([], json_encode(['key' => 'value']));

        $request = $this->createGenericRequest($subscriberType, '', $serverBase)
            ->setRequesterType($requesterType)
            ->setChannelGroup($group)
            ->setPubChannels($pubChannels)
            ->setSubChannels($subChannels)
            ->setClientId($clientId);
        $request->generateUrls();

        $request->mock->willRespond($expectedResponse);

        $response = $request->notifyController($event, $payload);
        $sendArgs = $request->mock->sendArguments[0];

        # notifyController() does not modify the result from send()
        $this->assertEquals($expectedResponse, $response);
        # notifyController() sends to controllerUrl
        $this->assertEquals($serverBase.$controllerUrl, $sendArgs['url']);
        # response() sends a JSON object with extra channel data
        $this->assertIsString($sendArgs['payload']);
        $this->assertEquals([
            'type' => 'message',
            'name' => $event,
            'payload' => array_merge($payload, $request->getRequestMetaData())
        ], json_decode($sendArgs['payload'], true));
    }

}
