<?php

namespace Tests\Feature;

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

/**
 * Each platform is asked how it takes a budget, in its own words.
 *
 * Found driving the Google flow end to end on 23 Sep. The budget question
 * offered four options written into SetCampaignBudget and shown everywhere:
 * "Daily", "Lifetime", "Daily, Advantage campaign budget" and "Lifetime,
 * Advantage campaign budget". Advantage campaign budget is Meta's name for a
 * Meta feature, and Google has neither it nor ad sets.
 *
 * So a Google buyer was asked to choose between an ad set budget and a Meta
 * feature, and three of the four answers could not be published: a Google
 * budget is one campaignBudget resource with an amountMicros, shared by the
 * ad groups, and GoogleAdsService sends that daily amount whichever option was
 * picked. The question was decoration over a decision that had already been
 * made for it.
 *
 * Same shape as the objectives and placements that leaked across before: a
 * platform difference hardcoded into a shared tool.
 */
class BudgetModesComeFromThePlatformTest 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',
            'ad_account_id' => $platform === 'meta' ? 'act_1238155271384134' : '3460855874',
            'currency' => 'USD',
        ]);

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

        return $campaign;
    }

    private function runBudgetTool(array $arguments = []): void
    {
        foreach (app(ToolRegistry::class)->resolve() as $tool) {
            if ($tool->name() === 'campaign__set_campaign_budget') {
                $tool->handle(new Request($arguments));

                return;
            }
        }

        $this->fail('set_campaign_budget is not registered.');
    }

    /** @return array<string,mixed>|null */
    private function offerFor(string $field): ?array
    {
        foreach (app(Choices::class)->all() as $offer) {
            if ($offer['field'] === $field) {
                return $offer;
            }
        }

        return null;
    }

    public function test_google_is_never_offered_metas_advantage_campaign_budget(): void
    {
        $this->campaignOn('google');
        $this->runBudgetTool();

        $offer = $this->offerFor('budget_strategy');

        $this->assertNull(
            $offer,
            'Google has one way of holding a budget, so there is nothing to ask: '
                .json_encode($offer['options'] ?? []),
        );
    }

    /**
     * Not asking still leaves the campaign set up.
     *
     * Skipping the question must not skip the answer, or the budget lands with
     * no level and the publish path has to guess.
     */
    public function test_googles_single_mode_is_applied_without_asking(): void
    {
        $campaign = $this->campaignOn('google');
        $this->runBudgetTool();

        $campaign->refresh();

        $this->assertSame('campaign', $campaign->budget_level, 'A Google budget is held by the campaign.');
        $this->assertSame('daily', $campaign->budget_mode);
    }

    /** It is the platform's rule, not the buyer's answer nor the model's guess. */
    public function test_the_applied_mode_is_recorded_as_the_platforms(): void
    {
        $campaign = $this->campaignOn('google');
        $this->runBudgetTool();

        $sources = app(Workspace::class)->sourcesFor($campaign->refresh());

        $this->assertSame(ArtifactFieldSource::PLATFORM_RETURNED, $sources['budget_level']);
        $this->assertSame(ArtifactFieldSource::PLATFORM_RETURNED, $sources['budget_mode']);
    }

    /** Meta really does offer all four, and keeps them. */
    public function test_meta_is_still_asked_and_still_gets_four(): void
    {
        $this->campaignOn('meta');
        $this->runBudgetTool();

        $offer = $this->offerFor('budget_strategy');

        $this->assertNotNull($offer, 'Meta genuinely has a choice here and must still be asked.');
        $this->assertCount(4, $offer['options']);

        $labels = array_column($offer['options'], 'label');

        $this->assertContains('Daily, Advantage campaign budget', $labels);
    }

    /**
     * Every platform answers the question, including the draft-only ones.
     *
     * The contract requires it so a new platform cannot silently inherit
     * another's, which is how this went wrong in the first place.
     */
    public function test_every_platform_declares_its_own_budget_modes(): void
    {
        foreach (array_keys((array) config('platforms.rules')) as $platform) {
            $modes = app(Platforms::class)->for($platform)->budgetModes();

            $this->assertNotEmpty($modes, "{$platform} offers no way to set a budget at all.");

            foreach ($modes as $mode) {
                $this->assertMatchesRegularExpression(
                    '/^(adset|campaign):(daily|lifetime)$/',
                    $mode['value'],
                    "{$platform} has a budget mode the tool cannot apply: {$mode['value']}",
                );
            }

            if ($platform === 'meta') {
                continue;
            }

            $this->assertStringNotContainsStringIgnoringCase(
                'Advantage',
                json_encode($modes) ?: '',
                "{$platform} is offering a Meta feature by name.",
            );
        }
    }
}
