<?php

declare(strict_types=1);

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * The media endpoint.
 *
 * It held up under manual probing but had no test, which for a path-handling
 * endpoint is the wrong way round: traversal defences are exactly the thing that
 * breaks silently during a refactor.
 */
final class MediaTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        Storage::fake('local');
        config()->set('filesystems.default', 'local');
    }

    #[Test]
    public function it_serves_a_file_from_an_allowed_prefix(): void
    {
        Storage::disk('local')->put('avatars/portrait.png', 'image-bytes');

        $this->get('/api/v1/media/avatars/portrait.png')->assertOk();
    }

    /**
     * Verification evidence shares the bucket and must be unreachable here,
     * whoever is asking.
     */
    #[Test]
    public function it_refuses_a_prefix_that_is_not_allowed(): void
    {
        Storage::disk('local')->put('verification-evidence/passport.jpg', 'PASSPORT-SECRET');

        $this->get('/api/v1/media/verification-evidence/passport.jpg')->assertNotFound();
    }

    #[Test]
    #[DataProvider('traversalPayloads')]
    public function it_refuses_traversal(string $path): void
    {
        Storage::disk('local')->put('verification-evidence/passport.jpg', 'PASSPORT-SECRET');
        Storage::disk('local')->put('avatars/portrait.png', 'image-bytes');

        $response = $this->get('/api/v1/media/'.$path);

        $this->assertContains($response->getStatusCode(), [400, 404], sprintf(
            'Traversal payload [%s] returned %d.',
            $path,
            $response->getStatusCode(),
        ));
        $this->assertStringNotContainsString('PASSPORT-SECRET', $response->getContent() ?: '');
    }

    /** @return iterable<string, array{string}> */
    public static function traversalPayloads(): iterable
    {
        yield 'parent escape' => ['../verification-evidence/passport.jpg'];
        yield 'escape from allowed prefix' => ['avatars/../verification-evidence/passport.jpg'];
        yield 'encoded separators' => ['avatars%2F..%2Fverification-evidence%2Fpassport.jpg'];
        yield 'double-dot-slash bypass' => ['avatars/....//verification-evidence/passport.jpg'];
        yield 'reach for env' => ['avatars/../../../.env'];
    }

    #[Test]
    public function a_missing_file_is_not_found(): void
    {
        $this->get('/api/v1/media/avatars/nothing-here.png')->assertNotFound();
    }

    /**
     * Uploads are user-controlled bytes, so the response must forbid sniffing
     * and script execution.
     */
    #[Test]
    public function it_sends_hardening_headers(): void
    {
        Storage::disk('local')->put('avatars/portrait.png', 'image-bytes');

        $this->get('/api/v1/media/avatars/portrait.png')
            ->assertOk()
            ->assertHeader('X-Content-Type-Options', 'nosniff')
            ->assertHeader('Content-Security-Policy', "default-src 'none'; sandbox");
    }

    #[Test]
    public function the_listings_prefix_is_not_served_until_something_writes_to_it(): void
    {
        Storage::disk('local')->put('listings/screenshot.png', 'bytes');

        $this->get('/api/v1/media/listings/screenshot.png')->assertNotFound();
    }
}
