<?php

namespace Tests\Feature;

use App\Agent\Choices;
use App\Agent\ToolRegistry;
use App\Agent\Tools\Campaign\ChoosePixel;
use App\Agent\Workspace;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Four things an AttestPath campaign on arb-dev was not told.
 *
 * Read back over 46 messages. None of them stopped the buyer, and all four are
 * the same kind of quiet: the product knew something and did not say it.
 */
class FourThingsTheChatShouldHaveSaidTest extends TestCase
{
    use RefreshDatabase;

    protected Brief $brief;

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

        config([
            'platforms.guardrails.max_daily_budget' => 20,
            'platforms.guardrails.max_ads_per_publish' => 5,
        ]);

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

        app(Workspace::class)->bindTo('conv-four-things');

        $this->brief = Brief::create([
            'conversation_id' => 'conv-four-things',
            'user_id' => auth()->id(),
            'landing_url' => 'https://attestpath.ai',
        ]);
    }

    private function campaign(string $platform, array $attributes = []): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'brief_id' => $this->brief->id,
            'name' => ucfirst($platform).' campaign',
            'platform' => $platform,
            'status' => 'draft',
            'budget_mode' => 'daily',
            'landing_url' => 'https://attestpath.ai',
            ...$attributes,
        ]);

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

        return $campaign;
    }

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

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

    // ------------------------------------------- 3: copy, capped per platform

    /**
     * Three variations each across three platforms is nine, and that is fine.
     *
     * The cap counted the whole submission, so nine against a limit of five was
     * refused and per-platform copy could not be used at three platforms at
     * all. PublishGate reads the limit off one campaign's own ads; this now
     * agrees with it.
     */
    public function test_three_variations_each_across_three_platforms_is_allowed(): void
    {
        foreach (['meta', 'google', 'linkedin'] as $platform) {
            $this->campaign($platform);
        }

        $ads = [];
        foreach (['meta', 'google', 'linkedin'] as $platform) {
            foreach ([1, 2, 3] as $i) {
                $ads[] = ['platform' => $platform, 'headline' => 'Automate audits '.$i, 'primary_text' => 'Body.', 'cta' => 'LEARN_MORE'];
            }
        }

        $result = $this->tool('campaign__write_ad_copy', ['ads' => $ads]);

        $this->assertTrue($result['ok'], $result['error'] ?? '');
    }

    /** One campaign still cannot take more than the cap. */
    public function test_one_platform_over_the_cap_is_still_refused(): void
    {
        $this->campaign('meta');

        $ads = [];
        foreach (range(1, 6) as $i) {
            $ads[] = ['platform' => 'meta', 'headline' => 'Automate audits '.$i, 'primary_text' => 'Body.', 'cta' => 'LEARN_MORE'];
        }

        $result = $this->tool('campaign__write_ad_copy', ['ads' => $ads]);

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

    // ------------------------------------------------ 4: budget against floors

    /**
     * A total the platforms cannot divide is refused when it is named.
     *
     * Floors are Meta 5, Google 0, LinkedIn 10, so three platforms need 15. A
     * buyer typed 10, it was accepted, and only the split two turns later
     * worked out that it was impossible.
     */
    public function test_a_total_below_the_platform_floors_is_refused(): void
    {
        foreach (['meta', 'google', 'linkedin'] as $platform) {
            $this->campaign($platform);
        }

        $result = $this->tool('campaign__set_campaign_budget', ['amount' => 10]);

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

    /** And the reply names each platform's own floor, not just the total. */
    public function test_the_refusal_shows_where_the_minimum_comes_from(): void
    {
        foreach (['meta', 'google', 'linkedin'] as $platform) {
            $this->campaign($platform);
        }

        $error = $this->tool('campaign__set_campaign_budget', ['amount' => 10])['error'];

        $this->assertStringContainsString('LinkedIn 10.00', $error);
        $this->assertStringContainsString('Meta 5.00', $error);
    }

    /** A total that clears the floors is still accepted. */
    public function test_a_workable_total_is_accepted(): void
    {
        foreach (['meta', 'google', 'linkedin'] as $platform) {
            $this->campaign($platform);
        }

        $result = $this->tool('campaign__set_campaign_budget', ['amount' => 25]);

        $this->assertTrue($result['ok'], $result['error'] ?? '');
    }

    /**
     * The amount picker offers rungs rather than an empty screen.
     *
     * It bounded the list by the floor of the whole brief and the ceiling of
     * one campaign, so on three platforms it wanted an amount both >= 30 and
     * <= 20 and returned nothing, silently. The buyer was told to choose from
     * options that were not there.
     */
    public function test_the_picker_offers_amounts_on_a_three_platform_brief(): void
    {
        foreach (['meta', 'google', 'linkedin'] as $platform) {
            $this->campaign($platform);
        }

        $result = $this->tool('campaign__set_campaign_budget');

        $this->assertTrue($result['ok'], $result['error'] ?? '');
        $this->assertNotEmpty(app(Choices::class)->all(), 'no amounts were put on screen');
    }

    // --------------------------------------- 1 and 2: the pixel that was named

    /** @return array<string,mixed> the reply from choosing that pixel */
    private function choosePixel(Campaign $campaign, string $id, string $name): array
    {
        $tool = app(ChoosePixel::class);

        $brand = new \ReflectionMethod($tool, 'brandMismatch');
        $brand->setAccessible(true);
        $goal = new \ReflectionMethod($tool, 'goalMismatch');
        $goal->setAccessible(true);

        $chosen = ['id' => $id, 'name' => $name];

        return [
            'brand_mismatch' => $brand->invoke($tool, $campaign, $chosen),
            'goal_mismatch' => $goal->invoke($tool, $campaign, $chosen),
        ];
    }

    /**
     * A pixel from another brand is flagged, not merely named.
     *
     * An AttestPath campaign was given the StockPrices pixel and the reply just
     * said its name. The Page mismatch two turns earlier got "please confirm
     * that is intentional"; this got nothing, and a pixel decides which
     * conversions are attributed and to whom.
     */
    public function test_a_pixel_from_another_brand_is_flagged(): void
    {
        $campaign = $this->campaign('meta', ['objective' => 'OUTCOME_LEADS']);

        $this->assertStringContainsString(
            'StockPrices',
            (string) $this->choosePixel($campaign, '2476726526117566', 'StockPrices')['brand_mismatch'],
        );
    }

    /** A pixel matching the brand says nothing. */
    public function test_a_matching_pixel_is_not_flagged(): void
    {
        $campaign = $this->campaign('meta', ['objective' => 'OUTCOME_LEADS']);

        $this->assertNull($this->choosePixel($campaign, '1', 'AttestPath Leads')['brand_mismatch']);
    }

    /**
     * A conversion that is not what the campaign optimises for is flagged.
     *
     * Objective LEADS, conversion action Purchase, and nothing said a word. The
     * campaign then buys the wrong outcome at full price and reports it as
     * underdelivery, which is the hardest mistake to trace because every number
     * looks plausible.
     */
    public function test_a_purchase_conversion_on_a_leads_campaign_is_flagged(): void
    {
        $campaign = $this->campaign('google', ['objective' => 'LEADS']);

        $this->assertStringContainsString(
            'leads',
            (string) $this->choosePixel($campaign, '1', 'Purchase')['goal_mismatch'],
        );
    }

    /** A lead conversion on a leads campaign is left alone. */
    public function test_a_lead_conversion_on_a_leads_campaign_is_not_flagged(): void
    {
        $campaign = $this->campaign('google', ['objective' => 'LEADS']);

        $this->assertNull($this->choosePixel($campaign, '1', 'Demo Request Form Submit')['goal_mismatch']);
    }

    /** And a traffic campaign has no expectation either way, so it says nothing. */
    public function test_a_traffic_campaign_has_no_goal_expectation(): void
    {
        $campaign = $this->campaign('google', ['objective' => 'WEBSITE_TRAFFIC']);

        $this->assertNull($this->choosePixel($campaign, '1', 'Purchase')['goal_mismatch']);
    }
}
