<?php

namespace Tests\Feature;

use App\Agent\Tools\Campaign\AnalyzeLandingPage;
use App\Agent\Tools\Campaign\StartCampaign;
use App\Agent\Workspace;
use App\Campaigns\Briefs;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Everything said in the first message still produces a brief.
 *
 * Found on the dev server on 23 Sep, not by the suite. "Advertise <url> on Meta
 * and Google" in one message analysed the page and created both campaigns, and
 * left brief_id null on both with no brief row anywhere. The landing page, the
 * budget, the dates and the creatives are all the brief's, so without one the
 * two campaigns share nothing and every question gets asked twice.
 *
 * The cause is ordering. A conversation row does not exist until the turn
 * finishes, so Briefs::current() has no conversation to key on while the tools
 * are running and correctly declines to invent one. Nothing then went back to
 * finish the job once the conversation did exist.
 *
 * The earlier manual test missed it by using two messages, which meant the
 * conversation already existed by the time start_campaign ran.
 */
class BriefSurvivesFirstMessageTest extends TestCase
{
    use RefreshDatabase;

    private function page(): void
    {
        Http::fake(['*' => Http::response(
            '<html><head><title>iBlockAds</title>'
            .'<meta name="description" content="Block annoying ads and browse faster.">'
            .'</head><body><h1>Block annoying ads</h1><p>Browse faster on every site today.</p></body></html>',
        )]);
    }

    /**
     * The whole first turn, in the order the agent runs it.
     *
     * Deliberately does not bind a conversation first: that is the condition
     * being tested, and binding one would make the test pass against the bug.
     */
    public function test_a_brief_is_created_when_everything_happens_in_the_first_message(): void
    {
        $this->page();

        $user = User::factory()->create();
        $this->actingAs($user);

        $workspace = app(Workspace::class);
        $workspace->ownedBy($user);

        // No bindTo(): on the first message there is no conversation yet.
        app(AnalyzeLandingPage::class)->handle(new Request(['url' => 'https://iblockads.org/']));
        app(StartCampaign::class)->handle(new Request(['platforms' => ['meta', 'google']]));

        // The conversation comes into being as the turn ends, and the
        // controller rebinds the workspace to it.
        $conversation = (string) Str::uuid();
        $workspace->bindTo($conversation);
        app(Briefs::class)->settle();

        $campaigns = Campaign::query()->orderBy('id')->get();

        $this->assertCount(2, $campaigns, 'Both platforms should have a campaign.');

        $brief = Brief::query()->first();

        $this->assertNotNull($brief, 'No brief was created for the first message.');
        $this->assertSame(
            'https://iblockads.org/',
            $brief->landing_url,
            'The page was analysed and the result went nowhere.',
        );

        foreach ($campaigns as $campaign) {
            $this->assertSame(
                $brief->id,
                $campaign->brief_id,
                "The {$campaign->platform} campaign is not on the brief, so it shares nothing.",
            );
        }
    }

    /**
     * The controller actually calls it.
     *
     * The behaviour test above invokes settle() itself, so it passes whether or
     * not anything in the request lifecycle does. Removing the call from
     * ChatController left it green, which is a test proving a method works
     * rather than proving the product uses it.
     *
     * Checked at the source, because the alternative is faking the agent and a
     * whole turn to observe one line of wiring.
     */
    public function test_the_controller_settles_the_brief_after_the_turn(): void
    {
        $source = (string) file_get_contents(app_path('Http/Controllers/ChatController.php'));

        $this->assertStringContainsString(
            '$this->briefs->settle();',
            $source,
            'Nothing settles the brief after the turn, so a first message creates campaigns with no brief.',
        );

        // And after the rebind, not before: settling needs the conversation
        // that the rebind is what supplies.
        $this->assertGreaterThan(
            strpos($source, '$this->bindWorkspace($agent->currentConversation(), $request);'),
            strpos($source, '$this->briefs->settle();'),
            'The brief is settled before the workspace is bound, so there is still no conversation to key on.',
        );
    }

    /**
     * A second platform added later joins nothing it should not.
     *
     * The back-fill must not sweep up campaigns from an earlier, separate
     * request into a brief they were never part of: a brief covers the
     * campaigns made together from it, and only those.
     */
    public function test_the_backfill_does_not_adopt_an_unrelated_campaign(): void
    {
        $this->page();

        $user = User::factory()->create();
        $this->actingAs($user);

        $conversation = (string) Str::uuid();
        $workspace = app(Workspace::class);
        $workspace->ownedBy($user);
        $workspace->bindTo($conversation);

        // An unrelated campaign, already in the conversation, with no brief.
        $stranger = Campaign::create([
            'user_id' => $user->id,
            'name' => 'Older campaign',
            'platform' => 'meta',
            'status' => 'published',
            'external_campaign_id' => '123',
        ]);
        $workspace->produced($stranger);

        app(Briefs::class)->settle();

        $this->assertNull(
            $stranger->fresh()->brief_id,
            'A published campaign that predates the brief was adopted into it.',
        );
    }
}
