<?php

namespace Tests\Feature;

use App\Agent\Choices;
use App\Agent\MarketingAgent;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\LaunchFlow;
use App\Campaigns\PublishGate;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Choosing a capped bid strategy must not put the conversation in a loop.
 *
 * Found on arb-dev on 17 Sep: clicking "Cost per result goal" asked "How should
 * it bid?" again, five times in one conversation. The tool refused a capped
 * strategy that arrived without its amount and stored nothing, so the flow's
 * next step was still bidding, so the controller's offerNextStep() called the
 * same tool with no arguments and re-offered the same three strategies. Every
 * turn, with no way out other than typing a number the screen never asked for.
 *
 * What these pin: a capped strategy is recorded on arrival, the follow-up
 * question is the amount rather than the strategy again, and publishing stays
 * blocked until the amount is given.
 */
class BiddingLoopTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

    protected Campaign $campaign;

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

        MarketingAgent::fake(['Pick one.']);

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

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-bidding-0001');
        $workspace->ownedBy($this->user);

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

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

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

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

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

    public function test_a_capped_strategy_without_an_amount_is_recorded_rather_than_refused(): void
    {
        $result = $this->runTool('campaign__set_bidding', ['strategy' => 'COST_CAP']);

        $this->assertTrue($result['ok'], 'Refusing here is what caused the loop.');
        $this->assertSame('COST_CAP', $this->campaign->fresh()->bid_strategy);
    }

    /**
     * The thing the user actually saw: the same question, again.
     *
     * The flow drives offerNextStep(), so a bidding step that never completes
     * is re-offered on every subsequent turn.
     */
    public function test_choosing_a_capped_strategy_settles_the_bidding_step(): void
    {
        $this->runTool('campaign__set_bidding', ['strategy' => 'COST_CAP']);

        $next = app(LaunchFlow::class)->next($this->campaign->fresh());

        $this->assertNotSame(
            'bidding',
            $next['key'] ?? null,
            'The flow still wants bidding, so the strategy chips are offered again and the loop repeats.',
        );
    }

    public function test_the_follow_up_question_asks_for_the_amount_not_the_strategy_again(): void
    {
        $this->runTool('campaign__set_bidding', ['strategy' => 'COST_CAP']);

        $offered = app(Choices::class)->all();

        $this->assertCount(1, $offered);
        $this->assertSame('bid_amount', $offered[0]['field']);
        $this->assertNotEmpty($offered[0]['options'], 'An open question with nothing to click is the dead end.');
    }

    /** Clicking one of those amounts sends only the number, so it has to be enough. */
    public function test_an_amount_alone_completes_the_strategy_already_chosen(): void
    {
        $this->runTool('campaign__set_bidding', ['strategy' => 'COST_CAP']);

        $result = $this->runTool('campaign__set_bidding', ['target_cost' => 5]);

        $this->assertTrue($result['ok']);

        $campaign = $this->campaign->fresh();
        $this->assertSame('COST_CAP', $campaign->bid_strategy);
        $this->assertEquals(5, $campaign->bid_amount);
    }

    /** Recording the strategy early must not let it reach Meta without its amount. */
    public function test_publishing_stays_blocked_until_the_amount_arrives(): void
    {
        $this->runTool('campaign__set_bidding', ['strategy' => 'COST_CAP']);

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

        $this->assertContains('bid strategy needs a target cost', $problems);
    }

    /** Highest volume has no amount to ask for, so it must still settle in one step. */
    public function test_the_uncapped_strategy_still_completes_immediately(): void
    {
        $result = $this->runTool('campaign__set_bidding', ['strategy' => 'LOWEST_COST_WITHOUT_CAP']);

        $this->assertTrue($result['ok']);
        $this->assertSame([], app(Choices::class)->all(), 'Nothing more to ask.');
        $this->assertNull($this->campaign->fresh()->bid_amount);
    }
}
