<?php

declare(strict_types=1);

/**
 * This file is part of CodeIgniter Shield.
 *
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Tests\Authentication;

use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\AccessToken;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Models\UserIdentityModel;
use CodeIgniter\Shield\Models\UserModel;
use Tests\Support\DatabaseTestCase;

/**
 * @internal
 */
final class HasHmacTokensTest extends DatabaseTestCase
{
    private User $user;

    protected function setUp(): void
    {
        parent::setUp();

        $this->user = fake(UserModel::class);
        $this->db->table($this->tables['identities'])->truncate();
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        // Reset the current time.
        Time::setTestNow();
    }

    public function testGenerateHmacToken(): void
    {
        $token = $this->user->generateHmacToken('foo');

        $this->assertSame('foo', $token->name);
        $this->assertNull($token->expires);

        $this->assertIsString($token->secret);
        $this->assertIsString($token->secret2);
        $this->assertIsString($token->rawSecretKey);

        // All scopes are assigned by default via wildcard
        $this->assertSame(['*'], $token->scopes);
    }

    public function testHmacTokens(): void
    {
        // Should return empty array when none exist
        $this->assertSame([], $this->user->accessTokens());

        // Give the user a couple of access tokens
        $token1 = fake(
            UserIdentityModel::class,
            ['user_id' => $this->user->id, 'type' => 'hmac_sha256', 'secret' => 'key1', 'secret2' => 'd862cd9ddc23e960ca6d45a3e0b64c7509f0c0ef0e5f7b64be8910a6a714c89b83fab95251bbf17f6c84b42c26cf460a28ea969591dc64b1f5c4b323f47615d2e8cbe4c62118001d3274e0f25850b0ac2617bc43119af22c99a1a83072002267177da01f9f37225435e1914be004f4d35a49869b737ed10ab232c1ed1048bb9'],
        );

        $token2 = fake(
            UserIdentityModel::class,
            ['user_id' => $this->user->id, 'type' => 'hmac_sha256', 'secret' => 'key2', 'secret2' => 'd862cd9ddc23e960ca6d45a3e0b64c7509f0c0ef0e5f7b64be8910a6a714c89b83fab95251bbf17f6c84b42c26cf460a28ea969591dc64b1f5c4b323f47615d2e8cbe4c62118001d3274e0f25850b0ac2617bc43119af22c99a1a83072002267177da01f9f37225435e1914be004f4d35a49869b737ed10ab232c1ed1048bb9'],
        );

        $tokens = $this->user->hmacTokens();

        $this->assertCount(2, $tokens);
        $this->assertSame($token1->id, $tokens[0]->id);
        $this->assertSame($token1->secret, $tokens[0]->secret); // Key
        $this->assertSame($token1->secret2, $tokens[0]->secret2); // Secret Key
        $this->assertSame($token2->id, $tokens[1]->id);
        $this->assertSame($token2->secret, $tokens[1]->secret);
        $this->assertSame($token2->secret2, $tokens[1]->secret2);
    }

    public function testGetHmacToken(): void
    {
        // Should return null when not found
        $this->assertNull($this->user->getHmacToken('foo'));

        $token = $this->user->generateHmacToken('foo');

        $found = $this->user->getHmacToken($token->secret);

        $this->assertInstanceOf(AccessToken::class, $found);
        $this->assertSame($token->id, $found->id);
        $this->assertSame($token->secret, $found->secret); // Key
        $this->assertSame($token->secret2, $found->secret2); // Secret Key
    }

    public function testGetHmacTokenById(): void
    {
        // Should return null when not found
        $this->assertNull($this->user->getHmacTokenById(123));

        $token = $this->user->generateHmacToken('foo');
        $found = $this->user->getHmacTokenById($token->id);

        $this->assertInstanceOf(AccessToken::class, $found);
        $this->assertSame($token->id, $found->id);
        $this->assertSame($token->secret, $found->secret); // Key
        $this->assertSame($token->secret2, $found->secret2); // Secret Key
    }

    public function testRevokeHmacToken(): void
    {
        $token = $this->user->generateHmacToken('foo');

        $this->assertCount(1, $this->user->hmacTokens());

        $this->user->revokeHmacToken($token->secret);

        $this->assertCount(0, $this->user->hmacTokens());
    }

    public function testRevokeAllHmacTokens(): void
    {
        $this->user->generateHmacToken('foo');
        $this->user->generateHmacToken('foo');

        $this->assertCount(2, $this->user->hmacTokens());

        $this->user->revokeAllHmacTokens();

        $this->assertCount(0, $this->user->hmacTokens());
    }

    public function testHmacTokenCanNoTokenSet(): void
    {
        $this->assertFalse($this->user->hmacTokenCan('foo'));
    }

    public function testHmacTokenCanBasics(): void
    {
        $token = $this->user->generateHmacToken('foo', ['foo.bar']);
        $this->user->setHmacToken($token);

        $this->assertTrue($this->user->hmacTokenCan('foo.bar'));
        $this->assertFalse($this->user->hmacTokenCan('foo.baz'));
    }

    public function testHmacTokenCantNoTokenSet(): void
    {
        $this->assertTrue($this->user->hmacTokenCant('foo'));
    }

    public function testHmacTokenCant(): void
    {
        $token = $this->user->generateHmacToken('foo', ['foo.bar']);
        $this->user->setHmacToken($token);

        $this->assertFalse($this->user->hmacTokenCant('foo.bar'));
        $this->assertTrue($this->user->hmacTokenCant('foo.baz'));
    }

    /**
     * See https://github.com/codeigniter4/shield/issues/926
     */
    public function testGenerateTokenWithExpiration(): void
    {
        $tokenExpiration = Time::parse('2024-11-03 12:00:00');

        $token = $this->user->generateHmacToken('foo', ['foo.bar'], $tokenExpiration);
        $this->user->setHmacToken($token);

        $this->assertSame($tokenExpiration->format('Y-m-d h:i:s'), $this->user->currentHmacToken()->expires->format('Y-m-d h:i:s'));

        $tokenExpiration = $tokenExpiration->addMonths(1)->addYears(1);

        $token = $this->user->generateHmacToken('foo', ['foo.bar'], $tokenExpiration);
        $this->user->setHmacToken($token);

        $this->assertSame($tokenExpiration->format('Y-m-d h:i:s'), $this->user->currentHmacToken()->expires->format('Y-m-d h:i:s'));
    }

    /**
     * See https://github.com/codeigniter4/shield/issues/926
     */
    public function testSetTokenExpirationById(): void
    {
        $token = $this->user->generateHmacToken('foo', ['foo.bar']);

        $this->user->setHmacToken($token);

        $this->assertNull($this->user->currentHmacToken()->expires);

        $tokenExpiration = Time::parse('2024-11-03 12:00:00');

        $this->assertTrue($this->user->updateHmacTokenExpiration($token->id, $tokenExpiration));

        $this->user->setHmacToken($this->user->getHmacTokenById($token->id));
        $this->assertSame($tokenExpiration->format('Y-m-d h:i:s'), $this->user->currentHmacToken()->expires->format('Y-m-d h:i:s'));
    }

    /**
     * See https://github.com/codeigniter4/shield/issues/926
     */
    public function testIsHmacTokenExpired(): void
    {
        $tokenExpiration = Time::parse('2024-11-03 12:00:00');

        $token = $this->user->generateHmacToken('foo', ['foo.bar'], $tokenExpiration);
        $this->user->setHmacToken($token);

        $this->assertTrue($this->user->isHmacTokenExpired($token));
    }

    /**
     * See https://github.com/codeigniter4/shield/issues/926
     */
    public function testHmacTokenTimeToExpired(): void
    {
        Time::setTestNow('2025-07-16 12:00:00');

        $tokenExpiration = Time::now();
        $tokenExpiration = $tokenExpiration->addYears(1);

        $token = $this->user->generateHmacToken('foo', ['foo.bar'], $tokenExpiration);

        $this->assertSame('in 1 year', $token->expires->humanize());
    }

    /**
     * See https://github.com/codeigniter4/shield/issues/926
     */
    public function testcanHmacTokenExpire(): void
    {
        $tokenExpiration = Time::now();
        $tokenExpiration = $tokenExpiration->addYears(1);

        $token = $this->user->generateHmacToken('foo', ['foo.bar'], $tokenExpiration);

        $this->assertTrue($this->user->canHmacTokenExpire($token));

        $token = $this->user->generateHmacToken('foo', ['foo.bar']);

        $this->assertFalse($this->user->canHmacTokenExpire($token));
    }

    /**
     * See https://github.com/codeigniter4/shield/issues/926
     */
    public function testHmacTokenRemoveExpiration(): void
    {
        $tokenExpiration = Time::now()->addYears(1);

        $token = $this->user->generateHmacToken('hmac', ['foo.bar'], $tokenExpiration);

        $this->user->setHmacToken($token);

        $this->assertTrue($this->user->canHmacTokenExpire($token));

        $this->assertTrue($this->user->removeHmacTokenExpiration($token->id));

        $this->assertFalse($this->user->canHmacTokenExpire($this->user->currentHmacToken()));
    }
}
