<?php

namespace Tests\Feature;

use App\Agent\Choices;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\Platforms\Platforms;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Clicks are tagged with macros the platform actually substitutes.
 *
 * Found finishing a Google campaign on 23 Sep. "Standard tagging" came from one
 * config string used everywhere:
 *
 *   utm_source=facebook&utm_medium=paid_social
 *     &utm_campaign={{campaign.name}}&utm_content={{ad.name}}
 *
 * The Google campaign was saved with all of it. Every click would have reached
 * the landing page claiming to come from facebook on paid_social, with the
 * double braces passed through as literal text because Google substitutes
 * {campaignid} and {creative}, not Meta's macros. Attribution broken on a live
 * campaign, reported by nothing.
 *
 * The third platform difference found in one run, after the objectives and the
 * budget modes: anything a platform words differently belongs on the platform.
 */
class TrackingMacrosComeFromThePlatformTest extends TestCase
{
    use RefreshDatabase;

    private User $user;

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

        $this->user = User::factory()->create();
        $this->actingAs($this->user);
        app(Workspace::class)->ownedBy($this->user);
    }

    private function campaignOn(string $platform): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => $this->user->id,
            'name' => ucfirst($platform).' campaign',
            'platform' => $platform,
            'status' => 'draft',
            'landing_url' => 'https://iblockads.org/',
        ]);

        app(Workspace::class)->produced($campaign);

        return $campaign;
    }

    /** @return list<array<string,mixed>> */
    private function taggingOptions(string $platform): array
    {
        $this->campaignOn($platform);

        foreach (app(ToolRegistry::class)->resolve() as $tool) {
            if ($tool->name() === 'campaign__set_tracking') {
                $tool->handle(new Request([]));
                break;
            }
        }

        foreach (app(Choices::class)->all() as $offer) {
            if ($offer['field'] === 'utm') {
                return $offer['options'];
            }
        }

        $this->fail('No tagging options were offered.');
    }

    public function test_google_is_not_tagged_as_facebook_paid_social(): void
    {
        $values = implode(' ', array_column($this->taggingOptions('google'), 'value'));

        $this->assertStringNotContainsString('facebook', $values);
        $this->assertStringNotContainsString('paid_social', $values);
        $this->assertStringContainsString('utm_source=google', $values);
    }

    /** Meta's macros are meaningless to Google and arrive as literal text. */
    public function test_google_does_not_carry_metas_macros(): void
    {
        $values = implode(' ', array_column($this->taggingOptions('google'), 'value'));

        $this->assertStringNotContainsString('{{', $values, 'Double braces are Meta syntax.');
        $this->assertStringContainsString('{campaignid}', $values);
        $this->assertStringContainsString('{creative}', $values);
    }

    /** Meta keeps its own, still read from config so an account can override it. */
    /**
     * Meta's macros, and the ids rather than the names.
     *
     * This asserted {{campaign.name}} and utm_source=facebook. A name changes
     * whenever the campaign is renamed and collides whenever two campaigns
     * share one, so a click tagged with it cannot be joined back to anything;
     * and the campaign may run on Instagram and Threads, which are not
     * facebook. Ids are stable and joinable, which is the point of tagging.
     */
    public function test_meta_keeps_its_macros(): void
    {
        $values = implode(' ', array_column($this->taggingOptions('meta'), 'value'));

        $this->assertStringContainsString('{{campaign.id}}', $values);
        $this->assertStringContainsString('{{adset.id}}', $values);
        $this->assertStringContainsString('{{ad.id}}', $values);
        $this->assertStringContainsString('utm_source=meta', $values);
        $this->assertStringContainsString('utm_medium=cpc', $values);
    }

    /** A name macro would undo the point of using ids. */
    public function test_meta_no_longer_tags_clicks_with_names(): void
    {
        $values = implode(' ', array_column($this->taggingOptions('meta'), 'value'));

        $this->assertStringNotContainsString('{{campaign.name}}', $values);
        $this->assertStringNotContainsString('{{ad.name}}', $values);
    }

    /** Opting out means the same thing everywhere, so it is offered everywhere. */
    public function test_no_tracking_is_offered_on_both(): void
    {
        foreach (['meta', 'google'] as $platform) {
            $this->assertContains(
                '',
                array_column($this->taggingOptions($platform), 'value'),
                "{$platform} cannot be published without tracking parameters.",
            );

            app(Choices::class)->drain();
        }
    }

    /**
     * A campaign already carrying Meta's macros is stopped at the gate.
     *
     * Campaign 47 on dev passed the gate with zero problems while tagged
     * utm_source=facebook and {{campaign.name}}, because it was built before
     * the options were split per platform. A wrong value looks exactly like a
     * right one, so the gate has to say so.
     */
    public function test_the_gate_refuses_a_google_campaign_tagged_with_meta_macros(): void
    {
        $campaign = $this->campaignOn('google');
        $campaign->update([
            'utm' => 'utm_source=facebook&utm_medium=paid_social&utm_campaign={{campaign.name}}',
        ]);

        $problems = implode(' | ', app(Platforms::class)->for('google')->additionalProblems($campaign->fresh()));

        $this->assertStringContainsString('Meta macros', $problems);
    }

    /** Google's own tagging passes. */
    public function test_the_gate_accepts_googles_own_macros(): void
    {
        $campaign = $this->campaignOn('google');
        $campaign->update(['utm' => 'utm_source=google&utm_medium=cpc&utm_campaign={campaignid}']);

        $problems = implode(' | ', app(Platforms::class)->for('google')->additionalProblems($campaign->fresh()));

        $this->assertStringNotContainsString('Meta macros', $problems);
    }

    /**
     * No platform borrows another's macro syntax.
     *
     * The draft-only platforms get plain parameters and no macros at all: a
     * macro guessed from another platform's syntax reaches the landing page as
     * literal braces, which is exactly the bug being fixed here.
     */
    public function test_no_platform_offers_another_platforms_macros(): void
    {
        foreach (array_keys((array) config('platforms.rules')) as $platform) {
            $values = implode(' ', array_column(
                app(Platforms::class)->for($platform)->trackingOptions(),
                'value',
            ));

            $this->assertStringNotContainsString(
                'utm_source=facebook',
                $platform === 'meta' ? '' : $values,
                "{$platform} tags its clicks as coming from facebook.",
            );

            if (! in_array($platform, ['meta', 'google'], true)) {
                $this->assertStringNotContainsString('{', $values, "{$platform} guessed a macro syntax.");
            }
        }
    }
}
