<?php

declare(strict_types=1);

namespace CpCreation\AltchaBundle\Tests\PublicApi;

use CpCreation\AltchaBundle\Controller\ChallengeController;
use CpCreation\AltchaBundle\Model\ScopePolicy;
use CpCreation\AltchaBundle\Scope\ScopeRegistry;
use CpCreation\AltchaBundle\Security\ProtectionEnvelopeManager;
use CpCreation\AltchaBundle\Tests\Double\FakeAltchaServiceClient;
use CpCreation\AltchaBundle\Tests\Double\LocalRateLimiterFactory;
use CpCreation\AltchaBundle\Tests\Double\MutableTimeProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;

final class ChallengeControllerTest extends TestCase
{
    public function testSameOriginEndpointReturnsANoStoreChallengeForADeclaredEnabledScope(): void
    {
        $client = new FakeAltchaServiceClient();
        $client->policy = new ScopePolicy(
            scope: 'custom.review', enabled: true, version: 5, altchaEnabled: true, profile: 'pbkdf2-default',
            minimumFillTimeMs: 500, maximumEnvelopeAgeSeconds: 900,
            subjectLimitsEnabled: false,
        );
        $manager = new ProtectionEnvelopeManager('controller-test-secret-with-enough-entropy', new MutableTimeProvider(), 900);
        $controller = new ChallengeController(
            $client,
            new ScopeRegistry(['custom.review' => 'Review']),
            $manager,
            LocalRateLimiterFactory::create(),
        );
        $formToken = $manager->issue('custom.review', 'example.test')->token;
        $request = Request::create('https://example.test/_altcha/challenge/custom.review?formToken='.urlencode($formToken));
        $request->server->set('REMOTE_ADDR', '203.0.113.4');

        $response = $controller($request, 'custom.review');

        self::assertSame(200, $response->getStatusCode());
        self::assertStringContainsString('no-store', (string) $response->headers->get('Cache-Control'));
        self::assertStringContainsString('max-age=0', (string) $response->headers->get('Cache-Control'));
        self::assertSame('PBKDF2', json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR)['parameters']['algorithm']);
        self::assertSame($formToken, $client->challengeRequests[0]['formToken']);
        self::assertSame(5, $client->challengeRequests[0]['policyVersion']);
    }

    public function testUndeclaredAndDisabledScopesAreNotExposed(): void
    {
        $client = new FakeAltchaServiceClient();
        $manager = new ProtectionEnvelopeManager('controller-test-secret-with-enough-entropy', new MutableTimeProvider(), 900);
        $controller = new ChallengeController(
            $client,
            new ScopeRegistry(['site.contact' => 'Contact']),
            $manager,
            LocalRateLimiterFactory::create(),
        );

        self::assertSame(404, $controller(Request::create('https://example.test'), 'custom.unknown')->getStatusCode());

        $client->policy = new ScopePolicy(
            scope: 'site.contact', enabled: false, version: 5, altchaEnabled: false, profile: null,
            minimumFillTimeMs: 0, maximumEnvelopeAgeSeconds: 900,
            subjectLimitsEnabled: false,
        );
        $formToken = $manager->issue('site.contact', 'example.test')->token;
        self::assertSame(404, $controller(Request::create('https://example.test?formToken='.urlencode($formToken)), 'site.contact')->getStatusCode());
    }

    public function testMissingOrTamperedFormTokenIsRejectedBeforeChallengeCreation(): void
    {
        $client = new FakeAltchaServiceClient();
        $manager = new ProtectionEnvelopeManager('controller-test-secret-with-enough-entropy', new MutableTimeProvider(), 900);
        $controller = new ChallengeController(
            $client,
            new ScopeRegistry(['site.contact' => 'Contact']),
            $manager,
            LocalRateLimiterFactory::create(),
        );

        self::assertSame(400, $controller(Request::create('https://example.test'), 'site.contact')->getStatusCode());
        self::assertSame(400, $controller(Request::create('https://example.test?formToken=tampered'), 'site.contact')->getStatusCode());
        self::assertSame([], $client->challengeRequests);
    }

    public function testLocalChallengeQuotaReturnsRetryAfterBeforeCallingDockerAgain(): void
    {
        $client = new FakeAltchaServiceClient();
        $manager = new ProtectionEnvelopeManager('controller-test-secret-with-enough-entropy', new MutableTimeProvider(), 900);
        $controller = new ChallengeController(
            $client,
            new ScopeRegistry(['site.contact' => 'Contact']),
            $manager,
            LocalRateLimiterFactory::create(challengeLimit: 1),
        );
        $formToken = $manager->issue('site.contact', 'example.test')->token;
        $request = Request::create('https://example.test?formToken='.urlencode($formToken));
        $request->server->set('REMOTE_ADDR', '203.0.113.55');

        self::assertSame(200, $controller($request, 'site.contact')->getStatusCode());
        $response = $controller($request, 'site.contact');

        self::assertSame(429, $response->getStatusCode());
        self::assertGreaterThanOrEqual(1, (int) $response->headers->get('Retry-After'));
        self::assertCount(1, $client->policyRequests, 'The second request must be stopped before Docker.');
        self::assertCount(1, $client->challengeRequests);
    }
}
