<?php

namespace Tests\Feature;

use App\Agent\Instructions;
use App\Agent\Workspace;
use App\Campaigns\PendingChanges;
use App\Http\Controllers\ChatController;
use App\Models\Campaign;
use App\Models\CampaignAd;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * An edit to a live campaign is pushed, not published, and says so.
 *
 * Driven through the chat on dev, 23 Sep. "Change the daily budget to 15 and
 * push it live" on a published Google campaign set the value locally, and the
 * chat answered:
 *
 *   Updated the campaign. Still needed: already published as 24285639010.
 *
 * Two things wrong, one cosmetic and one not.
 *
 * The sentence is the publish gate's. A live campaign has nothing left to
 * publish, so the gate reports "already published as X", and the controller
 * spliced that into "Still needed:", which reads as gibberish and describes
 * work nobody has.
 *
 * The real fault is that nothing reached Google. The prompt said nothing about
 * push_changes at all, so asked to push, the model reached for
 * publish_campaign, which answers "already published" and changes nothing. The
 * budget said 15.00 here, Google still held 12.00, and the chat had reported
 * the edit as done.
 */
class EditingALiveCampaignIsNotPublishingTest extends TestCase
{
    use RefreshDatabase;

    private function liveCampaign(): Campaign
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $campaign = Campaign::create([
            'user_id' => $user->id,
            'name' => 'iBlockAds Google Traffic',
            'platform' => 'google',
            'status' => 'published',
            'campaign_type' => 'search',
            'objective' => 'WEBSITE_TRAFFIC',
            'ad_account_id' => '3460855874',
            'landing_url' => 'https://iblockads.org/',
            'budget' => 12,
            'budget_level' => 'campaign',
            'budget_mode' => 'daily',
            'currency' => 'USD',
            'bid_strategy' => 'MAXIMIZE_CLICKS',
            'locations' => ['countries' => ['US']],
            'placements' => ['search'],
            'utm' => '',
            'external_campaign_id' => '24285639010',
            'external_ids' => ['campaign_id' => '24285639010', 'budget_id' => '15896507457'],
        ]);

        CampaignAd::create([
            'campaign_id' => $campaign->id,
            'headline' => 'Block Annoying Ads',
            'description' => 'Block pop-ups and banners instantly.',
            'primary_text' => 'Install iBlockAds and browse faster.',
            'cta' => 'LEARN_MORE',
            'status' => 'published',
        ]);

        $campaign = $campaign->fresh();
        $campaign->update(['published_state' => app(PendingChanges::class)->snapshot($campaign)]);

        return $campaign->fresh();
    }

    /**
     * The prompt tells the model which tool sends an edit.
     *
     * It said nothing at all about push_changes, which is why the model used
     * the publish tool on something already published.
     */
    public function test_the_prompt_says_push_changes_is_what_sends_an_edit(): void
    {
        $this->actingAs(User::factory()->create());

        $text = app(Instructions::class)->build();

        $this->assertStringContainsString('push_changes is what sends it', $text);
        $this->assertStringContainsString('publish_campaign is only for a campaign that has never', $text);
    }

    /** And not to call an edit live until it has actually been sent. */
    public function test_the_prompt_forbids_reporting_an_unsent_edit_as_live(): void
    {
        $this->actingAs(User::factory()->create());

        $this->assertStringContainsString(
            'Never report an edit as live, applied or updated on the',
            app(Instructions::class)->build(),
        );
    }

    /**
     * The prompt carries the real difference, so it need not be remembered.
     *
     * Nothing told the model what was unsent, so it described it from memory of
     * the conversation: after one budget edit it asked "Shall I push your budget
     * and bidding changes to Google Ads?" with the bidding untouched. Naming a
     * change that does not exist invites someone to approve a push believing it
     * covers something it does not.
     */
    public function test_the_prompt_lists_exactly_what_is_unsent(): void
    {
        $campaign = $this->liveCampaign();
        $campaign->update(['budget' => 15]);

        $this->focus($campaign->fresh());

        $text = app(Instructions::class)->build();

        $this->assertStringContainsString('NOT YET ON THE PLATFORM', $text);
        $this->assertStringContainsString('budget: live is "12.00", here it is "15.00"', $text);
        $this->assertStringContainsString('Name no others.', $text);

        // The fields that did not change are not in the list, which is the
        // whole point: "bidding" was named when it had not been touched.
        $section = substr($text, (int) strpos($text, 'NOT YET ON THE PLATFORM'));
        $this->assertStringNotContainsString('bid strategy', $section);
    }

    /** With nothing unsent it says so, rather than leaving the model to guess. */
    public function test_the_prompt_says_when_nothing_is_unsent(): void
    {
        $this->focus($this->liveCampaign());

        $this->assertStringContainsString(
            'Every value here matches the live campaign.',
            app(Instructions::class)->build(),
        );
    }

    /** A draft says nothing: everything is unsent before publishing anyway. */
    public function test_a_draft_campaign_gets_no_such_section(): void
    {
        $campaign = $this->liveCampaign();
        $campaign->update(['status' => 'draft', 'external_campaign_id' => null]);

        $this->focus($campaign->fresh());

        $this->assertStringNotContainsString('NOT YET ON THE PLATFORM', app(Instructions::class)->build());
    }

    /**
     * The fallback sentence names the pending field, not the publish gate.
     *
     * Asserted through the controller's own helper rather than a whole turn,
     * because what is being pinned is the sentence, and driving a live model to
     * fall silent on demand is not something a test can rely on.
     */
    public function test_a_live_campaign_is_told_what_is_unsent_rather_than_what_is_unpublished(): void
    {
        $campaign = $this->liveCampaign();
        $campaign->update(['budget' => 15]);

        $reply = $this->replyFor($campaign->fresh());

        $this->assertStringContainsString('budget', $reply);
        $this->assertStringContainsString('not on the platform yet', $reply);
        $this->assertStringNotContainsString('Still needed', $reply);
        $this->assertStringNotContainsString('already published as', $reply);
    }

    /** With nothing pending it says so, rather than listing publish blockers. */
    public function test_a_live_campaign_in_sync_says_it_matches(): void
    {
        $reply = $this->replyFor($this->liveCampaign());

        $this->assertStringContainsString('matches what is live', $reply);
        $this->assertStringNotContainsString('Still needed', $reply);
    }

    private function focus(Campaign $campaign): void
    {
        $workspace = app(Workspace::class);
        $workspace->ownedBy($campaign->user);
        $workspace->bindTo('conv-live-edit');
        $workspace->produced($campaign);
    }

    private function replyFor(Campaign $campaign): string
    {
        $controller = app(ChatController::class);

        $this->focus($campaign);

        $method = new \ReflectionMethod($controller, 'fallbackText');
        $method->setAccessible(true);

        return (string) $method->invoke($controller);
    }
}
