<?php

namespace Tests\Feature;

use App\Services\AI\WebsiteBrandSignalExtractor;
use PHPUnit\Framework\Attributes\DataProvider;
use ReflectionMethod;
use Tests\TestCase;

/**
 * A logo scraped off a stranger's site cannot bring code with it.
 *
 * The brand extractor lifts the header logo from whatever site the buyer names
 * in the chat, the model is then instructed to embed that markup byte-for-byte
 * in the generated landing page, and the page is served from our own origin in
 * the owner's authenticated session. So the markup is a stranger's and the
 * origin is ours.
 *
 * The guard was a handful of regexes removing script tags, quoted on* handlers
 * and non-fragment hrefs. It missed SMIL entirely - <animate> and <set> - and
 * <foreignObject>, which carries arbitrary HTML, and all three were confirmed
 * executing in a browser. It also missed unquoted handlers and <style>, which
 * nobody had thought to try.
 *
 * That is what a blacklist does, so this is an allow-list now: the elements and
 * attributes a logo is made of are kept and everything else goes, without
 * needing to know what it does.
 */
class AScrapedLogoCannotCarryScriptTest extends TestCase
{
    private function sanitize(string $svg): string
    {
        $method = new ReflectionMethod(WebsiteBrandSignalExtractor::class, 'sanitizeSvgMarkup');
        $method->setAccessible(true);

        return $method->invoke(app(WebsiteBrandSignalExtractor::class), $svg);
    }

    public static function payloads(): array
    {
        return [
            'SMIL animate rewriting href' => ['<svg xmlns="http://www.w3.org/2000/svg"><a><text>Acme</text><animate attributeName="href" values="javascript:alert(1)" begin="0s"/></a></svg>'],
            'SMIL set on a handler' => ['<svg xmlns="http://www.w3.org/2000/svg"><set attributeName="onload" to="alert(1)"/><path d="M0 0"/></svg>'],
            'foreignObject carrying html' => ['<svg xmlns="http://www.w3.org/2000/svg"><foreignObject><img src="x" onerror="alert(1)"/></foreignObject></svg>'],
            'a script element' => ['<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script><path d="M0 0"/></svg>'],
            'a quoted handler' => ['<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"><path d="M0 0"/></svg>'],
            'a style element' => ['<svg xmlns="http://www.w3.org/2000/svg"><style>*{background:url(javascript:alert(1))}</style><path d="M0 0"/></svg>'],
            'use pulling an external document' => ['<svg xmlns="http://www.w3.org/2000/svg"><use href="http://evil.example/x.svg#a"/></svg>'],
            'an image with a data url' => ['<svg xmlns="http://www.w3.org/2000/svg"><image href="data:text/html,<script>alert(1)</script>"/></svg>'],
        ];
    }

    #[DataProvider('payloads')]
    public function test_nothing_executable_survives(string $svg): void
    {
        $clean = $this->sanitize($svg);

        foreach (['script', 'onerror', 'onload', 'onclick', 'javascript:', 'foreignObject', '<animate', '<set', '<use', '<style', '<image'] as $forbidden) {
            $this->assertStringNotContainsStringIgnoringCase(
                $forbidden,
                $clean,
                "[{$forbidden}] survived sanitising",
            );
        }
    }

    /**
     * And the logo still looks like a logo afterwards.
     *
     * A sanitiser that empties everything is safe and useless. Gradients in
     * particular are what an over-eager strip tends to break.
     */
    public function test_a_real_logo_keeps_its_shape(): void
    {
        $clean = $this->sanitize(
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">'
            .'<defs><linearGradient id="g"><stop offset="0" stop-color="#f00"/></linearGradient></defs>'
            .'<path d="M2 2 L22 22" fill="url(#g)" stroke-width="2"/>'
            .'<text x="4" y="8" font-size="6">Acme</text></svg>'
        );

        foreach (['viewBox="0 0 24 24"', 'linearGradient', 'stop-color="#f00"', 'd="M2 2 L22 22"', 'fill="url(#g)"', 'Acme'] as $kept) {
            $this->assertStringContainsString($kept, $clean, "[{$kept}] was stripped from a legitimate logo");
        }
    }

    /** Markup that will not parse yields nothing rather than passing through. */
    public function test_unparseable_markup_yields_nothing(): void
    {
        $this->assertSame('', $this->sanitize('<svg><path d="M0 0"'));
        $this->assertSame('', $this->sanitize(''));
    }
}
