<?php

namespace Tests\Feature;

use App\Agent\MarketingAgent;
use App\Agent\Workspace;
use App\Models\Asset;
use App\Models\Campaign;
use App\Models\ConversationArtifact;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;

/**
 * The chat surface.
 *
 * These exercise the HTTP layer, not the model: what matters here is that a
 * message reaches the agent, an attachment becomes an asset linked to the
 * conversation, and publishing is refused unless the gate allows it.
 */
class ChatControllerTest extends TestCase
{
    use RefreshDatabase;

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

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

        // The agent must never reach a real provider from a test. Without this
        // every send() would spend tokens and take seconds, and the suite would
        // fail whenever the provider was busy rather than when the code broke.
        MarketingAgent::fake(['Noted.']);
    }

    protected User $user;

    /**
     * A real conversation row owned by the acting user.
     *
     * The endpoints refuse a conversation the caller does not own, so a made-up
     * id now returns 404 rather than reaching the handler. That is the point of
     * the check; these tests just have to own what they talk about.
     */
    private function ownedConversation(string $id): string
    {
        DB::table('agent_conversations')->insert([
            'id' => $id,
            'participant_type' => $this->user->getMorphClass(),
            'participant_id' => $this->user->getKey(),
            'title' => 'Test conversation',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return $id;
    }

    /**
     * "New chat" has to be blank.
     *
     * It redirected to /chat, which opens the most recent conversation, so the
     * button put you straight back in the chat you were trying to leave.
     */
    public function test_new_chat_does_not_reopen_the_last_conversation(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0007');

        DB::table('agent_conversation_messages')->insert([
            'id' => (string) Str::uuid7(),
            'conversation_id' => $conversation,
            'participant_type' => $this->user->getMorphClass(),
            'participant_id' => $this->user->getKey(),
            'agent' => MarketingAgent::class,
            'role' => 'user',
            'content' => 'Budget is eighty pounds a day.',
            'attachments' => '[]',
            'tool_calls' => '[]',
            'tool_results' => '[]',
            'usage' => '[]',
            'meta' => '[]',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        // Landing on /chat with nothing asked for still opens the latest one.
        $this->get(route('chat.show'))
            ->assertOk()
            ->assertSee('Budget is eighty pounds a day.', escape: false);

        $this->get(route('chat.new'))
            ->assertRedirect(route('chat.show', ['new' => 1]));

        $this->get(route('chat.show', ['new' => 1]))
            ->assertOk()
            ->assertDontSee('Budget is eighty pounds a day.', escape: false);
    }

    /**
     * Switching campaigns from the panel.
     *
     * A conversation can hold several. The endpoint takes an id from the page,
     * so it has to refuse one this conversation does not own rather than focus
     * whatever it is handed.
     */
    public function test_focus_switches_between_the_conversations_own_campaigns(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0008');

        $workspace = app(Workspace::class);
        $workspace->bindTo($conversation);

        $first = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
        ]);
        $second = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Spring Sale',
            'platform' => 'meta',
            'status' => 'draft',
        ]);

        $workspace->produced($first);
        $workspace->produced($second);

        $this->postJson(route('chat.focus'), [
            'conversation' => $conversation,
            'campaign' => $first->id,
        ])->assertOk()->assertJsonPath('ok', true);

        $this->assertSame($first->id, $workspace->focus()?->id);
    }

    public function test_focus_refuses_a_campaign_from_another_conversation(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0009');
        $elsewhere = $this->ownedConversation('conv-chat-0010');

        $workspace = app(Workspace::class);
        $workspace->bindTo($elsewhere);

        $outsider = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Not in this chat',
            'platform' => 'meta',
            'status' => 'draft',
        ]);
        $workspace->produced($outsider);

        $this->postJson(route('chat.focus'), [
            'conversation' => $conversation,
            'campaign' => $outsider->id,
        ])->assertNotFound();
    }

    public function test_the_chat_page_loads_with_an_empty_panel(): void
    {
        $this->get(route('chat.show'))
            ->assertOk()
            ->assertSee('New chat')
            ->assertSee('No campaign yet', escape: false);
    }

    public function test_the_chat_requires_authentication(): void
    {
        auth()->logout();

        $this->get(route('chat.show'))->assertRedirect(route('login'));
        $this->post(route('chat.send'), ['message' => 'hello'])->assertRedirect(route('login'));
    }

    public function test_an_attachment_becomes_an_asset_linked_to_the_conversation(): void
    {
        Storage::fake('local');

        $conversation = $this->ownedConversation('conv-chat-0001');

        $workspace = app(Workspace::class);
        $workspace->bindTo($conversation);

        $this->post(route('chat.send'), [
            'conversation' => $conversation,
            'message' => '',
            'files' => [UploadedFile::fake()->image('creative.png', 600, 600)],
        ]);

        $asset = Asset::first();

        $this->assertNotNull($asset, 'The upload should have become an asset.');
        $this->assertSame('upload', $asset->source);
        $this->assertSame('creative.png', $asset->original_name);
        $this->assertNotNull($asset->checksum);
        Storage::disk('local')->assertExists($asset->path);

        $this->assertSame(1, ConversationArtifact::where('conversation_id', 'conv-chat-0001')->count());
    }

    public function test_publishing_is_refused_while_the_campaign_is_incomplete(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0002');

        $workspace = app(Workspace::class);
        $workspace->bindTo($conversation);

        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Half built',
            'platform' => 'meta',
            'status' => 'draft',
        ]);
        $workspace->produced($campaign);

        $this->postJson(route('chat.publish'), ['conversation' => $conversation])
            ->assertStatus(422)
            ->assertJsonPath('ok', false)
            ->assertJsonFragment(['error' => 'The campaign is not ready to publish.']);

        $this->assertSame('draft', $campaign->fresh()->status);
    }

    public function test_publishing_without_a_campaign_is_refused(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0003');
        app(Workspace::class)->bindTo($conversation);

        $this->postJson(route('chat.publish'), ['conversation' => $conversation])
            ->assertStatus(422)
            ->assertJsonPath('ok', false);
    }

    /**
     * A turn that fails must still appear in the history.
     *
     * The package writes the transcript only on success, so a provider outage
     * used to erase both the question and the failure: the user saw an error,
     * reloaded, and found no trace of either.
     */
    public function test_a_failed_turn_is_still_recorded_in_the_transcript(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0004');

        MarketingAgent::fake(fn () => throw new \RuntimeException('Could not connect to AI provider [openai].'));

        $this->postJson(route('chat.send'), [
            'conversation' => $conversation,
            'message' => 'generate the landing page',
        ])->assertOk();

        $stored = DB::table('agent_conversation_messages')
            ->where('conversation_id', $conversation)
            ->orderBy('id')
            ->pluck('content', 'role');

        $this->assertCount(2, $stored);
        $this->assertSame('generate the landing page', $stored['user']);
        $this->assertStringContainsString('Could not connect', $stored['assistant']);
    }

    /**
     * A run paused for approval must survive a reload.
     *
     * The prompt was only ever drawn from a JSON response, so refreshing the
     * page left the run paused on the server with nothing on screen able to
     * resolve it, and the campaign stuck half decided.
     */
    public function test_a_pending_approval_is_restored_after_a_reload(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0005');

        DB::table('agent_conversation_messages')->insert([
            'id' => (string) Str::uuid7(),
            'conversation_id' => $conversation,
            'participant_type' => $this->user->getMorphClass(),
            'participant_id' => $this->user->getKey(),
            'agent' => MarketingAgent::class,
            'role' => 'assistant',
            'content' => 'Ready to publish.',
            'attachments' => '[]',
            'tool_calls' => json_encode([['id' => 'call_1', 'name' => 'campaign__publish_campaign', 'arguments' => []]]),
            'tool_results' => '[]',
            'usage' => '[]',
            'meta' => '[]',
            'approval_state' => json_encode(['pending' => ['call_1' => 'This creates the campaign on the platform.']]),
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        $this->get(route('chat.show', ['conversation' => $conversation]))
            ->assertOk()
            ->assertSee('call_1', escape: false)
            ->assertSee('campaign  publish campaign', escape: false);
    }

    /** A decided approval must not come back on the next load. */
    public function test_a_resolved_approval_is_not_restored(): void
    {
        $conversation = $this->ownedConversation('conv-chat-0006');

        DB::table('agent_conversation_messages')->insert([
            'id' => (string) Str::uuid7(),
            'conversation_id' => $conversation,
            'participant_type' => $this->user->getMorphClass(),
            'participant_id' => $this->user->getKey(),
            'agent' => MarketingAgent::class,
            'role' => 'assistant',
            'content' => 'Published.',
            'attachments' => '[]',
            'tool_calls' => '[]',
            'tool_results' => '[]',
            'usage' => '[]',
            'meta' => '[]',
            'approval_state' => json_encode(['pending' => []]),
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        $this->get(route('chat.show', ['conversation' => $conversation]))
            ->assertOk()
            ->assertDontSee('Approve:', escape: false);
    }

    public function test_oversized_and_unsupported_uploads_are_rejected(): void
    {
        $this->postJson(route('chat.send'), [
            'message' => 'here you go',
            'files' => [UploadedFile::fake()->create('payload.php', 10, 'application/x-php')],
        ])->assertStatus(422)->assertJsonValidationErrors('files.0');
    }
}
