<?php

namespace Tests\Feature;

use App\Campaigns\Publishing\MetaPublisher;
use App\Models\Asset;
use App\Models\AssetPlatformRef;
use App\Models\Campaign;
use App\Models\CampaignAd;
use App\Models\User;
use App\Support\CurrencyMinorUnits;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request as ClientRequest;
use Illuminate\Support\Facades\Http;
use Tests\SeedsVerifiedCountries;
use Tests\TestCase;

/**
 * A budget reaches Meta in the currency's own minor units.
 *
 * The conversion was a hardcoded multiply by 100 in six places. That is right
 * for the dollar and wrong for the yen, which has no subunit: a ¥3,000 daily
 * budget was sent as 300000, which Meta reads as ¥300,000. A hundred times what
 * the buyer approved, and a hundred times what BudgetCeiling had just checked -
 * and that class is currency-aware on purpose, naming yen accounts in its own
 * docblock, so this was a supported state rather than a hypothetical.
 *
 * It hid because both paths that read the figure back also divided by 100:
 * AdsMcp's audit line reported "3,000.00 a day" and CampaignImporter reported
 * the same on a re-read, so every record inside ARB agreed with the buyer while
 * the platform held a hundred times it.
 *
 * VerifyAdAccount writes campaign.currency straight from Meta's own account
 * record, so the currency is known before a budget is ever set.
 */
class AYenBudgetIsNotSpentAHundredTimesOverTest extends TestCase
{
    use RefreshDatabase;
    use SeedsVerifiedCountries;

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

        $this->seedVerifiedCountries();

        config([
            'platforms.meta.token' => 'test-token',
            'platforms.guardrails.allowed_ad_accounts' => ['act_111'],
        ]);

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

        Http::fake([
            '*/act_111/campaigns' => Http::response(['id' => 'camp-1']),
            '*/act_111/adsets' => Http::response(['id' => 'adset-1']),
            '*/act_111/adcreatives' => Http::response(['id' => 'creative-1']),
            '*/act_111/ads' => Http::response(['id' => 'ad-1']),
            '*/act_111*' => Http::response([
                'id' => 'act_111', 'account_status' => 1, 'currency' => 'JPY',
            ]),
            '*' => Http::response(['success' => true]),
        ]);
    }

    private function publish(string $currency, float $budget, ?float $bid = null): void
    {
        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_TRAFFIC',
            'ad_account_id' => 'act_111',
            'page_id' => 'page-1',
            'landing_url' => 'https://example.com',
            'budget' => $budget,
            'budget_mode' => 'daily',
            'budget_level' => 'adset',
            'currency' => $currency,
            'bid_amount' => $bid,
            'bid_strategy' => $bid ? 'COST_CAP' : null,
            'locations' => ['countries' => ['JP']],
            'placements' => ['facebook'],
        ]);

        $asset = Asset::create([
            'owner_type' => auth()->user()->getMorphClass(),
            'owner_id' => auth()->id(),
            'kind' => 'image', 'source' => 'generated', 'disk' => 'public',
            'path' => 'creatives/x.png', 'original_name' => 'x.png',
            'mime' => 'image/png', 'bytes' => 10, 'checksum' => 'sum',
        ]);

        AssetPlatformRef::create([
            'asset_id' => $asset->id, 'platform' => 'meta',
            'ad_account_id' => 'act_111', 'external_hash' => 'HASH123',
        ]);

        CampaignAd::create([
            'campaign_id' => $campaign->id,
            'headline' => 'Winter Sale',
            'primary_text' => 'Everything reduced.',
            'cta' => 'SHOP_NOW',
            'asset_id' => $asset->id,
        ]);

        $created = [];

        try {
            app(MetaPublisher::class)->create($campaign, 'tester', $created);
        } catch (\Throwable) {
            // The payload that went out is what is asserted.
        }
    }

    /** The body sent to one endpoint, matched on the whole path. */
    private function sentTo(string $path): ?array
    {
        foreach (Http::recorded() as [$request]) {
            /** @var ClientRequest $request */
            if ($request->method() === 'POST'
                && str_ends_with(rtrim((string) parse_url($request->url(), PHP_URL_PATH), '/'), $path)) {
                return $request->data();
            }
        }

        return null;
    }

    /** The defect: 3000 yen must not leave as 300000. */
    public function test_a_yen_budget_reaches_meta_unmultiplied(): void
    {
        $this->publish('JPY', 3000);

        $sent = $this->sentTo('/act_111/adsets');

        $this->assertNotNull($sent, 'Nothing was sent to the adsets endpoint.');
        $this->assertSame('3000', (string) ($sent['daily_budget'] ?? null), 'the yen budget was multiplied by 100');
    }

    /** And the dollar still goes out in cents, which was never wrong. */
    public function test_a_dollar_budget_still_reaches_meta_in_cents(): void
    {
        $this->publish('USD', 30);

        $this->assertSame('3000', (string) ($this->sentTo('/act_111/adsets')['daily_budget'] ?? null));
    }

    /** The bid travels the same road and had the same bug. */
    public function test_a_yen_bid_reaches_meta_unmultiplied(): void
    {
        $this->publish('JPY', 3000, 500);

        $this->assertSame('500', (string) ($this->sentTo('/act_111/adsets')['bid_amount'] ?? null));
    }

    /**
     * A three-decimal currency is not two either.
     *
     * The dinar has a thousand fils, so the same hardcoded 100 sent a tenth of
     * the budget rather than a hundred times it. Underspending is quieter than
     * overspending and no less wrong.
     */
    public function test_a_three_decimal_currency_uses_a_thousand(): void
    {
        $this->publish('KWD', 30);

        $this->assertSame('30000', (string) ($this->sentTo('/act_111/adsets')['daily_budget'] ?? null));
    }

    /** The conversion round-trips, which is what the audit line and re-import rely on. */
    public function test_the_conversion_survives_a_round_trip(): void
    {
        foreach (['JPY' => 3000.0, 'USD' => 30.0, 'KWD' => 30.0] as $currency => $amount) {
            $minor = CurrencyMinorUnits::toMinor($amount, $currency);

            $this->assertSame(
                $amount,
                CurrencyMinorUnits::toMajor($minor, $currency),
                "{$currency} did not survive the round trip",
            );
        }
    }

    /** An unknown currency keeps the two-decimal assumption this code always made. */
    public function test_an_unknown_currency_falls_back_to_two_decimals(): void
    {
        $this->assertSame(100, CurrencyMinorUnits::factor('ZZZ'));
        $this->assertSame(100, CurrencyMinorUnits::factor(null));
    }
}
