<?php

namespace Tests\Feature;

use App\Models\GoogleAdsConnection;
use App\Services\GoogleAdsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * A campaign told to count one conversion counts that one.
 *
 * The chosen conversion action used to go out as selectiveOptimization, which
 * Google refuses on Search: publishing campaign 54 on 23 Sep died on the first
 * mutate with OPERATION_NOT_PERMITTED_FOR_CONTEXT and named no field, so the
 * campaign and everything under it never existed. Removing it made publishing
 * work and left the answer going nowhere: the buyer was asked what counts as
 * success and Google was never told.
 *
 * A Search campaign says it through its conversion goals, which are separate
 * resources that cannot be set until the campaign exists. Google creates one
 * per conversion category on the account and marks every one biddable, so a
 * campaign asked to optimise for "Book Demo" also optimises for "Purchase".
 * Narrowing is flipping biddable off on the categories that were not chosen.
 *
 * All of it checked against the live account before it was written:
 *
 *   goals on a new campaign      PURCHASE biddable=true, BOOK_APPOINTMENT true
 *   update biddable=false        accepted
 *   read back                    PURCHASE absent, which is how Google says false
 */
class ConversionGoalReachesGoogleTest extends TestCase
{
    use RefreshDatabase;

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

        config([
            'services.google_ads.refresh_token' => 'a-refresh-token',
            'services.google_ads.customer_id' => '3460855874',
            'services.google_ads.developer_token' => 'dev-token',
            'platforms.guardrails.allowed_ad_accounts' => ['3460855874'],
        ]);
    }

    /**
     * @param  list<array{category: string, origin: string}>  $goals
     * @param  list<array{resource_name: string, category: string}>  $actions
     */
    private function fakeGoogle(array $goals, array $actions): void
    {
        Http::fake(function ($request) use ($goals, $actions) {
            $url = $request->url();

            if (str_contains($url, 'oauth2.googleapis.com')) {
                return Http::response(['access_token' => 'token', 'expires_in' => 3600]);
            }

            if (str_contains($url, 'googleAds:searchStream')) {
                $query = (string) ($request->data()['query'] ?? '');

                if (str_contains($query, 'campaign_conversion_goal')) {
                    return Http::response([['results' => array_map(fn (array $g): array => [
                        'campaignConversionGoal' => ['category' => $g['category'], 'origin' => $g['origin']],
                    ], $goals)]]);
                }

                if (str_contains($query, 'conversion_action')) {
                    return Http::response([['results' => array_map(fn (array $a): array => [
                        'conversionAction' => [
                            'resourceName' => $a['resource_name'],
                            'name' => $a['category'],
                            'category' => $a['category'],
                            'type' => 'WEBPAGE',
                        ],
                    ], $actions)]]);
                }

                return Http::response([['results' => []]]);
            }

            return Http::response(['results' => [['resourceName' => 'customers/3460855874/campaigns/900']]]);
        });
    }

    /** @return list<array<string,mixed>> the goal updates that went out */
    private function goalUpdatesFor(?string $action): array
    {
        $created = [];

        try {
            app(GoogleAdsService::class)->createPausedCampaign(GoogleAdsConnection::shared(), [
                'campaign_type' => 'search',
                'name' => 'Goals',
                'ad_group_name' => 'Goals - ad group',
                'daily_budget' => 10.0,
                'bidding_strategy' => 'maximize_conversions',
                'final_url' => 'https://iblockads.org/',
                'locations' => ['US'],
                'networks' => ['search'],
                'conversion_action' => $action,
                'keywords' => 'adblocker',
                'headlines' => ['One', 'Two', 'Three'],
                'descriptions' => ['First line.', 'Second line.'],
            ], $created);
        } catch (\Throwable) {
            // The fake does not model every response; the payload is the point.
        }

        foreach (Http::recorded() as [$request]) {
            /** @var Request $request */
            if (str_contains($request->url(), 'campaignConversionGoals:mutate')) {
                return $request->data()['operations'] ?? [];
            }
        }

        return [];
    }

    public function test_the_categories_not_chosen_are_turned_off(): void
    {
        $this->fakeGoogle(
            goals: [
                ['category' => 'PURCHASE', 'origin' => 'WEBSITE'],
                ['category' => 'BOOK_APPOINTMENT', 'origin' => 'WEBSITE'],
            ],
            actions: [
                ['resource_name' => 'customers/3460855874/conversionActions/1', 'category' => 'PURCHASE'],
                ['resource_name' => 'customers/3460855874/conversionActions/2', 'category' => 'BOOK_APPOINTMENT'],
            ],
        );

        $updates = $this->goalUpdatesFor('customers/3460855874/conversionActions/2');

        $this->assertCount(1, $updates, 'Exactly the one category not chosen should be turned off.');
        $this->assertSame(
            'customers/3460855874/campaignConversionGoals/900~PURCHASE~WEBSITE',
            $updates[0]['update']['resourceName'],
        );
        $this->assertFalse($updates[0]['update']['biddable']);
        $this->assertSame('biddable', $updates[0]['updateMask']);
    }

    /** The chosen one is left alone, because Google already made it biddable. */
    public function test_the_chosen_category_is_not_touched(): void
    {
        $this->fakeGoogle(
            goals: [
                ['category' => 'PURCHASE', 'origin' => 'WEBSITE'],
                ['category' => 'BOOK_APPOINTMENT', 'origin' => 'WEBSITE'],
            ],
            actions: [
                ['resource_name' => 'customers/3460855874/conversionActions/2', 'category' => 'BOOK_APPOINTMENT'],
            ],
        );

        $names = collect($this->goalUpdatesFor('customers/3460855874/conversionActions/2'))
            ->pluck('update.resourceName')
            ->implode(' ');

        $this->assertStringNotContainsString('BOOK_APPOINTMENT', $names);
    }

    /** No conversion action means no narrowing: the account's goals stand. */
    public function test_a_campaign_with_no_conversion_action_narrows_nothing(): void
    {
        $this->fakeGoogle(
            goals: [['category' => 'PURCHASE', 'origin' => 'WEBSITE']],
            actions: [['resource_name' => 'customers/3460855874/conversionActions/1', 'category' => 'PURCHASE']],
        );

        $this->assertSame([], $this->goalUpdatesFor(null));
    }

    /**
     * An action the account does not have narrows nothing.
     *
     * Turning every category off would leave a campaign optimising toward
     * nothing at all, which is worse than counting more than was asked.
     */
    public function test_an_unknown_conversion_action_leaves_every_goal_alone(): void
    {
        $this->fakeGoogle(
            goals: [
                ['category' => 'PURCHASE', 'origin' => 'WEBSITE'],
                ['category' => 'BOOK_APPOINTMENT', 'origin' => 'WEBSITE'],
            ],
            actions: [['resource_name' => 'customers/3460855874/conversionActions/1', 'category' => 'PURCHASE']],
        );

        $this->assertSame([], $this->goalUpdatesFor('customers/3460855874/conversionActions/999'));
    }

    /** Every origin of an unchosen category goes off, not just the website one. */
    public function test_each_origin_of_an_unchosen_category_is_turned_off(): void
    {
        $this->fakeGoogle(
            goals: [
                ['category' => 'PURCHASE', 'origin' => 'WEBSITE'],
                ['category' => 'PURCHASE', 'origin' => 'APP'],
                ['category' => 'BOOK_APPOINTMENT', 'origin' => 'WEBSITE'],
            ],
            actions: [['resource_name' => 'customers/3460855874/conversionActions/2', 'category' => 'BOOK_APPOINTMENT']],
        );

        $this->assertCount(2, $this->goalUpdatesFor('customers/3460855874/conversionActions/2'));
    }
}
