<?php

namespace Tests\Feature;

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

/**
 * The switch that stops everything reaching a platform.
 *
 * It only applies to calls marked as writes, and whether a call counted as one
 * was decided by whether its tool name contained "create". ads_update_entity
 * does not, so the call that moves a live campaign's budget was recorded as a
 * read and ran with the switch on. Someone could set it, believe writes had
 * stopped, and keep changing budgets on live campaigns.
 */
class KillSwitchTest extends TestCase
{
    use RefreshDatabase;

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

        config([
            'platforms.guardrails.allowed_ad_accounts' => ['act_777'],
            'platforms.guardrails.paused' => true,
            'platforms.meta.mcp.enabled' => true,
            'platforms.meta.mcp.token' => 'test-token',
            'platforms.meta.token' => 'test-token',
        ]);

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

        Http::fake(['*' => Http::response(['result' => ['content' => [['type' => 'text', 'text' => '{}']]]])]);
    }

    /** The case that was broken: an update is a write. */
    public function test_the_kill_switch_stops_a_budget_update(): void
    {
        $this->expectException(MetaException::class);

        app(Meta::class)->updateEntity('act_777', '990', 'ad_set', ['daily_budget' => 2000]);
    }

    public function test_the_kill_switch_stops_a_campaign_being_created(): void
    {
        $this->expectException(MetaException::class);

        app(Meta::class)->createCampaign(new CampaignSpec(
            adAccountId: 'act_777',
            name: 'Anything',
            objective: 'OUTCOME_LEADS',
        ));
    }

    /** Reads keep working, so the product can still explain itself while halted. */
    public function test_reads_still_work_while_writes_are_paused(): void
    {
        Http::fake(['*' => Http::response(['data' => []])]);

        $this->assertSame([], app(Meta::class)->adAccounts());
    }

    /** With the switch off, the same update goes through. */
    public function test_an_update_runs_when_writes_are_allowed(): void
    {
        config(['platforms.guardrails.paused' => false]);

        app(Meta::class)->updateEntity('act_777', '990', 'ad_set', ['daily_budget' => 2000]);

        $this->assertDatabaseHas('api_calls', [
            'endpoint' => 'ads_update_entity',
            'mutating' => true,
        ]);
    }

    /**
     * Reads and writes are told apart by what is known to read.
     *
     * The old rule matched "create" in the name, so anything Meta adds that is
     * not called create would have been logged as a read and escaped the kill
     * switch. Unknown now means dangerous.
     */
    public function test_only_known_reads_are_treated_as_reads(): void
    {
        $writes = new \ReflectionMethod(AdsMcp::class, 'writes');

        foreach (['ads_get_ad_entities', 'ads_get_ad_accounts', 'ads_library_search', 'ads_experiment_list_tests'] as $read) {
            $this->assertFalse($writes->invoke(null, $read), "{$read} should count as a read.");
        }

        foreach (['ads_update_entity', 'ads_create_campaign', 'ads_activate_entity', 'ads_something_new'] as $write) {
            $this->assertTrue($writes->invoke(null, $write), "{$write} should count as a write.");
        }
    }
}
