<?php

declare(strict_types=1);

namespace App\Tests\Unit\App;

use App\App\Config;
use PHPUnit\Framework\TestCase;

/**
 * Boot-time configuration validation.
 *
 * SEC_REVIEW F37: `LOCAL_ADMIN_PASSWORD_HASH` must be Argon2id or bcrypt
 * cost ≥ 12. Anything weaker — bcrypt cost 4, plain text, md5-crypt,
 * argon2i — is rejected at boot so a misconfigured deployment crashes
 * on `docker compose up` instead of accepting hashed-but-cracked
 * passwords on the local-admin path.
 */
final class ConfigTest extends TestCase
{
    public function testValidArgon2idIsAccepted(): void
    {
        $hash = password_hash('test', PASSWORD_ARGON2ID);
        self::assertSame([], Config::collectErrors($this->localBaseSettings($hash)));
    }

    public function testValidBcryptAtMinimumCostIsAccepted(): void
    {
        $hash = password_hash('test', PASSWORD_BCRYPT, ['cost' => Config::BCRYPT_MIN_COST]);
        self::assertSame([], Config::collectErrors($this->localBaseSettings($hash)));
    }

    public function testBcryptBelowMinimumCostIsRejected(): void
    {
        $hash = password_hash('test', PASSWORD_BCRYPT, ['cost' => 4]);
        $errors = Config::collectErrors($this->localBaseSettings($hash));
        self::assertNotEmpty($errors);
        $joined = implode("\n", $errors);
        self::assertStringContainsString('cost=4', $joined);
        self::assertStringContainsString('LOCAL_ADMIN_PASSWORD_HASH', $joined);
    }

    public function testArgon2iIsRejected(): void
    {
        if (!defined('PASSWORD_ARGON2I')) {
            self::markTestSkipped('argon2i not built into this PHP');
        }
        $hash = password_hash('test', PASSWORD_ARGON2I);
        $errors = Config::collectErrors($this->localBaseSettings($hash));
        self::assertNotEmpty($errors);
        self::assertStringContainsString('argon2i', implode("\n", $errors));
    }

    public function testMd5CryptIsRejected(): void
    {
        // crypt() with $1$ prefix produces md5-crypt, which password_get_info
        // reports as 'unknown'. The literal string SEC_REVIEW called out.
        $hash = '$1$salt1234$KdyIvFMZJKR1qDJ5qE5W31';
        $errors = Config::collectErrors($this->localBaseSettings($hash));
        self::assertNotEmpty($errors);
        self::assertStringContainsString('"unknown"', implode("\n", $errors));
    }

    public function testPlainStringIsRejected(): void
    {
        $errors = Config::collectErrors($this->localBaseSettings('not-a-hash-at-all'));
        self::assertNotEmpty($errors);
    }

    public function testEmptyHashIsStillRejectedWhenLocalEnabled(): void
    {
        $errors = Config::collectErrors($this->localBaseSettings(''));
        self::assertNotEmpty($errors);
        self::assertStringContainsString('empty', implode("\n", $errors));
    }

    public function testHashIsNotValidatedWhenLocalAdminDisabled(): void
    {
        // Operators who only run OIDC don't need a strong dummy hash.
        $settings = [
            'ui_service_token' => 'irdb_svc_AAAA',
            'api_base_url' => 'http://api:8081',
            'oidc_enabled' => true,
            'oidc_issuer' => 'https://issuer',
            'oidc_client_id' => 'cid',
            'oidc_client_secret' => 'csec',
            'oidc_redirect_uri' => 'https://r/cb',
            'local_admin_enabled' => false,
            'local_admin_username' => '',
            'local_admin_password_hash' => 'this-would-fail-if-checked',
        ];
        self::assertSame([], Config::collectErrors($settings));
    }

    /**
     * @return array<string, mixed>
     */
    private function localBaseSettings(string $hash): array
    {
        return [
            'ui_service_token' => 'irdb_svc_AAAA',
            'api_base_url' => 'http://api:8081',
            'oidc_enabled' => false,
            'local_admin_enabled' => true,
            'local_admin_username' => 'admin',
            'local_admin_password_hash' => $hash,
        ];
    }
}
