<?php

declare(strict_types=1);

/*
 * Copyright (c) 2017-2023 François Kooman <fkooman@tuxed.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

namespace fkooman\OAuth\Server\Tests;

use DateTimeImmutable;
use fkooman\OAuth\Server\Base64UrlSafe;
use fkooman\OAuth\Server\ClientInfo;
use fkooman\OAuth\Server\Exception\InvalidTokenException;
use fkooman\OAuth\Server\Http\Request;
use fkooman\OAuth\Server\Json;
use fkooman\OAuth\Server\PdoStorage;
use fkooman\OAuth\Server\Scope;
use fkooman\OAuth\Server\SimpleClientDb;
use PDO;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 *
 * @coversNothing
 */
final class BearerValidatorTest extends TestCase
{
    private DateTimeImmutable $dateTime;
    private TestBearerValidator $validator;
    private PdoStorage $storage;

    protected function setUp(): void
    {
        $this->dateTime = new DateTimeImmutable('2016-01-01');
        $clientDb = new SimpleClientDb();
        $clientDb->add(new ClientInfo('code-client', ['http://example.org/code-cb'], null, 'Code Client', true));
        $this->storage = new PdoStorage(new PDO('sqlite::memory:'));
        $this->storage->init();
        $this->validator = new TestBearerValidator(
            $this->storage,
            $clientDb,
            new DummySigner(),
            $this->dateTime
        );
    }

    public function testValidToken(): void
    {
        $this->storage->storeAuthorization('foo', 'code-client', new Scope('config'), 'random_1', $this->dateTime, new DateTimeImmutable('2016-03-31T00:00:00+00:00'));
        $providedAccessToken = Base64UrlSafe::encodeUnpadded(
            Json::encode(
                [
                    'type' => 'access_token',
                    'token_id' => 'foo',
                    'auth_key' => 'random_1',
                    'user_id' => 'foo',
                    'client_id' => 'code-client',
                    'scope' => 'config',
                    'expires_at' => '2016-01-01T01:00:00+00:00',
                    'authorization_expires_at' => '2016-03-31T00:00:00+00:00',
                ]
            )
        );
        $accessToken = $this->validator->validate(
            new Request(
                [
                    'HTTP_AUTHORIZATION' => 'Bearer ' . $providedAccessToken,
                ],
                [],
                []
            )
        );
        static::assertSame('foo', $accessToken->userId());
        static::assertSame('config', (string) $accessToken->scope());
        static::assertSame(
            'eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwidG9rZW5faWQiOiJmb28iLCJhdXRoX2tleSI6InJhbmRvbV8xIiwidXNlcl9pZCI6ImZvbyIsImNsaWVudF9pZCI6ImNvZGUtY2xpZW50Iiwic2NvcGUiOiJjb25maWciLCJleHBpcmVzX2F0IjoiMjAxNi0wMS0wMVQwMTowMDowMCswMDowMCIsImF1dGhvcml6YXRpb25fZXhwaXJlc19hdCI6IjIwMTYtMDMtMzFUMDA6MDA6MDArMDA6MDAifQ',
            $accessToken->raw()
        );
    }

    public function testInvalidSignature(): void
    {
        try {
            $providedAccessToken = Base64UrlSafe::encodeUnpadded(
                Json::encode(
                    [
                        'type' => 'access_token',
                        'token_id' => 'foo',
                        'auth_key' => 'invalid_sig',
                        'user_id' => 'foo',
                        'client_id' => 'code-client',
                        'scope' => 'config',
                        'expires_at' => '2016-01-01T01:00:00+00:00',
                        'authorization_expires_at' => '2016-03-31T00:00:00+00:00',
                    ]
                )
            );

            $this->storage->storeAuthorization('foo', 'code-client', new Scope('config'), 'random_1', $this->dateTime, new DateTimeImmutable('2016-03-31T00:00:00+00:00'));
            $this->validator->validate(
                new Request(
                    [
                        'HTTP_AUTHORIZATION' => 'Bearer ' . $providedAccessToken,
                    ],
                    [],
                    []
                )
            );
            static::fail();
        } catch (InvalidTokenException $e) {
            static::assertSame('invalid "access_token": invalid signature', $e->getDescription());
        }
    }

    public function testDeletedClient(): void
    {
        try {
            $this->validator = new TestBearerValidator(
                $this->storage,
                new SimpleClientDb(),
                new DummySigner(),
                new DateTimeImmutable('2016-01-01')
            );

            $providedAccessToken = Base64UrlSafe::encodeUnpadded(
                Json::encode(
                    [
                        'type' => 'access_token',
                        'token_id' => 'foo',
                        'auth_key' => 'random_1',
                        'user_id' => 'foo',
                        'client_id' => 'code-client',
                        'scope' => 'config',
                        'expires_at' => '2016-01-01T01:00:00+00:00',
                        'authorization_expires_at' => '2016-03-31T00:00:00+00:00',
                    ]
                )
            );

            $this->validator->validate(
                new Request(
                    [
                        'HTTP_AUTHORIZATION' => 'Bearer ' . $providedAccessToken,
                    ],
                    [],
                    []
                )
            );
            static::fail();
        } catch (InvalidTokenException $e) {
            static::assertSame('client "code-client" no longer registered', $e->getDescription());
        }
    }

    public function testInvalidSyntax(): void
    {
        try {
            $this->validator->validate(
                new Request(
                    [
                        'HTTP_AUTHORIZATION' => 'Bearer %%%%',
                    ],
                    [],
                    []
                )
            );
            static::fail();
        } catch (InvalidTokenException $e) {
            static::assertSame('invalid token', $e->getDescription());
        }
    }

    public function testExpiredToken(): void
    {
        $providedAccessToken = Base64UrlSafe::encodeUnpadded(
            Json::encode(
                [
                    'type' => 'access_token',
                    'token_id' => 'foo',
                    'auth_key' => 'random_1',
                    'user_id' => 'foo',
                    'client_id' => 'code-client',
                    'scope' => 'config',
                    'expires_at' => '2015-01-01T01:00:00+00:00',
                    'authorization_expires_at' => '2016-03-31T00:00:00+00:00',
                ]
            )
        );

        try {
            $this->validator->validate(
                new Request(
                    [
                        'HTTP_AUTHORIZATION' => 'Bearer ' . $providedAccessToken,
                    ],
                    [],
                    []
                )
            );
            static::fail();
        } catch (InvalidTokenException $e) {
            static::assertSame('"access_token" expired', $e->getDescription());
        }
    }

    public function testBasicAuthentication(): void
    {
        try {
            $this->validator->validate(
                new Request(
                    [
                        'HTTP_AUTHORIZATION' => 'Basic AAA==',
                    ],
                    [],
                    []
                )
            );
            static::fail();
        } catch (InvalidTokenException $e) {
            static::assertNull($e->getDescription());
        }
    }

    public function testCodeAsAccessToken(): void
    {
        try {
            $providedAccessToken = Base64UrlSafe::encodeUnpadded(
                Json::encode(
                    [
                        'type' => 'authorization_code',
                        'auth_key' => 'random_1',
                        'user_id' => 'foo',
                        'client_id' => 'code-client',
                        'scope' => 'config',
                        'redirect_uri' => 'http://example.org/code-cb',
                        'code_challenge' => 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
                        'expires_at' => '2016-01-01T00:05:00+00:00',
                    ]
                )
            );

            $this->validator->validate(
                new Request(
                    [
                        'HTTP_AUTHORIZATION' => 'Bearer ' . $providedAccessToken,
                    ],
                    [],
                    []
                )
            );
            static::fail();
        } catch (InvalidTokenException $e) {
            static::assertSame('"type" MUST be "access_token"', $e->getDescription());
        }
    }
}
