<?php

namespace Tests\Feature;

use App\Models\PromptLandingPageAttachment;
use App\Services\PromptLandingPageHtmlSanitizer;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;

/**
 * An uploaded filename is not a search-and-replace over the whole page.
 *
 * The attachment stripper ran `str_replace($token, '', $html)` for every token,
 * and one of those tokens is `original_name` - straight from
 * `$file->getClientOriginalName()` at PromptLandingPageController:711. So whoever
 * uploaded the file chose the needle, and it was applied to the page, its
 * stylesheet and its JavaScript.
 *
 * A file called `a` removed every letter a from all three. `div` unpicked the
 * markup. `>` would have left no markup at all. Nothing validated the name,
 * because nothing expected it to be used this way.
 *
 * What the stripper is for is references - a path or filename appearing in a src,
 * an href or a CSS url() - so that is where it looks now. A token with a slash in
 * it cannot collide with prose and is still removed everywhere; a filename short
 * enough to be a coincidence is only removed where it is being used as a file.
 */
class AFilenameIsNotASearchAndReplaceTest extends TestCase
{
    /**
     * A page that references none of the attachment's files.
     *
     * Deliberately so: the assertion below is byte-for-byte identity, so any
     * genuine reference would make it fail for the right reason and prove
     * nothing. The url() here names `banner-wide.png`, which no test uploads.
     */
    private const PAGE = '<html><head><style>.hero{background:url(/storage/img/banner-wide.png)}</style></head>'
        .'<body><div class="hero"><h1>A handmade advantage</h1>'
        .'<p>Our audience data is a decade deep.</p>'
        .'<a href="/signup">Start a trial</a></div>'
        .'<script>var ready=true;document.addEventListener("DOMContentLoaded",function(){});</script>'
        .'</body></html>';

    private function attachment(string $originalName, string $path = 'prompt-landing-pages/9/abc123def456.png'): PromptLandingPageAttachment
    {
        return new PromptLandingPageAttachment(['path' => $path, 'original_name' => $originalName]);
    }

    private function sanitize(string $html, string $originalName): string
    {
        return app(PromptLandingPageHtmlSanitizer::class)->sanitize($html, [$this->attachment($originalName)]);
    }

    // -------------------------------------------------------------- the defect

    /** @return list<array{0: string}> */
    public static function dangerousNames(): array
    {
        return [
            'one letter' => ['a'],
            'a tag name' => ['div'],
            'a css word' => ['url'],
            'a digit' => ['1'],
            'markup' => ['>'],
            'a quote' => ['"'],
            'a common word' => ['data'],
            'a keyword' => ['var'],
            'an attribute' => ['href'],
        ];
    }

    /**
     * A name the uploader chose does not rewrite the page.
     *
     * Byte-for-byte: this document contains none of the attachment's own paths,
     * so there is nothing here for the stripper to legitimately remove.
     */
    #[DataProvider('dangerousNames')]
    public function test_an_uploaded_name_does_not_shred_the_document(string $name): void
    {
        $this->assertSame(
            self::PAGE,
            $this->sanitize(self::PAGE, $name),
            "a file named [{$name}] rewrote the page",
        );
    }

    /** Spelled out for the case that named the bug. */
    public function test_a_file_named_a_leaves_the_letter_a_alone(): void
    {
        $result = $this->sanitize(self::PAGE, 'a');

        $this->assertStringContainsString('A handmade advantage', $result);
        $this->assertStringContainsString('audience data is a decade deep', $result);
        $this->assertStringContainsString('addEventListener', $result);
    }

    /** And the stylesheet and script are sanitised too, so both must survive. */
    public function test_the_stylesheet_and_script_are_not_shredded_either(): void
    {
        $sanitizer = app(PromptLandingPageHtmlSanitizer::class);
        $attachments = [$this->attachment('e')];

        $this->assertSame('.hero{display:flex;text-align:center}', $sanitizer->sanitize('.hero{display:flex;text-align:center}', $attachments));
        $this->assertSame('var ready=true;', $sanitizer->sanitize('var ready=true;', $attachments));
    }

    // --------------------------------------------- and it still does its job

    /** A reference to the attachment's real path is still removed. */
    public function test_the_attachment_path_is_still_stripped(): void
    {
        $html = '<img src="prompt-landing-pages/9/abc123def456.png" alt="Hero">';

        $result = $this->sanitize($html, 'hero.png');

        $this->assertStringNotContainsString('abc123def456', $result);
        $this->assertStringContainsString('alt="Hero"', $result, 'the whole tag was destroyed');
    }

    /** A src that names the uploaded file is emptied, value and all. */
    public function test_a_src_naming_the_upload_is_emptied(): void
    {
        $result = $this->sanitize('<img src="/uploads/a" alt="Hero">', 'a');

        $this->assertStringContainsString('src=""', $result);
        $this->assertStringContainsString('alt="Hero"', $result);
    }

    /**
     * The whole value goes, not the matched part of it.
     *
     * Cutting the filename out of the middle leaves a request for some other
     * file, which is worse than an empty src.
     */
    public function test_a_reference_is_emptied_rather_than_edited(): void
    {
        $result = $this->sanitize('<img src="/media/2026/hero.png?v=2">', 'hero.png');

        $this->assertStringContainsString('src=""', $result);
        $this->assertStringNotContainsString('/media/2026/', $result);
    }

    /** A CSS url() pointing at it is emptied as well. */
    public function test_a_css_url_naming_the_upload_is_emptied(): void
    {
        $result = app(PromptLandingPageHtmlSanitizer::class)->sanitize(
            '.hero{background:url("/uploads/hero.png");color:red}',
            [$this->attachment('hero.png')],
        );

        $this->assertStringContainsString('url()', $result);
        $this->assertStringContainsString('color:red', $result);
    }

    /** A different file with a similar name is not caught by mistake. */
    public function test_a_similarly_named_file_is_left_alone(): void
    {
        $html = '<img src="/media/other-hero.png"><img src="/media/hero.png.bak">';

        $result = $this->sanitize($html, 'hero.png');

        $this->assertStringContainsString('other-hero.png', $result);
        $this->assertStringContainsString('hero.png.bak', $result);
    }

    /** The static path tokens still go everywhere, because they cannot collide. */
    public function test_the_internal_path_markers_are_still_removed(): void
    {
        $html = '<p>See storage/app/private and file://tmp/x for details.</p>';

        $result = app(PromptLandingPageHtmlSanitizer::class)->sanitize($html, []);

        $this->assertStringNotContainsString('storage/app/private', $result);
        $this->assertStringNotContainsString('file://', $result);
    }

    /** Inline base64 images are still blanked, which was never the problem. */
    public function test_a_data_uri_image_is_still_blanked(): void
    {
        $result = app(PromptLandingPageHtmlSanitizer::class)->sanitize(
            '<img src="data:image/png;base64,iVBORw0KGgo=" alt="x">',
            [],
        );

        $this->assertStringContainsString('src=""', $result);
        $this->assertStringNotContainsString('base64', $result);
    }
}
