<?php

namespace Tests\Feature;

use App\Models\ApiCall;
use App\Models\User;
use App\Services\Meta\Meta;
use App\Services\Meta\MetaException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * The hourly call budget, which exists so other systems on the same token keep
 * their share.
 *
 * Two holes in it. It was asserted once before the retry loop, though every
 * retry is a real call that writes its own row, so a throttled burst could take
 * the budget past its limit while the check that prevents exactly that had
 * already passed. And it returned early for any call that named no ad account,
 * which is the two edges that sweep everything the token can see.
 */
class GuardrailBudgetTest extends TestCase
{
    use RefreshDatabase;

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

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

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

    private function spend(int $calls, ?string $account): void
    {
        for ($i = 0; $i < $calls; $i++) {
            ApiCall::create([
                'transport' => 'graph',
                'method' => 'GET',
                'endpoint' => 'probe',
                'ad_account_id' => $account,
                'mutating' => false,
                'request' => [],
                'ok' => true,
            ]);
        }
    }

    public function test_an_account_over_its_budget_is_refused(): void
    {
        $this->spend(3, 'act_777');

        Http::fake(['*' => Http::response(['id' => 'act_777'])]);

        $this->expectException(MetaException::class);
        $this->expectExceptionMessage('hourly call budget');

        app(Meta::class)->adAccount('act_777');
    }

    /**
     * Calls that name no account are budgeted too.
     *
     * Listing Pages and listing ad accounts are both account-less, and they are
     * the two that sweep everything the token can see, so they were the two
     * nothing bounded.
     */
    public function test_calls_naming_no_account_are_budgeted(): void
    {
        $this->spend(3, null);

        Http::fake(['*' => Http::response(['data' => []])]);

        $this->expectException(MetaException::class);
        $this->expectExceptionMessage('calls that name no ad account');

        app(Meta::class)->pages();
    }

    /** The two buckets are separate, so one cannot starve the other. */
    public function test_an_account_budget_is_not_spent_by_account_less_calls(): void
    {
        $this->spend(3, null);

        Http::fake(['*' => Http::response(['id' => 'act_777', 'name' => 'Fine'])]);

        $this->assertSame('act_777', app(Meta::class)->adAccount('act_777')['id']);
    }

    /**
     * Retries count against the budget as they happen.
     *
     * Each attempt is a real call and writes its own row. Checked once up
     * front, a burst of throttles could overshoot the limit by the retry count.
     */
    public function test_a_retry_is_refused_once_the_budget_runs_out_mid_call(): void
    {
        $this->spend(2, 'act_777');

        // Throttled, so the logger retries; the second attempt takes the count
        // to the limit and the third must not happen.
        Http::fake(['*' => Http::response(['error' => ['message' => 'too many calls', 'code' => 17]], 400)]);

        try {
            app(Meta::class)->adAccount('act_777');
        } catch (MetaException $e) {
            $this->assertStringContainsString('hourly call budget', $e->getMessage());

            $this->assertSame(
                3,
                ApiCall::where('ad_account_id', 'act_777')->count(),
                'the budget should stop the run at the limit, not past it',
            );

            return;
        }

        $this->fail('a throttled call over budget should have been refused');
    }
}
