<?php

namespace Tests\Feature;

use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\CampaignImporter;
use App\Campaigns\PendingChanges;
use App\Models\ApiCall;
use App\Models\Campaign;
use App\Models\CampaignChange;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Editing a campaign this product did not build.
 *
 * Most of what a media buyer touches was made elsewhere, and from their side
 * that is not a distinction at all: they asked to change a budget. Opening one
 * reads what the platform holds and records it as the baseline, so the ordinary
 * editing path applies and the first edit shows one change rather than every
 * field at once.
 */
class ExistingCampaignTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

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

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

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

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-adopt-0001');
        $workspace->ownedBy($this->user);
    }

    /** Meta's real shapes: minor units as strings, enums, targeting nested. */
    private function fakeMeta(array $overrides = []): void
    {
        Http::fake([
            '*/act_777/campaigns*' => Http::response(['data' => [
                ['id' => '1201', 'name' => 'Italy vacation packages', 'status' => 'PAUSED', 'objective' => 'OUTCOME_SALES'],
                ['id' => '1202', 'name' => 'Walk in tub guide', 'status' => 'ACTIVE', 'objective' => 'OUTCOME_LEADS'],
            ]]),
            '*/1201/adsets*' => Http::response(['data' => [[
                'id' => '990',
                'name' => 'Italy vacation packages',
                'daily_budget' => '2500',
                'bid_strategy' => 'COST_CAP',
                'bid_amount' => 50,
                'optimization_goal' => 'OFFSITE_CONVERSIONS',
                'start_time' => '2026-04-22T12:59:03+0000',
                'targeting' => [
                    'geo_locations' => ['countries' => ['US', 'GB']],
                    'publisher_platforms' => ['facebook', 'instagram'],
                ],
                'promoted_object' => ['pixel_id' => '1452001359666123'],
            ]]]),
            '*/1201*' => Http::response([
                'id' => '1201',
                'name' => 'Italy vacation packages',
                'status' => 'PAUSED',
                'objective' => 'OUTCOME_SALES',
                'account_id' => '777',
            ]),
            ...$overrides,
        ]);
    }

    private function runTool(string $name, array $arguments = []): string
    {
        $tool = collect(app(ToolRegistry::class)->resolve())->first(fn ($t) => $t->name() === $name);

        $this->assertNotNull($tool, "Tool {$name} is not registered.");

        return (string) $tool->handle(new Request($arguments));
    }

    public function test_campaigns_built_elsewhere_can_be_listed(): void
    {
        $this->fakeMeta();

        $result = $this->runTool('campaign__list_campaigns', ['ad_account_id' => 'act_777']);

        $this->assertStringContainsString('Italy vacation packages', $result);
        $this->assertStringContainsString('1201', $result);
    }

    /**
     * The conversion that would otherwise show up on an invoice.
     *
     * Meta states a budget in minor units as a string, so "2500" is $25. Taken
     * at face value it becomes a budget a hundred times too large.
     */
    public function test_money_is_read_as_minor_units(): void
    {
        $this->fakeMeta();

        $campaign = app(CampaignImporter::class)->adopt('1201', $this->user);

        $this->assertSame('25.00', (string) $campaign->budget);
        $this->assertSame('0.50', (string) $campaign->bid_amount);
    }

    public function test_opening_records_the_ids_needed_to_push_a_change(): void
    {
        $this->fakeMeta();

        $campaign = app(CampaignImporter::class)->adopt('1201', $this->user);

        $this->assertSame('1201', $campaign->external_campaign_id);
        $this->assertSame('990', $campaign->external_ids['adset_id']);
        $this->assertSame('published', $campaign->status);
        $this->assertSame('act_777', $campaign->ad_account_id);
        $this->assertSame(['US', 'GB'], $campaign->locations['countries']);
        $this->assertSame(['facebook', 'instagram'], $campaign->placements);
        $this->assertSame('1452001359666123', $campaign->pixel_id);
    }

    /**
     * The baseline is the whole point.
     *
     * Without published_state the first edit would diff against nothing and
     * report every field as changed.
     */
    public function test_an_opened_campaign_starts_with_nothing_pending(): void
    {
        $this->fakeMeta();

        $campaign = app(CampaignImporter::class)->adopt('1201', $this->user);

        $this->assertNotNull($campaign->published_state);
        $this->assertSame([], app(PendingChanges::class)->updatable($campaign));
    }

    /** And then an edit shows exactly one change. */
    public function test_editing_an_existing_campaign_shows_only_what_changed(): void
    {
        $this->fakeMeta();

        $campaign = app(CampaignImporter::class)->adopt('1201', $this->user);
        $campaign->update(['budget' => 40]);

        $pending = app(PendingChanges::class)->updatable($campaign->fresh());

        $this->assertSame(['budget'], array_keys($pending));
        $this->assertSame('25.00', (string) $pending['budget']['from']);
        $this->assertSame('40.00', (string) $pending['budget']['to']);
    }

    /** Opening twice must not make a second row fighting over the same objects. */
    public function test_opening_the_same_campaign_twice_returns_the_same_row(): void
    {
        $this->fakeMeta();

        $first = app(CampaignImporter::class)->adopt('1201', $this->user);
        $second = app(CampaignImporter::class)->adopt('1201', $this->user);

        $this->assertSame($first->id, $second->id);
        $this->assertSame(1, Campaign::count());
    }

    public function test_opening_focuses_it_so_the_editing_tools_apply(): void
    {
        $this->fakeMeta();

        $this->runTool('campaign__open_campaign', ['campaign_id' => '1201']);

        $focus = app(Workspace::class)->focus();

        $this->assertInstanceOf(Campaign::class, $focus);
        $this->assertSame('1201', $focus->external_campaign_id);
    }

    /**
     * The thing that makes open access acceptable.
     *
     * Everyone here can edit everyone's campaigns, so every change has to carry
     * a name. The audit columns existed but recorded "agent" and "chat", which
     * name the channel and answer nothing when someone asks who moved a budget.
     */
    public function test_a_pushed_change_records_who_made_it(): void
    {
        $this->fakeMeta([
            '*/990*' => Http::response(['success' => true]),
        ]);

        $this->runTool('campaign__open_campaign', ['campaign_id' => '1201']);
        $campaign = Campaign::firstWhere('external_campaign_id', '1201');
        $campaign->update(['budget' => 40]);

        $result = $this->runTool('campaign__push_changes');

        $change = CampaignChange::latest('id')->first();

        $this->assertNotNull($change, 'The push should have recorded a change. Tool said: '.$result);
        $this->assertStringContainsString($this->user->email, (string) $change->actor);
        $this->assertStringContainsString('#'.$this->user->id, (string) $change->actor);
    }

    /** And the platform call carries the same name. */
    public function test_the_platform_call_records_who_made_it(): void
    {
        $this->fakeMeta([
            '*/990*' => Http::response(['success' => true]),
        ]);

        $this->runTool('campaign__open_campaign', ['campaign_id' => '1201']);
        Campaign::firstWhere('external_campaign_id', '1201')->update(['budget' => 40]);

        $this->runTool('campaign__push_changes');

        $write = ApiCall::where('mutating', true)->latest('id')->first();

        $this->assertNotNull($write, 'The push should have recorded a platform call.');
        $this->assertStringContainsString($this->user->email, (string) $write->actor);
    }

    /** Opening must never change anything on the platform. */
    public function test_opening_writes_nothing_to_the_platform(): void
    {
        $this->fakeMeta();

        $this->runTool('campaign__open_campaign', ['campaign_id' => '1201']);

        // assertNothingSent() ignores a closure, so it would assert no calls at
        // all were made. Adoption reads, so what matters is that none of them wrote.
        Http::assertNotSent(fn ($request): bool => $request->method() !== 'GET');
    }
}
