<?php

declare(strict_types=1);

namespace HyperfTest\Cases\Unit\Model;

use App\Model\ApiKey;
use App\Model\User;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @coversNothing
 */
class ApiKeyTest extends TestCase
{
    protected function runInCoroutine(callable $callback): void
    {
        if (\Swoole\Coroutine::getCid() > 0) {
            $callback();
            return;
        }

        $exception = null;
        \Swoole\Coroutine\run(static function () use ($callback, &$exception): void {
            try {
                $callback();
            } catch (\Throwable $e) {
                $exception = $e;
            }
        });
        if ($exception) {
            throw $exception;
        }
    }

    protected function createTestUser(): User
    {
        $suffix = bin2hex(random_bytes(4));

        return User::query()->create([
            'username' => 'apikey_test_' . $suffix,
            'password' => 'Pass_' . $suffix,
            'email' => 'apikey_test_' . $suffix . '@example.com',
            'status' => 1,
            'api_key_enabled' => true,
        ]);
    }

    public function test_generate_returns_plain_key_with_user_id_prefix(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Test Key');

            $this->assertArrayHasKey('plain_key', $result);
            $this->assertArrayHasKey('api_key', $result);
            $this->assertInstanceOf(ApiKey::class, $result['api_key']);

            // 格式为 {user_id}#{64hex}
            $this->assertStringContainsString('#', $result['plain_key']);
            [$prefix, $token] = explode('#', $result['plain_key'], 2);
            $this->assertSame((string) $user->id, $prefix);
            $this->assertSame(64, strlen($token));
            $this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $token);
        });
    }

    public function test_generate_stores_token_only_hash(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Hash Test');

            // key_hash 是 token 部分的 SHA-256，不含 user_id 前缀
            $token = explode('#', $result['plain_key'], 2)[1];
            $expected_hash = hash('sha256', $token);
            $this->assertSame($expected_hash, $result['api_key']->key_hash);
        });
    }

    public function test_generate_key_prefix_from_token_part(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Prefix Test');

            // key_prefix 取 token 前 8 位（不是 plain_key 前 8 位）
            $token = explode('#', $result['plain_key'], 2)[1];
            $expected_prefix = substr($token, 0, 8);
            $this->assertSame($expected_prefix, $result['api_key']->key_prefix);
        });
    }

    public function test_find_by_plain_key_returns_valid_key(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Find Test');

            $found = ApiKey::findByPlainKey($result['plain_key']);
            $this->assertNotNull($found);
            $this->assertSame($result['api_key']->id, $found->id);
        });
    }

    public function test_find_by_plain_key_new_format(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'New Format Test');

            // 新格式 {user_id}#{token} 能正确查找
            $found = ApiKey::findByPlainKey($result['plain_key']);
            $this->assertNotNull($found);
            $this->assertSame($result['api_key']->id, $found->id);
            $this->assertSame($user->id, $found->user_id);
        });
    }

    public function test_find_by_plain_key_rejects_old_format(): void
    {
        $this->runInCoroutine(function (): void {
            // 旧格式（纯 hex，不含 #）直接返回 null
            $found = ApiKey::findByPlainKey(bin2hex(random_bytes(32)));
            $this->assertNull($found);
        });
    }

    public function test_find_by_plain_key_returns_null_for_invalid_key(): void
    {
        $this->runInCoroutine(function (): void {
            $found = ApiKey::findByPlainKey('999#invalid_token_that_does_not_exist');
            $this->assertNull($found);
        });
    }

    public function test_find_by_plain_key_returns_disabled_key(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Disabled Test');

            $result['api_key']->enabled = false;
            $result['api_key']->save();

            $found = ApiKey::findByPlainKey($result['plain_key']);
            $this->assertNotNull($found);
            $this->assertFalse($found->enabled);
        });
    }

    public function test_find_by_plain_key_excludes_expired_key(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Expired Test', \Carbon\Carbon::now()->subDay()->toDateTimeString());

            $found = ApiKey::findByPlainKey($result['plain_key']);
            $this->assertNull($found);
        });
    }

    public function test_key_hash_is_hidden_in_json(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Hidden Test');

            $json = $result['api_key']->toArray();
            $this->assertArrayNotHasKey('key_hash', $json);
        });
    }

    public function test_is_valid_returns_true_for_active_key(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Valid Test');

            $this->assertTrue($result['api_key']->isValid());
        });
    }

    public function test_is_valid_returns_false_for_disabled_key(): void
    {
        $this->runInCoroutine(function (): void {
            $user = $this->createTestUser();
            $result = ApiKey::generate($user->id, 'Disabled Valid');

            $result['api_key']->enabled = false;
            $this->assertFalse($result['api_key']->isValid());
        });
    }
}
