<?php

declare(strict_types=1);

namespace App\Tests\Integration\Public;

use App\Tests\Integration\Support\AppTestCase;

/**
 * SEC_REVIEW F58 — `/api/docs` loads the RapiDoc viewer from jsDelivr.
 * The `<script>` tag must carry an SRI `integrity` hash and
 * `crossorigin="anonymous"` so the browser refuses to execute the JS
 * if a CDN compromise or in-flight content modification serves
 * different bytes.
 *
 * Also covers SEC_REVIEW F68 — the docs routes are gated behind
 * `API_DOCS_PUBLIC` (default off). Tests that reach the routes
 * enable the flag and rebuild the app; the dedicated F68 cases
 * leave it off and assert 404.
 */
final class DocsControllerTest extends AppTestCase
{
    public function testDocsPageEmbedsRapiDocWithSriIntegrity(): void
    {
        $this->enableDocs();

        $resp = $this->request('GET', '/api/docs');
        self::assertSame(200, $resp->getStatusCode());
        self::assertStringContainsString('text/html', $resp->getHeaderLine('Content-Type'));

        $html = (string) $resp->getBody();
        // Script tag points at the locked RapiDoc version.
        self::assertStringContainsString(
            'https://cdn.jsdelivr.net/npm/rapidoc@9.3.4/dist/rapidoc-min.js',
            $html,
        );
        // Integrity hash present and well-formed (sha384- + base64).
        self::assertMatchesRegularExpression(
            '/integrity="sha384-[A-Za-z0-9+\/=]{64}"/',
            $html,
            'expected sha384 SRI on the rapidoc <script> tag',
        );
        // crossorigin=anonymous required alongside SRI for cross-origin scripts.
        self::assertStringContainsString('crossorigin="anonymous"', $html);
    }

    public function testOpenapiSpecIsServed(): void
    {
        // Sanity check that we didn't break the spec endpoint while
        // editing the controller.
        $this->enableDocs();
        $resp = $this->request('GET', '/api/v1/openapi.yaml');
        // The spec file isn't shipped in tests; either 200 with content
        // or 500 with `spec_unavailable`. Both are acceptable here —
        // we just want to prove the route still wires through the
        // edited controller.
        self::assertContains($resp->getStatusCode(), [200, 500]);
    }

    public function testDocsPageIs404ByDefault(): void
    {
        // SEC_REVIEW F68: `/api/docs` is gated behind
        // `API_DOCS_PUBLIC`. AppTestCase builds the container with
        // the env var unset, so the default (false) applies and the
        // route is never registered → Slim returns 404.
        $resp = $this->request('GET', '/api/docs');
        self::assertSame(404, $resp->getStatusCode());
    }

    public function testOpenapiSpecIs404ByDefault(): void
    {
        $resp = $this->request('GET', '/api/v1/openapi.yaml');
        self::assertSame(404, $resp->getStatusCode());
    }

    /**
     * Re-bind `settings.api_docs_public` to `true` and rebuild the app
     * so the docs routes are registered. Same pattern
     * `JobsAdminControllerTest::testRefreshGeoip412UnderMaxmindWithoutKey`
     * uses to swap a binding mid-test.
     */
    private function enableDocs(): void
    {
        if (method_exists($this->container, 'set')) {
            /** @var \DI\Container $c */
            $c = $this->container;
            $c->set('settings.api_docs_public', true);
            $this->app = \App\App\AppFactory::build($this->container);
        }
    }
}
