<?php

namespace Tests\Feature;

use App\Agent\Answers;
use App\Agent\Choices;
use App\Agent\MarketingAgent;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\PublishGate;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * One budget, divided between the platforms it runs on.
 *
 * The brief holds the total the buyer named; each campaign holds its share,
 * which is what the publish gate, the ceiling and the platform floor already
 * read, so none of them needed changing.
 *
 * What these pin: nothing is divided without being asked, a split that cannot
 * run is refused where it is proposed, and refusing leaves the money exactly
 * as it was rather than half applied.
 */
class BudgetSplitTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

    protected Brief $brief;

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

        MarketingAgent::fake(['Noted.']);

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

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

        // Headroom, so these exercise the split rather than the ceiling. The
        // ceiling has its own test below, which sets its own limit.
        config(['platforms.guardrails.max_daily_budget' => 100]);

        $this->brief = Brief::create([
            'user_id' => $this->user->id,
            'conversation_id' => 'conv-split-0001',
            'budget' => 50,
            'budget_mode' => 'daily',
            'currency' => 'USD',
        ]);
    }

    private function campaign(string $platform): Campaign
    {
        return Campaign::create([
            'user_id' => $this->user->id,
            'brief_id' => $this->brief->id,
            'name' => ucfirst($platform).' campaign',
            'platform' => $platform,
            'status' => 'draft',
        ]);
    }

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

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

        return json_decode((string) $resolved->handle(new Request($arguments)), true);
    }

    private function twoPlatforms(): void
    {
        $meta = $this->campaign('meta');
        $this->campaign('google');

        app(Workspace::class)->focusOn($meta->fresh());
    }

    public function test_the_splits_are_offered_rather_than_applied(): void
    {
        $this->twoPlatforms();

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

        $this->assertTrue($result['ok']);

        $offered = collect(app(Choices::class)->all())->firstWhere('field', 'budget_split');
        $this->assertNotNull($offered);
        $this->assertContains('even', array_column($offered['options'], 'value'));

        // Nothing moved just by asking.
        $this->assertNull(Campaign::where('platform', 'meta')->first()->getRawOriginal('budget'));
    }

    public function test_an_even_split_divides_the_total(): void
    {
        $this->twoPlatforms();

        $result = $this->runTool('split_budget', ['split' => 'even']);

        $this->assertTrue($result['ok']);
        $this->assertEquals(25, Campaign::where('platform', 'meta')->first()->budget);
        $this->assertEquals(25, Campaign::where('platform', 'google')->first()->budget);
    }

    /** The shares have to add back up to what the buyer named. */
    public function test_a_split_that_does_not_divide_evenly_still_totals_correctly(): void
    {
        $this->brief->update(['budget' => 50.01]);
        $this->twoPlatforms();

        $this->runTool('split_budget', ['split' => 'even']);

        $total = Campaign::all()->sum(fn (Campaign $c) => (float) $c->getRawOriginal('budget'));

        $this->assertEqualsWithDelta(50.01, $total, 0.001);
    }

    public function test_explicit_shares_are_written_as_given(): void
    {
        $this->twoPlatforms();

        $this->runTool('split_budget', ['shares' => ['meta' => 30, 'google' => 20]]);

        $this->assertEquals(30, Campaign::where('platform', 'meta')->first()->budget);
        $this->assertEquals(20, Campaign::where('platform', 'google')->first()->budget);
    }

    /**
     * A share below the platform's own floor cannot run.
     *
     * Refused here rather than at publish, when the user has stopped looking.
     *
     * Meta is the short share, not Google. Google enforces no daily minimum, so
     * it cannot demonstrate a floor; it used to, because it had been given
     * Meta's 5.00 and the product stated it as Google's own rule.
     */
    public function test_a_share_below_the_platform_floor_is_refused(): void
    {
        $this->twoPlatforms();

        $result = $this->runTool('split_budget', ['shares' => ['meta' => 2, 'google' => 48]]);

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

    /** Refusing must leave the money exactly as it was. */
    public function test_a_refused_split_changes_nothing(): void
    {
        $this->twoPlatforms();

        $this->runTool('split_budget', ['shares' => ['meta' => 2, 'google' => 48]]);

        foreach (Campaign::all() as $campaign) {
            $this->assertNull($campaign->getRawOriginal('budget'), 'A refused split was half applied.');
        }
    }

    public function test_a_share_above_the_product_ceiling_is_refused(): void
    {
        config(['platforms.guardrails.max_daily_budget' => 20]);

        $this->twoPlatforms();

        $result = $this->runTool('split_budget', ['shares' => ['meta' => 45, 'google' => 5]]);

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

    public function test_a_platform_left_out_is_refused_rather_than_guessed(): void
    {
        $this->twoPlatforms();

        $result = $this->runTool('split_budget', ['shares' => ['meta' => 50]]);

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

    /** Splitting is meaningless with one platform, and says so. */
    public function test_a_single_platform_is_told_to_use_the_ordinary_budget_tool(): void
    {
        $only = $this->campaign('meta');
        app(Workspace::class)->focusOn($only->fresh());

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

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

    /** Clicking the suggestion applies it here, not through the model. */
    public function test_the_clicked_split_applies_server_side(): void
    {
        $this->twoPlatforms();

        app(Answers::class)->applyAll([['field' => 'budget_split', 'value' => 'even']]);

        $this->assertEquals(25, Campaign::where('platform', 'meta')->first()->budget);
    }

    /**
     * With several platforms the amount is the total, and it lands on the brief.
     *
     * Writing it to the campaign would mean each platform spending the whole
     * budget, which is the opposite of what was agreed.
     */
    public function test_the_budget_tool_records_the_total_on_the_brief(): void
    {
        $this->twoPlatforms();

        $result = $this->runTool('set_campaign_budget', ['amount' => 40, 'mode' => 'daily', 'level' => 'adset']);

        $this->assertTrue($result['ok']);
        $this->assertTrue($result['is_total_across_platforms']);
        $this->assertEquals(40, $this->brief->fresh()->budget);
        $this->assertNull(Campaign::where('platform', 'meta')->first()->getRawOriginal('budget'));
    }

    /**
     * Naming every share names the total, so nothing is left blocked.
     *
     * The shares were written and the brief kept whatever total had been
     * recorded before, and PublishGate refuses a brief whose shares exceed its
     * total. So "10 and 10 and 10" against a recorded total of 10 was accepted
     * here, reported as a discrepancy, and then refused at publish with nothing
     * the buyer could act on: no tool asks for the total again and each share
     * was perfectly valid. Seen on arb-dev on 25 Sep.
     */
    public function test_explicit_shares_bring_the_brief_total_with_them(): void
    {
        $this->brief->update(['budget' => 10]);
        $this->twoPlatforms();

        $this->runTool('split_budget', ['shares' => ['meta' => 10, 'google' => 10]]);

        $this->assertEquals(20, $this->brief->refresh()->budget);
    }

    /** And the publish gate is satisfied afterwards, which is the point. */
    public function test_the_gate_no_longer_blocks_after_an_explicit_split(): void
    {
        $this->brief->update(['budget' => 10]);
        $this->twoPlatforms();

        $this->runTool('split_budget', ['shares' => ['meta' => 10, 'google' => 10]]);

        $problems = json_encode(app(PublishGate::class)
            ->check($this->brief->refresh()->campaigns()->first()));

        $this->assertStringNotContainsString('add up to', $problems);
    }

    /** The change is said out loud rather than made quietly. */
    public function test_the_restated_total_is_reported(): void
    {
        $this->brief->update(['budget' => 10]);
        $this->twoPlatforms();

        $result = $this->runTool('split_budget', ['shares' => ['meta' => 10, 'google' => 10]]);

        $this->assertStringContainsString('brief now records 20.00', $result['note']);
    }

    /** A split that already matches says nothing and changes nothing. */
    public function test_a_split_that_matches_the_total_is_not_announced(): void
    {
        $this->brief->update(['budget' => 20]);
        $this->twoPlatforms();

        $result = $this->runTool('split_budget', ['shares' => ['meta' => 10, 'google' => 10]]);

        $this->assertArrayNotHasKey('note', $result);
        $this->assertEquals(20, $this->brief->refresh()->budget);
    }

    /** A refused split still changes nothing, including the total. */
    public function test_a_refused_split_leaves_the_total_alone(): void
    {
        $this->brief->update(['budget' => 10]);
        $this->twoPlatforms();

        $this->runTool('split_budget', ['shares' => ['meta' => 10, 'google' => 0]]);

        $this->assertEquals(10, $this->brief->refresh()->budget);
    }
}
