<?php

namespace Tests\Feature;

use App\DTOs\LandingPageAnalysis;
use App\Services\LandingPageAnalyzer;
use App\Services\RenderedPageFetcher;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * A page whose content only arrives with its JavaScript is read again in a
 * browser.
 *
 * lucos.com answers 200 with 1362 bytes: a doctype, a CSP meta tag, a favicon,
 * a viewport and one script tag. Its own CSP says why - connect-src
 * https://api.lucos.com - the content comes from an API after the page loads.
 *
 * So the analyser reported exactly what was there, a title and nothing else,
 * and every ad written from it read "Visit lucos.com. Explore lucos.com to
 * learn more." Four of Kaushal's five conversations went that way, and it reads
 * as bad copywriting rather than as a page we could not see.
 *
 * The three sites where suggestions were good - iblockads.org 52kB,
 * attestpath.ai 58kB, rebates.com 18kB - are server rendered and must never pay
 * for a browser launch, which is why the trigger is the empty read rather than
 * the presence of script tags.
 */
class AClientRenderedPageIsReadInABrowserTest extends TestCase
{
    /** The real shape of lucos.com, trimmed. */
    private const SHELL = '<!doctype html><html lang="en"><head>'
        .'<meta charset="UTF-8"><meta http-equiv="Content-Security-Policy" content="default-src \'self\'">'
        .'<title>lucos.com</title></head><body><div id="root"></div><script src="/app.js"></script></body></html>';

    private const RENDERED = '<!doctype html><html><head><title>Lucos</title>'
        .'<meta name="description" content="Agent workflows with plan mode and model choice.">'
        .'</head><body><h1>Plan Mode: review before changes</h1>'
        .'<p>Lucos runs multi-agent workflows. Choose your model, review the plan before anything '
        .'is changed, and keep every step auditable across your whole team and organisation.</p>'
        .'</body></html>';

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

        config(['scraping.puppeteer.enabled' => true]);
    }

    private function analyze(string $served): LandingPageAnalysis
    {
        Http::fake(['*' => Http::response($served)]);

        return app(LandingPageAnalyzer::class)->analyze('https://lucos.com/landing');
    }

    /** The defect: a shell produced a title and nothing to write ads from. */
    public function test_an_app_shell_is_rendered_and_re_read(): void
    {
        $this->mock(RenderedPageFetcher::class)
            ->shouldReceive('fetch')->once()->andReturn(self::RENDERED);

        $analysis = $this->analyze(self::SHELL);

        $this->assertNotEmpty($analysis->headings, 'the rendered headings were not used');
        $this->assertStringContainsString('Plan Mode', implode(' ', $analysis->headings));
        $this->assertStringContainsString('multi-agent', $analysis->text);
    }

    /**
     * A page that already has content never reaches the browser.
     *
     * Rendering costs a launch and several seconds. The sites that work today
     * must not start paying for it.
     */
    public function test_a_server_rendered_page_is_not_rendered_again(): void
    {
        $this->mock(RenderedPageFetcher::class)->shouldNotReceive('fetch');

        $analysis = $this->analyze(self::RENDERED);

        $this->assertStringContainsString('Plan Mode', implode(' ', $analysis->headings));
    }

    /**
     * A render that comes back no better is discarded.
     *
     * A consent wall or a bot check renders successfully and returns less than
     * the plain fetch did. Replacing a thin analysis with an empty one helps
     * nobody.
     */
    public function test_a_render_that_is_no_better_is_discarded(): void
    {
        $this->mock(RenderedPageFetcher::class)
            ->shouldReceive('fetch')->once()
            ->andReturn('<html><head><title>Just a title</title></head><body></body></html>');

        $analysis = $this->analyze(self::SHELL);

        $this->assertSame('lucos.com', $analysis->title, 'the worse render replaced the original read');
    }

    /** And a render that fails leaves the original analysis alone. */
    public function test_a_failed_render_leaves_the_first_read_in_place(): void
    {
        $this->mock(RenderedPageFetcher::class)
            ->shouldReceive('fetch')->once()->andReturnNull();

        $this->assertSame('lucos.com', $this->analyze(self::SHELL)->title);
    }

    /** With rendering switched off the fetcher does nothing at all. */
    public function test_the_fetcher_is_inert_when_puppeteer_is_disabled(): void
    {
        config(['scraping.puppeteer.enabled' => false]);

        $this->assertNull(app(RenderedPageFetcher::class)->fetch('https://lucos.com/landing'));
    }

    /**
     * A browser is never pointed at a private address.
     *
     * It fetches every subresource the page names, so handing it an internal
     * host is worse here than anywhere else in the application.
     */
    public function test_the_fetcher_refuses_a_private_address(): void
    {
        foreach ([
            'http://127.0.0.1:6379/',
            'http://169.254.169.254/latest/meta-data/',
            'file:///etc/passwd',
        ] as $url) {
            $this->assertNull(app(RenderedPageFetcher::class)->fetch($url), "{$url} was handed to a browser");
        }
    }
}
