<?php
// AttributesTest.php
// Created: 2024-08-16
// Updated: 2025-03-20

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
use Index\Http\{HttpRequest,HttpUri};
use Index\Http\Routing\Router;
use Index\Http\Streams\Stream;
use RPCii\{HmacVerificationProvider,RpciiMsgPack};
use RPCii\Server\{RpcAction,RpcHandler,RpcHandlerCommon,RpcProcedure,RpcQuery,HttpRpcServer};

#[CoversClass(RpcAction::class)]
#[CoversClass(RpcHandler::class)]
#[CoversClass(RpcProcedure::class)]
#[CoversClass(RpcQuery::class)]
#[UsesClass(HttpRpcServer::class)]
#[UsesClass(HmacVerificationProvider::class)]
final class AttributesTest extends TestCase {
    public function testAttributes(): void {
        $handler = new class($this) implements RpcHandler {
            use RpcHandlerCommon;

            private static AttributesTest $that;

            public function __construct(private AttributesTest $self) {
                self::$that = $self;
            }

            #[RpcProcedure(true, 'aiwass:test:proc1')]
            public function procedureRegisteredUsingActionAttribute(): string {
                return 'procedure registered using action attribute';
            }

            #[RpcProcedure(false, 'aiwass:test:query1')]
            public function queryRegisteredUsingActionAttribute(string $beans): string {
                $this->self->assertSame('it is beans', $beans);
                return 'query registered using action attribute';
            }

            #[RpcQuery('aiwass:test:query2')]
            public static function staticQueryRegisteredUsingQueryAttribute(string $required, string $optional = 'the'): string {
                self::$that->assertSame('internet', $required);
                self::$that->assertSame('the', $optional);
                return 'static query registered using query attribute';
            }

            #[RpcAction('aiwass:test:proc2')]
            public function dynamicProcedureRegisteredUsingProcedureAttribute(string $optional = 'meow'): string {
                $this->self->assertSame('meow', $optional);
                return 'dynamic procedure registered using procedure attribute';
            }

            #[RpcQuery('aiwass:test:query4')]
            public function queryForRouter(string $required, string $optional = 'the'): string {
                return sprintf('required=%s, optional=%s', $required, $optional);
            }

            #[RpcAction('aiwass:test:proc4')]
            public function procedureForRouter(string $required, string $optional = 'the'): string {
                return sprintf('optional=%s, required=%s', $required, $optional);
            }

            #[RpcQuery('aiwass:test:query3')]
            #[RpcAction('aiwass:test:proc3')]
            public function multiple(): string {
                return 'a dynamic method registered as both a query and a procedure';
            }

            public function hasNoAttr(): string {
                return 'not an action handler';
            }
        };

        $server = new HttpRpcServer;
        $server->register($handler);

        $this->assertNull($server->getProcedureInfo('aiwass:none'));

        $act1 = $server->getProcedureInfo('aiwass:test:proc1');
        $this->assertNotNull($act1);
        $this->assertTrue($act1->action);
        $this->assertSame('aiwass:test:proc1', $act1->name);
        $this->assertSame('procedure registered using action attribute', $act1->invokeHandler());

        $act2 = $server->getProcedureInfo('aiwass:test:proc2');
        $this->assertNotNull($act2);
        $this->assertTrue($act2->action);
        $this->assertSame('aiwass:test:proc2', $act2->name);
        $this->assertSame('dynamic procedure registered using procedure attribute', $act2->invokeHandler());

        $query1 = $server->getProcedureInfo('aiwass:test:query1');
        $this->assertNotNull($query1);
        $this->assertFalse($query1->action);
        $this->assertSame('aiwass:test:query1', $query1->name);
        $this->assertSame('query registered using action attribute', $query1->invokeHandler(['beans' => 'it is beans']));

        $query2 = $server->getProcedureInfo('aiwass:test:query2');
        $this->assertNotNull($query2);
        $this->assertFalse($query2->action);
        $this->assertSame('aiwass:test:query2', $query2->name);
        $this->assertSame('static query registered using query attribute', $query2->invokeHandler(['required' => 'internet']));

        $query3 = $server->getProcedureInfo('aiwass:test:query3');
        $proc3 = $server->getProcedureInfo('aiwass:test:proc3');
        $this->assertNotNull($query3);
        $this->assertNotNull($proc3);
        $this->assertFalse($query3->action);
        $this->asserttrue($proc3->action);
        $this->assertSame('aiwass:test:query3', $query3->name);
        $this->assertSame('aiwass:test:proc3', $proc3->name);
        $this->assertEquals($query3->handler, $proc3->handler);
        $this->assertSame('a dynamic method registered as both a query and a procedure', $query3->invokeHandler());
        $this->assertSame('a dynamic method registered as both a query and a procedure', $proc3->invokeHandler());

        $this->assertNull($server->getProcedureInfo('doesnotexist'));

        $router = new Router;
        $verification = new HmacVerificationProvider(fn() => 'meow');
        $router->register($server->createRouteHandler($verification));

        $response = $router->handle(HttpRequest::createRequestWithoutBody(
            'GET', '/_rpcii/aiwass:test:query4',
            [
                'X-RPCii-Format' => ['2'],
                'X-RPCii-Verify' => [$verification->sign(false, 'aiwass:test:query4', 'required=mewow')],
            ],
        )->withQueryParams(['required' => ['mewow']]));
        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame('application/vnd.msgpack', $response->getHeaderLine('Content-Type'));
        $this->assertSame('2', $response->getHeaderLine('X-RPCii-Format'));
        $this->assertSame(
            ['state' => 'ok', 'result' => 'required=mewow, optional=the'],
            RpciiMsgPack::decode((string)$response->getBody())
        );

        $response = $router->handle(HttpRequest::createRequestWithBody(
            'POST', '/_rpcii/aiwass:test:proc4',
            [
                'Content-Type' => ['application/x-www-form-urlencoded'],
                'X-RPCii-Format' => ['2'],
                'X-RPCii-Verify' => [$verification->sign(true, 'aiwass:test:proc4', 'optional=omg&required=woof')],
            ],
            Stream::createStream(http_build_query(['optional' => 'omg', 'required' => 'woof'], '', '&', PHP_QUERY_RFC3986)),
        ));
        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame('application/vnd.msgpack', $response->getHeaderLine('Content-Type'));
        $this->assertSame('2', $response->getHeaderLine('X-RPCii-Format'));
        $this->assertSame(
            ['state' => 'ok', 'result' => 'optional=woof, required=omg'],
            RpciiMsgPack::decode((string)$response->getBody())
        );

        $this->expectException(RuntimeException::class);
        $query1->invokeHandler(['notbeans' => 'it is not beans']);
    }
}