<?php

namespace Tests\Feature;

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

/**
 * Google sells three kinds of campaign, and the buyer picks which.
 *
 * Meta has no equivalent, so the shared flow never asked, and the publisher
 * hardcoded 'search'. Offering the choice without honouring it would be worse
 * than not offering it: picking Display would have quietly produced a Search
 * campaign, which is the kind of wrong that succeeds.
 */
class GoogleCampaignTypeTest extends TestCase
{
    use RefreshDatabase;

    private User $user;

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

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

    private function campaign(string $platform, array $attributes = []): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => $this->user->id,
            'name' => 'Test',
            'platform' => $platform,
            'status' => 'draft',
            ...$attributes,
        ]);

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

        return $campaign;
    }

    private function offer(): array
    {
        app(ChooseCampaignType::class)->handle(new Request([]));

        return app(Choices::class)->all()[0]['options'] ?? [];
    }

    /** @return array<string, mixed>|null the option carrying the Suggested badge */
    private function suggested(): ?array
    {
        return collect($this->offer())->firstWhere('badge', 'Suggested');
    }

    /** Search suits a campaign built from a page and nothing else. */
    public function test_search_is_suggested_for_an_ordinary_campaign(): void
    {
        $campaign = $this->campaign('google', ['objective' => 'LEADS']);

        $this->assertSame('search', $this->suggested()['value']);

        // Suggested, not set. Picking a type commits the buyer to what it
        // needs, so nothing is decided for them.
        $this->assertNull($campaign->fresh()->campaign_type);
    }

    /**
     * Awareness is not a search campaign.
     *
     * Reach is bought on impressions and the search network does not sell them.
     * Search was suggested for every campaign regardless, which is a default
     * rather than a recommendation.
     */
    public function test_display_is_suggested_for_an_awareness_campaign(): void
    {
        $this->campaign('google', ['objective' => 'BRAND_AWARENESS_AND_REACH']);

        $suggested = $this->suggested();

        $this->assertSame('display', $suggested['value']);
        $this->assertStringContainsString('impressions', $suggested['hint']);
    }

    /** The recommendation says why, not just that. */
    public function test_the_suggestion_carries_its_reason(): void
    {
        $this->campaign('google', ['objective' => 'LEADS']);

        $this->assertStringContainsString('Suggested because', $this->suggested()['hint']);
    }

    /** Every type is still offered, whichever one is recommended. */
    public function test_all_three_types_are_offered(): void
    {
        $this->campaign('google');

        $this->assertEqualsCanonicalizing(
            ['search', 'display', 'performance_max'],
            array_column($this->offer(), 'value'),
        );
    }

    /**
     * Nothing is suggested that the campaign could not then publish.
     *
     * Performance Max needs images at both sizes. Without them the gate would
     * refuse it, and recommending something we then refuse is worse than not
     * recommending it.
     */
    public function test_performance_max_is_not_suggested_without_the_images_it_needs(): void
    {
        $this->campaign('google', [
            'objective' => 'SALES',
            'pixel_id' => 'customers/1/conversionActions/9',
        ]);

        $this->assertNotSame('performance_max', $this->suggested()['value']);
    }

    /** Meta never sees the question, because Meta has no types. */
    public function test_a_platform_without_types_is_not_asked(): void
    {
        $campaign = $this->campaign('meta');

        $result = json_decode((string) app(ChooseCampaignType::class)->handle(new Request([])), true);

        $this->assertTrue($result['ok']);
        $this->assertSame([], app(Choices::class)->all());

        $outstanding = array_column(app(LaunchFlow::class)->progress($campaign), 'key');
        $this->assertNotContains('campaign_type', $outstanding);
    }

    /** And Google does, as its own step. */
    public function test_google_is_asked_as_a_step_in_the_flow(): void
    {
        $campaign = $this->campaign('google');

        $steps = collect(app(LaunchFlow::class)->progress($campaign));

        $this->assertTrue($steps->contains('key', 'campaign_type'));
        $this->assertFalse($steps->firstWhere('key', 'campaign_type')['done']);

        $campaign->update(['campaign_type' => 'search']);

        $this->assertTrue(
            collect(app(LaunchFlow::class)->progress($campaign->fresh()))
                ->firstWhere('key', 'campaign_type')['done'],
        );
    }

    /** A type Google does not sell here is refused by name. */
    public function test_an_unsupported_type_is_refused(): void
    {
        $this->campaign('google');

        $result = json_decode((string) app(ChooseCampaignType::class)->handle(
            new Request(['campaign_type' => 'shopping']),
        ), true);

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('shopping', $result['error']);
    }

    /**
     * A Search ad is text, a Display ad is pictures.
     *
     * So on Google this is a property of the campaign type rather than of the
     * platform, and the flow asks for creatives only where they are used.
     */
    public function test_creatives_are_asked_for_display_and_not_for_search(): void
    {
        $rules = app(Platforms::class)->for('google');

        $search = $this->campaign('google', ['campaign_type' => 'search']);
        $display = $this->campaign('google', ['campaign_type' => 'display']);

        $this->assertFalse($rules->requiresCreativeAsset($search));
        $this->assertTrue($rules->requiresCreativeAsset($display));

        $this->assertNotContains(
            'creatives',
            array_column(app(LaunchFlow::class)->progress($search), 'key'),
        );
        $this->assertContains(
            'creatives',
            array_column(app(LaunchFlow::class)->progress($display), 'key'),
        );
    }

    /**
     * Display is refused without images at the sizes Google demands.
     *
     * Refused here rather than by the API, which only says so after the
     * campaign and its budget already exist on the account.
     */
    public function test_display_is_refused_without_the_required_image_sizes(): void
    {
        $campaign = $this->campaign('google', ['campaign_type' => 'display']);

        $problems = app(PublishGate::class)->check($campaign);

        $this->assertNotEmpty(array_filter(
            $problems,
            fn (string $p): bool => str_contains($p, '1200x628'),
        ), 'Display published without a landscape creative. Problems: '.implode(' | ', $problems));

        $this->assertNotEmpty(array_filter(
            $problems,
            fn (string $p): bool => str_contains($p, '1200x1200'),
        ));
    }

    /** Search is not asked for images it has no use for. */
    public function test_search_is_not_refused_over_image_sizes(): void
    {
        $campaign = $this->campaign('google', ['campaign_type' => 'search']);

        $problems = app(PublishGate::class)->check($campaign);

        $this->assertEmpty(array_filter(
            $problems,
            fn (string $p): bool => str_contains($p, '1200x'),
        ));
    }
}
