<?php

namespace Tests\Feature;

use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\Briefs;
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;

/**
 * Two steps a buyer could answer correctly and still not get past.
 *
 * Both were found in one conversation on arb-dev on 25 Sep, and both have the
 * same shape: the product asks a question, the buyer gives an answer that is
 * right, and the answer is refused for a reason that is about the product
 * rather than about the answer. The buyer's only way forward was to work out
 * what the product wanted to hear.
 */
class ThreeStepsThatCouldNotBeFinishedTest extends TestCase
{
    use RefreshDatabase;

    protected Brief $brief;

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

        config([
            'platforms.guardrails.max_daily_budget' => 20,
            'platforms.guardrails.budget_ceiling_currency' => 'USD',
        ]);

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

        app(Workspace::class)->bindTo('conv-three-steps');

        $this->brief = Brief::create([
            'conversation_id' => 'conv-three-steps',
            'user_id' => auth()->id(),
        ]);
    }

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

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

        return $campaign;
    }

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

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

    /**
     * The ceiling is one campaign's daily spend, so a total is divided first.
     *
     * 30 a day across three platforms is 10 each and every share is inside the
     * 20 ceiling, and it was refused as though one campaign had asked for 30.
     * The check ran before anything had decided whether the amount was a total.
     * The buyer got past it by naming all three shares by hand.
     */
    public function test_a_total_across_three_platforms_is_not_judged_as_one_campaigns_spend(): void
    {
        $this->campaign('meta');
        $this->campaign('google');
        $this->campaign('linkedin');

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

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

    /**
     * The total is recorded on the brief, where a total belongs.
     *
     * Read from the raw column, because ResolvesFromBrief makes an unset
     * campaign budget read through to the brief's. $meta->budget is 30 here by
     * design and says nothing about which row holds it.
     */
    public function test_the_total_reaches_the_brief_rather_than_one_campaign(): void
    {
        $meta = $this->campaign('meta');
        $this->campaign('google');
        $this->campaign('linkedin');

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

        app(Briefs::class)->settle();

        $this->assertNull($meta->refresh()->getRawOriginal('budget'));
        $this->assertEquals(30, $this->brief->refresh()->budget);
    }

    /**
     * A total that cannot be divided under the ceiling is still refused.
     *
     * 90 across three is 30 each, which is over the 20 ceiling however it is
     * split, so it fails here rather than three steps later. The reply says
     * the per-platform number, because 90 on its own looks allowed.
     */
    public function test_a_total_that_no_split_could_rescue_is_still_refused(): void
    {
        $this->campaign('meta');
        $this->campaign('google');
        $this->campaign('linkedin');

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

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('30.00 a day each', $result['error']);
    }

    /** One platform is still measured whole, which is the original behaviour. */
    public function test_a_single_campaign_is_still_held_to_the_ceiling(): void
    {
        $this->campaign('meta');

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

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

    /**
     * "None" is an answer to a date question, not an unreadable date.
     *
     * "When should the campaign start?" was asked four times in one
     * conversation. The answer went to Carbon::parse, which throws on "none",
     * so the reply was "[none] is not a date I can read" and the question came
     * straight back. The step could not be finished by answering it.
     */
    public function test_none_is_understood_as_an_answer_to_the_start_date(): void
    {
        $campaign = $this->campaign('meta');

        $result = $this->tool('campaign__set_schedule', ['starts_at' => 'none']);

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

    /** And the answer is recorded, which is what lets the step settle. */
    public function test_the_answer_is_recorded_so_the_step_does_not_come_back(): void
    {
        $campaign = $this->campaign('meta');

        $this->tool('campaign__set_schedule', ['starts_at' => 'none']);

        $this->assertArrayHasKey('starts_at', app(Workspace::class)->sourcesFor($campaign->refresh()));
    }

    /** The same words work on the end date, where they mean runs until stopped. */
    public function test_continuous_is_understood_as_an_answer_to_the_end_date(): void
    {
        $campaign = $this->campaign('meta');

        $result = $this->tool('campaign__set_schedule', ['ends_at' => 'continuous']);

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

    /** "As soon as possible" is the same answer said differently. */
    public function test_asap_is_understood(): void
    {
        $campaign = $this->campaign('meta');

        $result = $this->tool('campaign__set_schedule', ['starts_at' => 'As soon as possible']);

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

    /**
     * A date it cannot read is still refused by name.
     *
     * The list is short on purpose. Guessing at "next quarter" is how a
     * campaign starts on a day nobody chose, which is worse than asking again.
     */
    public function test_a_date_that_is_not_a_date_is_still_refused(): void
    {
        $this->campaign('meta');

        $result = $this->tool('campaign__set_schedule', ['starts_at' => 'next quarter']);

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

    /** A real date still parses. */
    public function test_a_real_date_is_unaffected(): void
    {
        $campaign = $this->campaign('meta');

        $result = $this->tool('campaign__set_schedule', ['starts_at' => '2026-10-01T09:00:00Z']);

        $this->assertTrue($result['ok'], $result['error'] ?? '');
        $this->assertSame('2026-10-01', $campaign->refresh()->starts_at->toDateString());
    }
}
