<?php

namespace Tests\Feature;

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

/**
 * The same read, asked for twice in one turn, is made once.
 *
 * Measured on dev from the audit trail: 86 of 306 non-mutating calls were exact
 * repeats within ten seconds, costing 34.8 seconds of somebody's turn. One ad
 * account was read seven times while building a single campaign, because every
 * tool that needs its name or currency fetches it and none of them knew another
 * already had.
 *
 * This is deduplication, not caching. An ad account read twice a second apart
 * cannot legitimately have changed, and nothing is held between turns:
 * CallLogger is scoped, so a Page created between two messages still appears on
 * the next one. Deciding how stale a Page list may be is a different question
 * and is not being answered here.
 */
class RepeatedReadsAreNotRepeatedTest extends TestCase
{
    use RefreshDatabase;

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

        config(['platforms.guardrails.allowed_ad_accounts' => ['act_1']]);
    }

    private function read(string $endpoint, array $query = []): CallContext
    {
        return new CallContext(
            transport: 'graph',
            method: 'GET',
            endpoint: $endpoint,
            adAccountId: 'act_1',
            request: $query,
        );
    }

    public function test_the_same_read_reaches_the_platform_once(): void
    {
        $logger = app(CallLogger::class);
        $calls = 0;

        $make = function () use (&$calls): array {
            $calls++;

            return ['id' => 'act_1', 'name' => 'StockPrices'];
        };

        $first = $logger->record($this->read('act_1'), $make);
        $second = $logger->record($this->read('act_1'), $make);

        $this->assertSame(1, $calls, 'The account was fetched twice in one turn.');
        $this->assertSame($first, $second, 'The repeat returned something different.');
    }

    /** And the trail records what reached the platform, not what was asked. */
    public function test_a_repeat_writes_no_audit_row(): void
    {
        $logger = app(CallLogger::class);

        $logger->record($this->read('act_1'), fn (): array => ['ok' => true]);
        $logger->record($this->read('act_1'), fn (): array => ['ok' => true]);

        $this->assertSame(1, ApiCall::query()->count());
    }

    /** Different arguments are a different question. */
    public function test_a_different_query_is_fetched_again(): void
    {
        $logger = app(CallLogger::class);
        $calls = 0;

        $make = function () use (&$calls): array {
            $calls++;

            return ['data' => []];
        };

        $logger->record($this->read('me/accounts', ['limit' => 50]), $make);
        $logger->record($this->read('me/accounts', ['limit' => 10]), $make);

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

    /**
     * Writes are never deduplicated.
     *
     * Two identical writes are two intentions. Collapsing them would silently
     * drop the second, which is the opposite of the double-publish guard's job
     * and would hide it rather than refuse it.
     */
    public function test_two_identical_writes_both_happen(): void
    {
        $logger = app(CallLogger::class);
        $calls = 0;

        $write = new CallContext(
            transport: 'graph',
            method: 'POST',
            endpoint: 'act_1/campaigns',
            adAccountId: 'act_1',
            request: ['name' => 'One'],
            mutating: true,
        );

        $make = function () use (&$calls): array {
            $calls++;

            return ['id' => '1'];
        };

        $logger->record($write, $make);
        $logger->record($write, $make);

        $this->assertSame(2, $calls, 'A second write was swallowed.');
        $this->assertSame(2, ApiCall::query()->count());
    }

    /** A fresh turn is a fresh logger, so nothing carries over. */
    public function test_nothing_is_held_between_turns(): void
    {
        $calls = 0;

        $make = function () use (&$calls): array {
            $calls++;

            return ['ok' => true];
        };

        app(CallLogger::class)->record($this->read('act_1'), $make);

        // What Laravel does between requests for a scoped binding.
        app()->forgetScopedInstances();

        app(CallLogger::class)->record($this->read('act_1'), $make);

        $this->assertSame(2, $calls, 'A read was held across turns, so new data would never appear.');
    }
}
