<?php

namespace Tests\Feature;

use App\Campaigns\Publishing\MetaPublisher;
use App\Models\Campaign;
use App\Models\User;
use App\Services\Meta\Meta;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * Creating through the MCP is not publishing, and this is what closes the gap.
 *
 * ads_create_campaign has no status parameter. What it makes is an unpublished
 * Ads Manager draft with the status staged ACTIVE, which neither serves nor runs
 * and which goes live the moment somebody presses Review and publish. Every
 * campaign this product reported as published on production between 30 Sep and
 * 1 Oct was one of those: campaign 120249665465240510 read back from
 * ads_get_ad_entities as source=mcp_campaign_creation, status=EDITING, staged
 * status ACTIVE.
 *
 * Two calls close it, both verified against the live endpoint on 1 Oct before
 * any of this was written. ads_update_entity restages a draft object PAUSED and
 * answers is_draft true. ads_activate_entity with publish_as_active false
 * publishes the staged objects exactly as staged, which is what turns a draft
 * into a live campaign.
 *
 * What these pin is the pair of mistakes that would put spend at risk: leaving
 * the draft staged ACTIVE, and publishing with publish_as_active true.
 */
class McpPublishesPausedTest extends TestCase
{
    use RefreshDatabase;

    /** @var list<array{tool: string, arguments: array<string, mixed>}> */
    private array $calls = [];

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

        config([
            'platforms.meta.mcp.enabled' => true,
            'platforms.meta.mcp.token' => 'test-token',
            'platforms.meta.token' => 'test-token',
            'platforms.guardrails.paused' => false,
            'platforms.guardrails.allowed_ad_accounts' => ['act_1238155271384134'],
        ]);

        $this->calls = [];

        // Every create answers with an id so the publisher can walk the tree,
        // and every call is recorded so the order can be asserted.
        Http::fake(function ($request) {
            $body = $request->data();
            $tool = $body['params']['name'] ?? '';

            // Graph speaks a different shape, and this test also runs with the
            // MCP off to prove nothing publishes on that path.
            if ($tool === '') {
                $this->calls[] = ['tool' => $request->url(), 'arguments' => $body];

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

            $this->calls[] = ['tool' => $tool, 'arguments' => $body['params']['arguments'] ?? []];

            $payload = match ($tool) {
                'ads_create_campaign' => ['campaign_id' => '900001'],
                'ads_create_ad_set' => ['ad_set_id' => '900002'],
                'ads_create_creative' => ['creative_id' => '900003'],
                'ads_create_ad' => ['ad_id' => '900004'],
                default => ['success' => true],
            };

            return Http::response([
                'result' => ['content' => [['type' => 'text', 'text' => json_encode($payload)]]],
            ]);
        });
    }

    private function publish(): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => User::factory()->create()->id,
            'name' => 'Paused on arrival',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_TRAFFIC',
            'ad_account_id' => 'act_1238155271384134',
            'page_id' => '1207169149145078',
            'landing_url' => 'https://example.com',
            'budget' => 20,
            'budget_mode' => 'daily',
            'currency' => 'USD',
            'audience' => ['countries' => ['US'], 'placements' => ['facebook'], 'devices' => ['mobile', 'desktop']],
        ]);

        $created = [];

        app(MetaPublisher::class)->create($campaign->fresh(), 'tester', $created);

        return $campaign->fresh();
    }

    /** @return list<array<string, mixed>> */
    private function argumentsFor(string $tool): array
    {
        return array_values(array_map(
            fn (array $call): array => $call['arguments'],
            array_filter($this->calls, fn (array $call): bool => $call['tool'] === $tool),
        ));
    }

    /**
     * The draft is never left staged.
     *
     * Without this the campaign is only ever an Ads Manager draft, which is the
     * state production was in: nothing served, nothing ran, and the product said
     * it had published.
     */
    public function test_the_staged_draft_is_published(): void
    {
        $this->publish();

        $this->assertNotEmpty(
            $this->argumentsFor('ads_activate_entity'),
            'Nothing published the draft, so the campaign exists only in Ads Manager.',
        );
    }

    /** And published as staged rather than as live. */
    public function test_it_is_never_published_as_active(): void
    {
        $this->publish();

        foreach ($this->argumentsFor('ads_activate_entity') as $arguments) {
            $this->assertFalse(
                $arguments['publish_as_active'] ?? true,
                'The campaign was published active, so it begins spending immediately.',
            );
        }
    }

    /**
     * Every level is restaged PAUSED before the publish.
     *
     * Publishing takes the staged values exactly as they are, and ACTIVE is what
     * Meta stages, so without this the publish itself is what makes it live.
     */
    public function test_every_object_is_restaged_paused_first(): void
    {
        $this->publish();

        $paused = [];

        foreach ($this->argumentsFor('ads_update_entity') as $arguments) {
            $fields = json_decode((string) ($arguments['fields'] ?? '{}'), true);

            if (($fields['status'] ?? null) === 'PAUSED') {
                $paused[] = $arguments['entity_type'];
            }
        }

        $this->assertEqualsCanonicalizing(['campaign', 'ad_set'], $paused);
    }

    /** The restaging happens before the publish, or it changes nothing. */
    public function test_the_restaging_comes_before_the_publish(): void
    {
        $this->publish();

        $tools = array_column($this->calls, 'tool');

        $this->assertLessThan(
            array_search('ads_activate_entity', $tools, true),
            array_search('ads_update_entity', $tools, true),
            'The publish ran before anything was restaged, so it published ACTIVE.',
        );
    }

    /**
     * The whole tree is published together.
     *
     * object_ids publishes exactly what it lists. A campaign published without
     * its ad sets leaves them staged under a parent that is now live, which is
     * the half-made campaign the rollback exists to prevent.
     */
    public function test_the_campaign_its_ad_set_and_its_ads_publish_together(): void
    {
        app(Meta::class)->settleAsPaused('act_1238155271384134', [
            ['id' => '900001', 'type' => 'campaign'],
            ['id' => '900002', 'type' => 'ad_set'],
            ['id' => '900004', 'type' => 'ad'],
            ['id' => '900005', 'type' => 'ad'],
        ]);

        $arguments = $this->argumentsFor('ads_activate_entity')[0];

        $this->assertSame(['900001', '900002', '900004', '900005'], $arguments['object_ids']);
        $this->assertSame('900001', $arguments['entity_id']);
        $this->assertSame('campaign', $arguments['entity_type']);
        $this->assertFalse($arguments['publish_as_active']);

        // Each level restaged, ads included.
        $types = array_column($this->argumentsFor('ads_update_entity'), 'entity_type');
        $this->assertSame(['campaign', 'ad_set', 'ad', 'ad'], $types);
    }

    /**
     * Graph creates live and paused already, so it must not publish anything.
     *
     * ads_activate_entity against a Graph-made campaign is not a no-op: it is
     * the call that turns a paused campaign on.
     */
    public function test_nothing_is_published_when_the_mcp_is_off(): void
    {
        config(['platforms.meta.mcp.enabled' => false]);

        $this->publish();

        foreach ($this->calls as $call) {
            $this->assertStringNotContainsString('activate', strtolower($call['tool']));
        }
    }
}
