<?php

namespace Tests\Feature;

use App\Campaigns\CampaignImporter;
use App\Models\User;
use App\Services\Meta\MetaException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * An unpermitted account is not read on the way to being refused.
 *
 * The allow list was consulted, and it did refuse the adoption - but only after
 * the whole campaign had been read. A campaign is addressed by its own id, so
 * there is no account in the path for the guardrail to check beforehand, and the
 * conclusion drawn was that the read could not be helped.
 *
 * It could. The account id is one field. Reading name, objective, budget, bid
 * strategy, schedule and special ad categories on the way to discovering whose
 * campaign it is was a choice, and those values went into the response, into
 * CallLogger, and so into our own database as the body of a logged request -
 * another advertiser's campaign settings, stored here, because someone pasted an
 * id into the chat.
 *
 * What is pinned: the first request asks for account_id and nothing else, the
 * refusal happens before any second request, and the account written on the row
 * is the one that was checked.
 */
class NothingIsReadBeforePermissionTest extends TestCase
{
    use RefreshDatabase;

    private const CAMPAIGN = '120252047635070626';

    private const MINE = '1950320145707342';

    private const SOMEONE_ELSES = '1099887766554433';

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

        config(['platforms.meta.token' => 'test-token']);
    }

    /** Meta answers the account lookup, then everything else if asked. */
    private function metaHolds(string $accountId): void
    {
        Http::fake([
            '*/'.self::CAMPAIGN.'*' => Http::response([
                'id' => self::CAMPAIGN,
                'account_id' => $accountId,
                'name' => 'Their Q4 prospecting',
                'objective' => 'OUTCOME_SALES',
                'daily_budget' => '85000',
                'bid_strategy' => 'LOWEST_COST_WITH_BID_CAP',
                'start_time' => '2026-09-01T00:00:00+0000',
            ]),
            '*/adsets*' => Http::response(['data' => []]),
            '*' => Http::response(['data' => [], 'currency' => 'USD']),
        ]);
    }

    private function adopt(): void
    {
        app(CampaignImporter::class)->adopt(self::CAMPAIGN, User::factory()->create());
    }

    /** @return list<Request> */
    private function requests(): array
    {
        return Http::recorded()->map(fn (array $pair): Request => $pair[0])->all();
    }

    // ------------------------------------------------------------- the defect

    /** On an unpermitted account, the only field ever asked for is account_id. */
    public function test_an_unpermitted_campaign_is_never_read_beyond_its_account(): void
    {
        config(['platforms.guardrails.allowed_ad_accounts' => ['meta:act_'.self::MINE]]);
        $this->metaHolds(self::SOMEONE_ELSES);

        try {
            $this->adopt();
            $this->fail('adopting a campaign on an unpermitted account was allowed');
        } catch (MetaException) {
            // expected
        }

        $requests = $this->requests();

        $this->assertCount(1, $requests, 'more than one request was made before the refusal');
        $this->assertStringContainsString('fields=account_id', urldecode($requests[0]->url()));

        foreach (['daily_budget', 'bid_strategy', 'objective', 'special_ad_categories', 'start_time'] as $field) {
            $this->assertStringNotContainsString(
                $field,
                urldecode($requests[0]->url()),
                "{$field} was requested from an account we are not permitted to touch",
            );
        }
    }

    /** And nothing was written locally. */
    public function test_nothing_is_stored_for_an_unpermitted_campaign(): void
    {
        config(['platforms.guardrails.allowed_ad_accounts' => ['meta:act_'.self::MINE]]);
        $this->metaHolds(self::SOMEONE_ELSES);

        try {
            $this->adopt();
        } catch (MetaException) {
            // expected
        }

        $this->assertDatabaseCount('campaigns', 0);
    }

    // -------------------------------------------------------- the ordinary case

    /** A permitted campaign is read in full and adopted. */
    public function test_a_permitted_campaign_is_still_adopted(): void
    {
        config(['platforms.guardrails.allowed_ad_accounts' => ['meta:act_'.self::MINE]]);
        $this->metaHolds(self::MINE);

        $this->adopt();

        $this->assertDatabaseHas('campaigns', [
            'external_campaign_id' => self::CAMPAIGN,
            'ad_account_id' => 'act_'.self::MINE,
            'objective' => 'OUTCOME_SALES',
        ]);
    }

    /** The full read is scoped to the account, so later calls are vettable. */
    public function test_the_second_read_carries_the_account(): void
    {
        config(['platforms.guardrails.allowed_ad_accounts' => ['meta:act_'.self::MINE]]);
        $this->metaHolds(self::MINE);

        $this->adopt();

        $requests = $this->requests();

        $this->assertGreaterThan(1, count($requests), 'the campaign was never read in full');
        $this->assertStringContainsString('daily_budget', urldecode($requests[1]->url()));
    }

    /**
     * A wildcard still means every account, and still only one lookup first.
     *
     * The saving is not conditional on the allow list being narrow: the account
     * lookup is cheap and the ordering is what matters.
     */
    public function test_a_wildcard_permits_the_adoption(): void
    {
        config(['platforms.guardrails.allowed_ad_accounts' => ['meta:*']]);
        $this->metaHolds(self::SOMEONE_ELSES);

        $this->adopt();

        $this->assertDatabaseHas('campaigns', ['ad_account_id' => 'act_'.self::SOMEONE_ELSES]);
    }

    /** A campaign Meta does not know is a clear failure, not a null adoption. */
    public function test_an_unknown_campaign_is_refused(): void
    {
        config(['platforms.guardrails.allowed_ad_accounts' => ['meta:*']]);

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

        $this->expectExceptionMessage('no campaign');

        $this->adopt();
    }
}
