<?php

namespace Tests\Feature;

use App\Agent\Choices;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\LaunchFlow;
use App\Campaigns\Platforms\Platforms;
use App\Campaigns\Publishing\MetaPublisher;
use App\Models\Campaign;
use App\Models\User;
use App\Services\Meta\AdSetSpec;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * The ad set optimises toward an event the pixel actually fires.
 *
 * It was never asked. The event came from the objective alone: leads meant LEAD,
 * sales meant PURCHASE, and whether that pixel had ever seen such an event was
 * not consulted. Meta accepts it either way, so the campaign spends against
 * something it can never observe and the result reads as underdelivery. Every
 * number looks plausible, which is what makes it the hardest of these to find.
 *
 * Kaushal's campaign 120252047635070626 is the live case: pixel
 * 1017787001151608 fires PageView 102 times and Subscribe once, has never fired
 * Lead, and the ad set is asking Meta to optimise toward LEAD.
 */
class TheEventTheCampaignOptimisesTowardTest extends TestCase
{
    use RefreshDatabase;

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

        config([
            'platforms.meta.token' => 'test-token',
            'platforms.guardrails.allowed_ad_accounts' => ['meta:*'],
        ]);

        $this->actingAs(User::factory()->create());
        app(Workspace::class)->bindTo('conv-conversion-event');
    }

    /** @param array<string,int> $events */
    private function pixelFiring(array $events): void
    {
        $buckets = collect($events)
            ->map(fn (int $count, string $name): array => [
                'aggregation' => 'event',
                'data' => [['value' => $name, 'count' => $count]],
            ])
            ->values()
            ->all();

        Http::fake([
            '*/stats*' => Http::response(['data' => $buckets]),
            '*' => Http::response(['data' => []]),
        ]);
    }

    private function campaign(array $attributes = []): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Lucos leads',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_LEADS',
            'ad_account_id' => 'act_1950320145707342',
            'pixel_id' => '1017787001151608',
            ...$attributes,
        ]);

        app(Workspace::class)->focusOn($campaign);

        return $campaign;
    }

    private function tool(array $arguments = []): array
    {
        $result = collect(app(ToolRegistry::class)->resolve())
            ->first(fn ($t) => $t->name() === 'campaign__choose_conversion_event')
            ->handle(new Request($arguments));

        return json_decode((string) $result, true);
    }

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

    /**
     * The objective's default is called out when the pixel has never fired it.
     *
     * This is the whole tool. Kaushal's exact pixel and objective.
     */
    public function test_a_default_event_the_pixel_never_fired_is_flagged(): void
    {
        $this->pixelFiring(['PageView' => 102, 'Subscribe' => 1]);
        $this->campaign();

        $result = $this->tool();

        $this->assertTrue($result['ok'], $result['error'] ?? '');
        $this->assertTrue($result['default_never_fired'] ?? false, 'a LEAD default on this pixel was not flagged');
        $this->assertStringContainsString('never fired', $result['note']);
    }

    /** And the events it has fired are what is offered. */
    public function test_the_events_the_pixel_fired_are_offered(): void
    {
        $this->pixelFiring(['PageView' => 102, 'Subscribe' => 1]);
        $this->campaign();

        $this->tool();

        $offered = collect(app(Choices::class)->all())->firstWhere('field', 'conversion_event');

        $this->assertNotNull($offered, 'nothing was put on screen');
        $this->assertSame(['PageView', 'Subscribe'], collect($offered['options'])->pluck('value')->all());
    }

    /** A pixel that does fire the default says nothing alarming. */
    public function test_a_default_the_pixel_does_fire_is_not_flagged(): void
    {
        $this->pixelFiring(['Lead' => 34, 'PageView' => 900]);
        $this->campaign();

        $result = $this->tool();

        $this->assertArrayNotHasKey('default_never_fired', $result);
        $this->assertStringNotContainsString('never fired', $result['note']);
    }

    /** The suggested badge goes on the objective's own event, when it has fired. */
    public function test_the_objectives_event_is_marked_suggested(): void
    {
        $this->pixelFiring(['Lead' => 34, 'PageView' => 900]);
        $this->campaign();

        $this->tool();

        $options = collect(app(Choices::class)->all())->firstWhere('field', 'conversion_event')['options'];

        $this->assertSame('Suggested', collect($options)->firstWhere('value', 'Lead')['badge']);
        $this->assertNull(collect($options)->firstWhere('value', 'PageView')['badge']);
    }

    // ------------------------------------------------------------- the answer

    /** Choosing one stores Meta's enum, not the pixel's spelling. */
    public function test_the_chosen_event_is_stored_as_the_enum(): void
    {
        $this->pixelFiring(['PageView' => 102, 'Subscribe' => 1]);
        $campaign = $this->campaign();

        $result = $this->tool(['conversion_event' => 'Subscribe']);

        $this->assertTrue($result['ok'], $result['error'] ?? '');
        $this->assertSame('SUBSCRIBE', $result['conversion_event']);
        $this->assertSame('SUBSCRIBE', $campaign->refresh()->conversion_event);
    }

    /** Counts come back with it, because that is what justified the choice. */
    public function test_the_reply_says_how_often_it_fired(): void
    {
        $this->pixelFiring(['PageView' => 102, 'Subscribe' => 1]);
        $this->campaign();

        $this->assertSame(102, $this->tool(['conversion_event' => 'PageView'])['times_fired']);
    }

    /** An event the pixel has never fired is refused, and says what it has. */
    public function test_an_event_the_pixel_never_fired_is_refused(): void
    {
        $this->pixelFiring(['PageView' => 102, 'Subscribe' => 1]);
        $this->campaign();

        $result = $this->tool(['conversion_event' => 'Purchase']);

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('PageView (102)', $result['error']);
    }

    /** Case is not the buyer's problem. */
    public function test_the_event_name_is_matched_case_insensitively(): void
    {
        $this->pixelFiring(['Subscribe' => 1]);
        $campaign = $this->campaign();

        $this->assertTrue($this->tool(['conversion_event' => 'subscribe'])['ok']);
        $this->assertSame('SUBSCRIBE', $campaign->refresh()->conversion_event);
    }

    /**
     * A custom event is a real answer and becomes OTHER.
     *
     * Meta has no enum member for it, and refusing would make a site that
     * tracks its own events unable to optimise toward any of them.
     */
    public function test_a_custom_event_is_published_as_other(): void
    {
        $this->pixelFiring(['SubscribedToPlan' => 12]);
        $campaign = $this->campaign();

        $result = $this->tool(['conversion_event' => 'SubscribedToPlan']);

        $this->assertSame('OTHER', $result['conversion_event']);
        $this->assertSame('OTHER', $campaign->refresh()->conversion_event);
        $this->assertStringContainsString('custom event', $result['note']);
    }

    // ------------------------------------------------------------ the refusals

    /**
     * A pixel with no history is not a refusal.
     *
     * One installed an hour ago has fired nothing, and the objective's default
     * is then the only thing anyone can go on. Refusing here would stop a new
     * advertiser from finishing at all.
     */
    public function test_a_pixel_with_no_history_falls_back_to_the_default(): void
    {
        $this->pixelFiring([]);
        $campaign = $this->campaign();

        $result = $this->tool();

        $this->assertTrue($result['ok']);
        $this->assertSame('LEAD', $result['conversion_event']);
        $this->assertStringContainsString('not installed', $result['note']);

        // The step has to close, or the flow asks again every turn with nothing
        // on screen to click and the campaign can never be finished.
        $this->assertSame('LEAD', $campaign->refresh()->conversion_event);
        $this->assertTrue(
            collect(app(LaunchFlow::class)->progress($campaign->fresh()))
                ->firstWhere('key', 'conversion_event')['done'] ?? false,
            'a pixel with no history left the conversion event step permanently outstanding',
        );
    }

    /** A traffic campaign has no event to optimise toward, and is told so. */
    public function test_a_traffic_objective_has_nothing_to_choose(): void
    {
        $this->pixelFiring(['PageView' => 102]);
        $this->campaign(['objective' => 'OUTCOME_TRAFFIC']);

        $result = $this->tool();

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('does not optimise toward a pixel event', $result['error']);
    }

    /** The pixel comes first, since it is where the events are read from. */
    public function test_a_campaign_with_no_pixel_is_refused(): void
    {
        $this->pixelFiring(['PageView' => 102]);
        $this->campaign(['pixel_id' => null]);

        $result = $this->tool();

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('Choose the pixel first', $result['error']);
    }

    /** Google counts conversions its own way and has no part in this. */
    public function test_a_google_campaign_is_refused(): void
    {
        $this->pixelFiring(['PageView' => 102]);
        $this->campaign(['platform' => 'google', 'ad_account_id' => '3460855874', 'objective' => 'LEADS']);

        $this->assertFalse($this->tool()['ok']);
    }

    // ------------------------------------------------- what reaches the publisher

    /**
     * The chosen event is what is published, not the objective's default.
     *
     * Through MetaPublisher's own ad set builder, because asserting on an
     * AdSetSpec built by hand passes whether the publisher reads the column or
     * keeps deriving the event, which is how this test was first written: the
     * whole fix could be reverted with the suite still green.
     */
    public function test_the_publisher_sends_the_chosen_event(): void
    {
        $campaign = $this->campaign([
            'conversion_event' => 'SUBSCRIBE',
            'optimization_goal' => 'OFFSITE_CONVERSIONS',
            'budget' => 10,
            'budget_mode' => 'daily',
            'page_id' => 'page-1',
        ]);

        $spec = $this->adSetFor($campaign);

        $this->assertSame('SUBSCRIBE', $spec->promotedObject()['custom_event_type']);
    }

    /** And the objective's default still applies when nobody chose. */
    public function test_the_publisher_falls_back_to_the_objectives_event(): void
    {
        $campaign = $this->campaign([
            'conversion_event' => null,
            'optimization_goal' => 'OFFSITE_CONVERSIONS',
            'budget' => 10,
            'budget_mode' => 'daily',
            'page_id' => 'page-1',
        ]);

        $this->assertSame('LEAD', $this->adSetFor($campaign)->promotedObject()['custom_event_type']);
    }

    /** The publisher's own ad set builder, which is the line that was wrong. */
    private function adSetFor(Campaign $campaign): AdSetSpec
    {
        $publisher = app(MetaPublisher::class);

        $method = new \ReflectionMethod($publisher, 'adSet');
        $method->setAccessible(true);

        return $method->invoke($publisher, $campaign, 'campaign-1');
    }

    /**
     * Only Meta gets the row and the step.
     *
     * LinkedInRules::conversionEvent() maps objectives to events for its own
     * publisher while having no step to choose one and no tool that accepts one,
     * so gating either on that method gives LinkedIn a question nothing can
     * answer and a panel row that stays grey for the life of the campaign.
     */
    public function test_only_metas_flow_asks_for_a_conversion_event(): void
    {
        $asks = fn (string $platform): bool => collect(app(Platforms::class)
            ->for($platform)
            ->steps())
            ->contains(fn (array $step): bool => ($step['key'] ?? null) === 'conversion_event');

        $this->assertTrue($asks('meta'));
        $this->assertFalse($asks('linkedin'), 'LinkedIn has no tool for this and must not be asked');
        $this->assertFalse($asks('google'));
    }
}
