<?php

namespace Tests\Feature;

use App\Agent\MarketingAgent;
use App\Agent\Workspace;
use App\Models\Campaign;
use App\Models\ConversationArtifact;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Responses\Data\ToolCall;
use Tests\TestCase;

/**
 * The agent loop itself, with the provider scripted rather than called.
 *
 * Everything else drives tools directly or fakes the whole agent, which leaves
 * the part between "the model asked" and "the tool ran" untested. Three bugs
 * came out of exactly that gap: artifacts orphaned on the first turn because no
 * conversation existed yet, a stale focus handed to the tool after it, and tool
 * names one provider rejected. Each is asserted here.
 */
class AgentLoopTest extends TestCase
{
    use RefreshDatabase;

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

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

    protected User $user;

    /**
     * The first turn is the dangerous one: the conversation row does not exist
     * while the tools run, so anything created then has nothing to belong to.
     */
    public function test_a_tool_called_on_the_very_first_turn_is_linked_to_the_conversation(): void
    {
        MarketingAgent::fake([
            $this->toolCall('campaign__start_campaign', ['name' => 'Winter Sale', 'platform' => 'meta']),
            'Drafted Winter Sale on Meta.',
        ]);

        $agent = app(MarketingAgent::class)->forParticipant($this->user);
        $response = $agent->prompt('Build a Meta campaign called Winter Sale.');

        $this->assertSame('Drafted Winter Sale on Meta.', trim($response->text));

        $campaign = Campaign::sole();
        $this->assertSame('Winter Sale', $campaign->name);

        // The point of the test: it belongs to the conversation, not to nothing.
        $this->assertSame(1, ConversationArtifact::query()
            ->where('conversation_id', $agent->currentConversation())
            ->where('artifactable_id', $campaign->id)
            ->count());
    }

    /**
     * A second tool in the same turn must see what the first one wrote, not a
     * copy taken before it ran.
     */
    public function test_a_later_tool_in_the_same_turn_sees_the_earlier_tool_s_writes(): void
    {
        MarketingAgent::fake([
            $this->toolCall('campaign__start_campaign', ['name' => 'Winter Sale', 'platform' => 'meta']),
            $this->toolCall('campaign__set_campaign_budget', ['daily_budget' => 15]),
            'Set the budget to 15.',
        ]);

        app(MarketingAgent::class)
            ->forParticipant($this->user)
            ->prompt('Build a Meta campaign called Winter Sale at 15 a day.');

        // set_campaign_budget acts on whatever is in focus. If focus were stale
        // or unset it would have refused, and the budget would still be null.
        $this->assertSame('15.00', Campaign::sole()->budget);
    }

    /**
     * Creating something focuses it, so a later call in the same turn lands on
     * the new one rather than whatever came before.
     */
    public function test_creating_an_artifact_focuses_it_for_later_calls(): void
    {
        MarketingAgent::fake([
            $this->toolCall('campaign__start_campaign', ['name' => 'First', 'platform' => 'meta']),
            $this->toolCall('campaign__start_campaign', ['name' => 'Second', 'platform' => 'google']),
            $this->toolCall('campaign__set_campaign_budget', ['daily_budget' => 12]),
            'Created both and set the budget on the second.',
        ]);

        app(MarketingAgent::class)
            ->forParticipant($this->user)
            ->prompt('Create First on Meta, then Second on Google, and make that one 12 a day.');

        // The budget belongs to the most recently created campaign.
        $this->assertNull(Campaign::where('name', 'First')->sole()->budget);
        $this->assertSame('12.00', Campaign::where('name', 'Second')->sole()->budget);
    }

    /** A refusal comes back as a result the model can act on, not an exception. */
    public function test_a_refused_tool_call_does_not_break_the_turn(): void
    {
        config(['platforms.guardrails.max_daily_budget' => 20]);

        MarketingAgent::fake([
            $this->toolCall('campaign__start_campaign', ['name' => 'Winter Sale', 'platform' => 'meta']),
            $this->toolCall('campaign__set_campaign_budget', ['daily_budget' => 5000]),
            'That budget is above the ceiling this product allows.',
        ]);

        $response = app(MarketingAgent::class)
            ->forParticipant($this->user)
            ->prompt('Build a Meta campaign at 5000 a day.');

        $this->assertStringContainsString('ceiling', $response->text);
        $this->assertNull(Campaign::sole()->budget);
    }

    /** The workspace follows the conversation when an older one is resumed. */
    public function test_continuing_a_conversation_rebinds_the_workspace_to_it(): void
    {
        MarketingAgent::fake([
            $this->toolCall('campaign__start_campaign', ['name' => 'Winter Sale', 'platform' => 'meta']),
            'Created.',
            'Still here.',
        ]);

        $first = app(MarketingAgent::class)->forParticipant($this->user);
        $first->prompt('Create it.');
        $conversationId = $first->currentConversation();

        $resumed = app(MarketingAgent::class)->continue($conversationId, $this->user);
        $resumed->prompt('Anything else?');

        $this->assertSame($conversationId, app(Workspace::class)->id());
        $this->assertInstanceOf(Campaign::class, app(Workspace::class)->focus());
    }

    /** @param array<string,mixed> $arguments */
    private function toolCall(string $name, array $arguments): ToolCall
    {
        return new ToolCall(id: 'call_'.$name, name: $name, arguments: $arguments);
    }
}
