<?php

namespace Tests\Feature;

use App\Models\ApiCall;
use App\Services\Meta\MetaException;
use App\Services\Platforms\Support\CallContext;
use App\Services\Platforms\Support\CallLogger;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * Trying again when Meta says "not now".
 *
 * Meta's ads-management limit blocked account enumeration for hours during
 * testing, and a throttled turn died outright rather than waiting. A throttle
 * is an explicit refusal, so repeating one cannot create a second campaign.
 *
 * A timeout is a different thing entirely and is treated as such: the reply may
 * have been lost after the work was done, so a write that times out is never
 * repeated. Same reasoning as the double publish guard.
 */
class RateLimitRetryTest extends TestCase
{
    use RefreshDatabase;

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

        config([
            'platforms.guardrails.allowed_ad_accounts' => ['act_777'],
            // No point sitting through the real backoff to prove the count.
            'platforms.guardrails.retry_backoff_ms' => 0,
        ]);
    }

    private function context(bool $mutating = false): CallContext
    {
        return new CallContext('graph', 'POST', 'act_777/campaigns', 'act_777', [], mutating: $mutating);
    }

    /** The exact error that blocked us, as Meta words it. */
    public function test_a_throttled_read_is_tried_again(): void
    {
        $calls = 0;

        $result = app(CallLogger::class)->record($this->context(), function () use (&$calls): array {
            $calls++;

            if ($calls < 3) {
                throw new MetaException(
                    'There have been too many calls to this ad-account. Wait a bit and try again '
                    .'[OAuthException 80004/2446079] on me/adaccounts'
                );
            }

            return ['id' => 'act_777'];
        });

        $this->assertSame(['id' => 'act_777'], $result);
        $this->assertSame(3, $calls);
    }

    /**
     * A refusal is safe to repeat even for a write, because nothing was done.
     */
    public function test_a_throttled_write_is_tried_again(): void
    {
        $calls = 0;

        app(CallLogger::class)->record($this->context(mutating: true), function () use (&$calls): array {
            $calls++;

            if ($calls < 2) {
                throw new MetaException('User request limit reached [OAuthException 17] on act_777/campaigns');
            }

            return ['id' => 'camp-1'];
        });

        $this->assertSame(2, $calls);
    }

    /**
     * The case that must never be repeated.
     *
     * A write that times out may already have created the campaign, and the
     * reply was simply lost. Repeating it is how one campaign becomes two.
     */
    public function test_a_write_that_times_out_is_never_repeated(): void
    {
        $calls = 0;

        try {
            app(CallLogger::class)->record($this->context(mutating: true), function () use (&$calls): array {
                $calls++;

                throw new MetaException('cURL error 28: Operation timed out after 120000 milliseconds');
            });
        } catch (MetaException) {
            // expected
        }

        $this->assertSame(1, $calls, 'A timed-out write must not be sent twice.');
    }

    /** A read that times out changed nothing, so it can go again. */
    public function test_a_read_that_times_out_is_tried_again(): void
    {
        $calls = 0;

        try {
            app(CallLogger::class)->record($this->context(), function () use (&$calls): array {
                $calls++;

                throw new MetaException('cURL error 28: Operation timed out after 120000 milliseconds');
            });
        } catch (MetaException) {
            // expected
        }

        $this->assertSame(3, $calls);
    }

    /** An ordinary rejection is not worth repeating; it will be rejected again. */
    public function test_a_real_error_is_not_retried(): void
    {
        $calls = 0;

        try {
            app(CallLogger::class)->record($this->context(mutating: true), function () use (&$calls): array {
                $calls++;

                throw new MetaException('Invalid image hash [OAuthException 100] on act_777/adcreatives');
            });
        } catch (MetaException) {
            // expected
        }

        $this->assertSame(1, $calls);
    }

    public function test_it_gives_up_rather_than_retrying_forever(): void
    {
        $calls = 0;

        try {
            app(CallLogger::class)->record($this->context(), function () use (&$calls): array {
                $calls++;

                throw new MetaException('There have been too many calls to this ad-account [OAuthException 80004]');
            });
        } catch (MetaException) {
            // expected
        }

        $this->assertSame(3, $calls);
    }

    /**
     * Every attempt is a real call against Meta's quota, so every attempt is
     * recorded. An audit log that showed one row for three calls would
     * understate what we did to the account.
     */
    public function test_every_attempt_is_audited(): void
    {
        try {
            app(CallLogger::class)->record($this->context(), function (): array {
                throw new MetaException('Rate limit reached [OAuthException 613]');
            });
        } catch (MetaException) {
            // expected
        }

        $this->assertSame(3, ApiCall::count());
        $this->assertSame(0, ApiCall::where('ok', true)->count());
    }
}
