<?php

namespace Tests\Feature;

use App\Agent\Workspace;
use App\Models\Campaign;
use App\Models\User;
use App\Services\GoogleAdsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Laravel\Ai\Models\Conversation;
use ReflectionMethod;
use Tests\TestCase;

/**
 * Three places where a value decided elsewhere reached something it should not.
 *
 * Grouped because they are one shape: a boundary that was checked somewhere
 * else, or not at all, and a caller that trusted it.
 */
class ThreeWaysToActOnSomethingYouShouldNotTest extends TestCase
{
    use RefreshDatabase;

    // ----------------------------------------------------------- GAQL literal

    private function escape(string $value): string
    {
        $method = new ReflectionMethod(GoogleAdsService::class, 'escapeGaqlLiteral');
        $method->setAccessible(true);

        return $method->invoke(app(GoogleAdsService::class), $value);
    }

    /**
     * Where the quoted literal actually ends in the query we would send.
     *
     * Walking it is the only honest check: eyeballing backslashes in a test
     * assertion is how the original escape came to look correct.
     */
    private function literalClosesAtEnd(string $name): bool
    {
        $query = "WHERE geo_target_constant.name = '".$this->escape($name)."'";

        $inside = false;
        $i = 0;
        $length = strlen($query);

        while ($i < $length) {
            if ($query[$i] === '\\') {
                $i += 2;   // an escaped character, whatever it is

                continue;
            }

            if ($query[$i] === "'") {
                if ($inside) {
                    return $i === $length - 1;
                }

                $inside = true;
            }

            $i++;
        }

        return false;
    }

    /**
     * The defect: a value ending in backslash-quote closed the literal.
     *
     * The escape handled the quote and left the escape character alone, so \'
     * became an escaped backslash followed by a live quote and the rest was
     * parsed as GAQL. Locations and languages come from the chat.
     */
    public function test_a_backslash_cannot_close_the_gaql_literal(): void
    {
        $this->assertTrue($this->literalClosesAtEnd(
            "US\\' AND campaign.id > 0 AND geo_target_constant.name != '"
        ), 'the injected value closed the quoted literal');
    }

    /** A place with an apostrophe in its name is still a place. */
    public function test_a_legitimate_apostrophe_survives(): void
    {
        $this->assertTrue($this->literalClosesAtEnd("Cote d'Ivoire"));
        $this->assertSame("Cote d\\'Ivoire", $this->escape("Cote d'Ivoire"));
    }

    /** And an ordinary name is untouched. */
    public function test_an_ordinary_name_is_unchanged(): void
    {
        $this->assertSame('United States', $this->escape('United States'));
    }

    // ------------------------------------------- the performance page sidebar

    private function conversationFor(User $user, string $title): void
    {
        Conversation::query()->create([
            'id' => (string) Str::uuid7(),
            'participant_type' => $user->getMorphClass(),
            'participant_id' => $user->getKey(),
            'title' => $title,
        ]);
    }

    /**
     * The defect: the page built its own unscoped conversation list.
     *
     * The sidebar composer only fills the list when the page has not supplied
     * one, so supplying an unscoped list here actively defeated the scoping
     * ChatController exists to apply. ChatListIsScopedTest covers /chats, the
     * dashboard and the chat page, and did not cover this route.
     */
    public function test_the_performance_page_does_not_list_another_users_chats(): void
    {
        $mine = User::factory()->create();
        $theirs = User::factory()->create();

        $this->conversationFor($mine, 'My own campaign chat');
        $this->conversationFor($theirs, 'Their secret launch');

        $this->actingAs($mine)
            ->get('/campaign-performance')
            ->assertOk()
            ->assertSee('My own campaign chat')
            ->assertDontSee('Their secret launch');
    }

    // --------------------------------------------- publishing what is on screen

    /**
     * The defect: the button published whatever was in focus.
     *
     * panel() renders and computes `ready` for the campaign being viewed, and
     * clicking a tab deliberately does not move focus, so the confirm dialog
     * described one campaign while Publisher::propose() was handed the other.
     * The panel now reports which campaign it drew and the button sends it back.
     */
    public function test_the_panel_reports_which_campaign_it_rendered(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $conversation = (string) Str::uuid7();

        Conversation::query()->create([
            'id' => $conversation,
            'participant_type' => $user->getMorphClass(),
            'participant_id' => $user->getKey(),
            'title' => 'Two platforms',
        ]);

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

        $meta = Campaign::create([
            'user_id' => $user->id, 'name' => 'Meta one',
            'platform' => 'meta', 'status' => 'draft',
        ]);
        $google = Campaign::create([
            'user_id' => $user->id, 'name' => 'Google one',
            'platform' => 'google', 'status' => 'draft',
        ]);

        $workspace->produced($meta);
        $workspace->produced($google);
        $workspace->focusOn($meta);

        // Looking at the Google one while the Meta one is in focus.
        $viewed = $this->postJson(route('chat.view'), [
            'conversation' => $conversation,
            'campaign' => $google->id,
        ])->assertOk();

        $this->assertSame(
            $google->id,
            $viewed->json('campaignId'),
            'the panel did not say which campaign it drew, so the button cannot target it',
        );
    }

    /** An id from another conversation resolves to nothing rather than publishing. */
    public function test_publishing_refuses_a_campaign_this_conversation_does_not_hold(): void
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $conversation = (string) Str::uuid7();

        Conversation::query()->create([
            'id' => $conversation,
            'participant_type' => $user->getMorphClass(),
            'participant_id' => $user->getKey(),
            'title' => 'Mine',
        ]);

        $elsewhere = Campaign::create([
            'user_id' => $user->id, 'name' => 'Somewhere else',
            'platform' => 'meta', 'status' => 'draft',
        ]);

        $this->postJson(route('chat.publish'), [
            'conversation' => $conversation,
            'campaign' => $elsewhere->id,
        ])->assertStatus(422);
    }
}
