<?php

namespace Tests\Feature;

use App\Agent\MarketingAgent;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use App\Support\CurrencyMinorUnits;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Dividing a budget produces amounts the currency can actually express.
 *
 * The money path was corrected to convert by the currency's own exponent, and
 * every send and read went through it - but the one place that does arithmetic
 * on money was missed. split_budget floored to two decimals with a hardcoded
 * factor of 100, so a three-way split of 1000 JPY became 333.33 each. There is
 * no such amount of yen: toMinor() rounds each share to 333, the campaigns hold
 * 999 between them, and the brief still records 1000.
 *
 * That matters more than a rounding error, because the shares adding back up to
 * the total is the one guarantee this tool makes - its own docblock says a penny
 * is not worth explaining but a total that does not reconcile is - and
 * PublishGate refuses a brief whose shares exceed its total.
 *
 * Three currencies, because the two-decimal case is the one that already worked
 * and must keep working, and the three-decimal case fails in the opposite
 * direction: floor to 2 there throws away fils that are real money.
 */
class AYenSplitStillAddsUpTest extends TestCase
{
    use RefreshDatabase;

    private function splitThreeWays(string $currency, float $total): array
    {
        MarketingAgent::fake(['Noted.']);

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

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

        // Well clear of the ceiling: this is about the arithmetic, not the limit.
        config(['platforms.guardrails.max_daily_budget' => 10_000_000]);

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

        $campaigns = collect(['meta', 'google', 'linkedin'])->map(fn (string $platform): Campaign => Campaign::create([
            'user_id' => $user->id,
            'brief_id' => $brief->id,
            'name' => ucfirst($platform).' campaign',
            'platform' => $platform,
            'status' => 'draft',
            'currency' => $currency,
        ]));

        $workspace->focusOn($campaigns->first()->fresh());

        $result = collect(app(ToolRegistry::class)->resolve())
            ->first(fn ($t) => $t->name() === 'campaign__split_budget')
            ->handle(new Request(['split' => 'even']));

        return [json_decode((string) $result, true), $brief->fresh(), $campaigns];
    }

    /**
     * The defect: 1000 JPY three ways.
     *
     * Each share must be a whole yen, and the three must still be 1000.
     */
    public function test_a_yen_split_is_whole_yen_and_reconciles(): void
    {
        [$result, $brief, $campaigns] = $this->splitThreeWays('JPY', 1000);

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

        $shares = $campaigns->map(fn (Campaign $c): float => (float) $c->fresh()->budget);

        foreach ($shares as $share) {
            $this->assertSame(
                $share,
                floor($share),
                "{$share} is not a whole number of yen, and the platform will round it away",
            );
        }

        $this->assertSame(1000.0, (float) $shares->sum(), 'the shares no longer add up to the total');
    }

    /** And what reaches Meta is the same total, in minor units. */
    public function test_the_minor_units_sent_also_reconcile(): void
    {
        [, , $campaigns] = $this->splitThreeWays('JPY', 1000);

        $sent = $campaigns->sum(fn (Campaign $c): int => $c->fresh()->budgetInMinorUnits());

        $this->assertSame(1000, $sent, 'the amounts actually sent do not add up to the approved total');
    }

    /** The dollar case, which already worked, still does. */
    public function test_a_dollar_split_is_unchanged(): void
    {
        [$result, , $campaigns] = $this->splitThreeWays('USD', 100);

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

        $shares = $campaigns->map(fn (Campaign $c): float => (float) $c->fresh()->budget)->sort()->values();

        $this->assertSame([33.33, 33.33, 33.34], $shares->all());
    }

    /**
     * A three-decimal currency reconciles, and loses its third decimal.
     *
     * Both halves are the point. Rounding a dinar share to its own precision
     * gives 33.333, but `campaigns.budget` is decimal(12,2) with a decimal:2
     * cast, so the write stores 33.33 and three of those are 99.99 - dividing to
     * a precision the column cannot keep breaks reconciliation, which is the one
     * guarantee this tool makes. So the split is capped at two decimals and the
     * total still holds.
     *
     * The cost is recorded here rather than hidden: three thousandths of a dinar
     * per share is real money, and the fix is decimal(12,3) on both budget
     * columns, which belongs with the money path and not with a splitter. When
     * someone widens the column, this test is what tells them to come back.
     */
    public function test_a_dinar_split_reconciles_but_is_capped_by_the_column(): void
    {
        [$result, , $campaigns] = $this->splitThreeWays('KWD', 100);

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

        $shares = $campaigns->map(fn (Campaign $c): float => (float) $c->fresh()->budget);

        $this->assertSame(100.0, (float) $shares->sum(), 'the shares no longer add up to the total');

        // The known limit, asserted so it cannot change unnoticed.
        foreach ($shares as $share) {
            $this->assertSame(
                $share,
                round($share, 2),
                'a third decimal survived the split, which decimal(12,2) cannot store',
            );
        }
    }

    /** The helper this rests on, stated directly. */
    public function test_the_currency_decides_the_precision(): void
    {
        $this->assertSame(0, CurrencyMinorUnits::decimals('JPY'));
        $this->assertSame(2, CurrencyMinorUnits::decimals('USD'));
        $this->assertSame(3, CurrencyMinorUnits::decimals('KWD'));
        $this->assertSame(2, CurrencyMinorUnits::decimals(null));

        $this->assertSame(333.0, CurrencyMinorUnits::round(333.33, 'JPY'));
        $this->assertSame(33.333, CurrencyMinorUnits::round(33.3333, 'KWD'));

        // The helper states the currency's own fact. SplitBudget clamps it to
        // what the column holds; see the dinar case above.
        $this->assertSame(3, CurrencyMinorUnits::decimals('BHD'));
    }
}
