<?php

declare(strict_types=1);

namespace Apexl\Tests\User\FeatureTests;

use apexl\Io\enums\HttpMethod;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\enums\permissions\Common;
use apexl\Io\TestInfrastructure\FeatureTest;
use Apexl\Tests\User\Helpers\EnrolsTwoFactor;
use Apexl\Tests\User\Helpers\ManagesTwoFactorConfig;
use PHPUnit\Framework\Attributes\Test;

/**
 * End-to-end HTTP tests through the real middleware stack (Session -> TwoFactor
 * -> Permission), proving the enforcement, the login-response contract, the
 * blocked-response contract, and the off-mode compatibility guarantee.
 */
final class TwoFactorHttpFlowTest extends FeatureTest
{
    use ManagesTwoFactorConfig;
    use EnrolsTwoFactor;

    // '/authenticated' carries no permission guard (so PermissionMiddleware
    // never blocks) and is NOT on the 2FA ignore-list, so it isolates 2FA
    // enforcement as the only possible gate.
    private const string PROTECTED_URI = '/authenticated';
    private const string PASSWORD = 'Sup3r-Secret-Pass!';

    protected function tearDown(): void
    {
        $this->restoreTwoFactorConfig();
        parent::tearDown();
    }

    private function privilegedUser(): userEntity
    {
        return $this->makeUser(['password' => self::PASSWORD]);
    }

    private function login(userEntity $user): array
    {
        return $this->post('/api/v1/user/action/login', [
            'username' => $user->email,
            'password' => self::PASSWORD,
        ]);
    }

    private function bearer(string $token): array
    {
        return ['Authorization' => 'Bearer ' . $token];
    }

    #[Test]
    public function offModeIsAByteForByteNoOp(): void
    {
        $this->setTwoFactorMode('off');
        $user = $this->privilegedUser();

        $login = $this->login($user);
        $this->assertTrue($login['success']);
        $this->assertArrayNotHasKey('two_factor_required', $login);

        // A protected route is reachable with just the access token — no gate.
        $this->executeRequest(
            HttpMethod::GET,
            self::PROTECTED_URI,
            headers: $this->bearer($login['access_token']),
            expectCode: 200,
        );
    }

    #[Test]
    public function optionalUnenrolledUserIsNotGated(): void
    {
        $this->setTwoFactorMode('optional');
        $user = $this->privilegedUser();

        $login = $this->login($user);
        $this->assertArrayNotHasKey('two_factor_required', $login);

        $this->executeRequest(
            HttpMethod::GET,
            self::PROTECTED_URI,
            headers: $this->bearer($login['access_token']),
            expectCode: 200,
        );
    }

    #[Test]
    public function enrolledUserIsChallengedThenPassesAfterAValidCode(): void
    {
        $this->setTwoFactorMode('optional');
        $user = $this->privilegedUser();
        $secret = $this->enrolUser($user);

        // Login still succeeds (session + JWT minted) but flags the challenge.
        $login = $this->login($user);
        $this->assertTrue($login['success']);
        $this->assertTrue($login['two_factor_required']);
        $this->assertSame('challenge', $login['two_factor_status']);

        $token = $login['access_token'];

        // The protected route is blocked with the challenge contract...
        $blocked = $this->executeRequest(
            HttpMethod::GET,
            self::PROTECTED_URI,
            headers: $this->bearer($token),
            expectCode: 403,
        );
        $this->assertTrue($blocked['two_factor_required']);
        $this->assertSame('challenge', $blocked['two_factor_status']);

        // ...a wrong code is rejected...
        $this->executeRequest(
            HttpMethod::POST,
            '/api/v1/user/action/two-factor-auth/authenticate',
            body: ['code' => '000000'],
            headers: $this->bearer($token),
            expectCode: 422,
        );

        // ...still blocked...
        $this->executeRequest(
            HttpMethod::GET,
            self::PROTECTED_URI,
            headers: $this->bearer($token),
            expectCode: 403,
        );

        // ...a valid code passes the session...
        $passed = $this->executeRequest(
            HttpMethod::POST,
            '/api/v1/user/action/two-factor-auth/authenticate',
            body: ['code' => $this->totpCodeFor($secret)],
            headers: $this->bearer($token),
            expectCode: 200,
        );
        $this->assertTrue($passed['two_factor_passed']);

        // ...and the protected route is now reachable on the same session.
        $this->executeRequest(
            HttpMethod::GET,
            self::PROTECTED_URI,
            headers: $this->bearer($token),
            expectCode: 200,
        );
    }

    #[Test]
    public function enforcedUnenrolledUserIsForcedToSetUpButCanReachSetupRoute(): void
    {
        $this->setTwoFactorConfig([
            'auth.two_factor.mode' => 'enforced',
            'auth.two_factor.grace_period_days' => 0,
        ]);
        $user = $this->privilegedUser();

        $login = $this->login($user);
        $this->assertSame('setup_required', $login['two_factor_status']);
        $token = $login['access_token'];

        $blocked = $this->executeRequest(
            HttpMethod::GET,
            self::PROTECTED_URI,
            headers: $this->bearer($token),
            expectCode: 403,
        );
        $this->assertSame('setup_required', $blocked['two_factor_status']);

        // The setup action route stays reachable while gated (ignore-list).
        $setup = $this->executeRequest(
            HttpMethod::POST,
            '/api/v1/user/action/two-factor-auth/setup',
            headers: $this->bearer($token),
            expectCode: 200,
        );
        $this->assertArrayHasKey('provisioning_uri', $setup);
        $this->assertArrayHasKey('qr_code', $setup);
    }
}
