<?php

namespace Tests\Feature;

use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\PublishGate;
use App\Models\Campaign;
use App\Models\User;
use App\Services\Meta\CampaignSpec;
use App\Services\Meta\GraphApi;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * What Meta is told the campaign is about, legally.
 *
 * All three write paths hardcoded an empty list, so every campaign published so
 * far declared itself as no special category without anyone being asked. For a
 * housing, employment or credit ad that is a false declaration to Meta, and
 * those categories carry anti-discrimination obligations.
 *
 * The distinction these cover is between "nobody answered" and "answered, none
 * apply". Both look identical once sent, so the difference has to be kept here.
 */
class SpecialAdCategoryTest extends TestCase
{
    use RefreshDatabase;

    protected Campaign $campaign;

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

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

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

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-sac-0001');

        $this->campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_TRAFFIC',
        ]);

        $workspace->focusOn($this->campaign);
    }

    private function runTool(string $name, array $arguments = []): string
    {
        $tool = collect(app(ToolRegistry::class)->resolve())->first(fn ($t) => $t->name() === $name);

        $this->assertNotNull($tool, "Tool {$name} is not registered.");

        return (string) $tool->handle(new Request($arguments));
    }

    /** Nobody answered is not the same as answered none. */
    public function test_publishing_is_refused_until_the_category_is_declared(): void
    {
        $this->assertNull($this->campaign->special_ad_categories);

        $problems = app(PublishGate::class)->check($this->campaign);

        $this->assertNotEmpty(array_filter($problems, fn ($p) => str_contains($p, 'special ad category')));
    }

    public function test_declaring_none_satisfies_the_gate(): void
    {
        $this->runTool('campaign__declare_ad_category', ['categories' => ['NONE']]);

        $problems = app(PublishGate::class)->check($this->campaign->fresh());

        $this->assertSame([], array_filter($problems, fn ($p) => str_contains($p, 'special ad category')));
        $this->assertSame(['NONE'], $this->campaign->fresh()->special_ad_categories);
    }

    public function test_a_real_category_is_recorded(): void
    {
        $this->runTool('campaign__declare_ad_category', ['categories' => ['housing']]);

        $this->assertSame(['HOUSING'], $this->campaign->fresh()->special_ad_categories);
    }

    /** NONE means none apply, so it cannot sit alongside one that does. */
    public function test_none_cannot_be_combined_with_a_category(): void
    {
        $result = $this->runTool('campaign__declare_ad_category', ['categories' => ['NONE', 'CREDIT']]);

        $this->assertStringContainsString('cannot be combined', $result);
        $this->assertNull($this->campaign->fresh()->special_ad_categories);
    }

    public function test_an_unknown_category_is_refused(): void
    {
        $result = $this->runTool('campaign__declare_ad_category', ['categories' => ['GAMBLING']]);

        $this->assertStringContainsString('Unknown category', $result);
        $this->assertNull($this->campaign->fresh()->special_ad_categories);
    }

    /** The declaration is the advertiser's, so it must never read as inferred. */
    public function test_the_declaration_is_recorded_as_something_the_user_said(): void
    {
        $this->runTool('campaign__declare_ad_category', ['categories' => ['EMPLOYMENT']]);

        $sources = app(Workspace::class)->sourcesFor($this->campaign->fresh());

        $this->assertSame('user_stated', $sources['special_ad_categories'] ?? null);
    }

    /** What actually reaches Meta, which is where the old bug lived. */
    public function test_a_declared_category_is_sent_to_the_platform(): void
    {
        Http::fake(['*' => Http::response(['id' => 'camp-1'])]);

        app(GraphApi::class)->createCampaign(new CampaignSpec(
            adAccountId: 'act_777',
            name: 'Winter Sale',
            objective: 'OUTCOME_TRAFFIC',
            specialAdCategories: ['HOUSING'],
        ));

        Http::assertSent(function ($request): bool {
            return str_contains($request->url(), '/campaigns')
                && ($request->data()['special_ad_categories'] ?? null) === json_encode(['HOUSING']);
        });
    }

    /** NONE is ours; Meta wants an empty list, not the word. */
    public function test_none_is_sent_to_the_platform_as_an_empty_list(): void
    {
        Http::fake(['*' => Http::response(['id' => 'camp-1'])]);

        app(GraphApi::class)->createCampaign(new CampaignSpec(
            adAccountId: 'act_777',
            name: 'Winter Sale',
            objective: 'OUTCOME_TRAFFIC',
            specialAdCategories: ['NONE'],
        ));

        Http::assertSent(function ($request): bool {
            return str_contains($request->url(), '/campaigns')
                && ($request->data()['special_ad_categories'] ?? null) === json_encode([]);
        });
    }
}
