<?php

namespace Tests\Feature;

use App\Mcp\Tools\GoogleAds\SelectGoogleImageAsset;
use App\Models\Creative;
use App\Models\User;
use App\Services\GoogleAdsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Models\Conversation;
use ReflectionClass;
use ReflectionMethod;
use Tests\TestCase;

/**
 * The blockers raised reviewing PR #22, pinned so they cannot return.
 *
 * Each of these shipped on the google-mcp branch and each is the kind of thing
 * that reads as harmless until somebody states the consequence.
 */
class Pr22BlockersTest extends TestCase
{
    use RefreshDatabase;

    /**
     * No live credential may sit in the source.
     *
     * A Google refresh token was a public constant, used whenever
     * GOOGLE_ADS_REFRESH_TOKEN was unset. Any environment missing its config
     * reached Google as whoever owned that token, and anyone with repository
     * access held a long lived credential.
     */
    public function test_no_refresh_token_is_hardcoded_in_the_service(): void
    {
        $constants = (new ReflectionClass(GoogleAdsService::class))->getConstants();

        $this->assertArrayNotHasKey('FALLBACK_REFRESH_TOKEN', $constants);

        $source = file_get_contents((new ReflectionClass(GoogleAdsService::class))->getFileName());

        // Google refresh tokens begin 1// and are the shape that matters here.
        $this->assertDoesNotMatchRegularExpression(
            "/'1\/\/[A-Za-z0-9_\-]{20,}'/",
            $source,
            'A Google refresh token literal is back in GoogleAdsService.',
        );
    }

    /**
     * An image must not be written into somebody else's campaign draft.
     *
     * The conversation id arrives as request input, and the lookup was on the
     * id alone. The creative listing was scoped by user_id, so the read was
     * safe and the write was not.
     */
    public function test_an_image_cannot_be_written_into_another_users_draft(): void
    {
        Storage::fake('local');

        $owner = User::factory()->create();
        $attacker = User::factory()->create();

        $conversationId = 'conv-victim-0001';

        DB::table('agent_conversations')->insert([
            'id' => $conversationId,
            'participant_type' => $owner->getMorphClass(),
            'participant_id' => $owner->getKey(),
            'chat_type' => 'google_ads',
            'title' => 'Victim',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        // The attacker names the victim's conversation on the request.
        request()->merge(['conversation' => $conversationId]);

        $save = new ReflectionMethod(SelectGoogleImageAsset::class, 'saveDraftImage');
        $save->invoke(
            app(SelectGoogleImageAsset::class),
            'marketing_image_assets',
            'customers/1/assets/999',
            $attacker,
        );

        $this->assertNull(
            Conversation::find($conversationId)->google_campaign_draft,
            'Another user wrote an image asset into this draft.',
        );
    }

    /** The owner writing to their own draft still works, which is what makes it a scope and not a block. */
    public function test_the_owner_can_still_write_to_their_own_draft(): void
    {
        Storage::fake('local');

        $owner = User::factory()->create();
        $conversationId = 'conv-owner-0001';

        DB::table('agent_conversations')->insert([
            'id' => $conversationId,
            'participant_type' => $owner->getMorphClass(),
            'participant_id' => $owner->getKey(),
            'chat_type' => 'google_ads',
            'title' => 'Mine',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        request()->merge(['conversation' => $conversationId]);

        $save = new ReflectionMethod(SelectGoogleImageAsset::class, 'saveDraftImage');
        $save->invoke(
            app(SelectGoogleImageAsset::class),
            'marketing_image_assets',
            'customers/1/assets/999',
            $owner,
        );

        // Decoded, because json_encode escapes the slashes in a resource name.
        $draft = json_decode((string) Conversation::find($conversationId)->google_campaign_draft, true);

        $this->assertSame('customers/1/assets/999', $draft['marketing_image_assets'] ?? null);
    }

    /**
     * Every migration has to survive being run against a database that already
     * has the column.
     *
     * Two migrations add google_chat_conversation_id to google_ads_campaigns.
     * Unguarded, the second fails with a duplicate column and takes the whole
     * migrate run with it.
     */
    public function test_google_migrations_are_guarded_against_existing_columns(): void
    {
        $unguarded = [];

        foreach (glob(database_path('migrations/*.php')) as $path) {
            $name = basename($path);

            if (! str_contains($name, 'google')) {
                continue;
            }

            $source = file_get_contents($path);

            // Only the ones that alter an existing table. A create_ migration
            // has nothing to collide with.
            if (! str_contains($source, 'Schema::table(')) {
                continue;
            }

            if (! str_contains($source, 'hasColumn')) {
                $unguarded[] = $name;
            }
        }

        $this->assertSame([], $unguarded, 'These alter a table without checking the column exists first.');
    }

    /**
     * The guards actually hold, rather than merely being present.
     *
     * Grepping for hasColumn proves somebody typed it. Running up() a second
     * time against a database that already has the columns proves it works,
     * which is the situation that broke the migrate run in the first place.
     */
    public function test_every_google_migration_can_be_run_twice(): void
    {
        foreach (glob(database_path('migrations/*google*.php')) as $path) {
            $migration = require $path;

            if (! str_contains(file_get_contents($path), 'Schema::table(')) {
                continue;
            }

            try {
                // The schema is already migrated by RefreshDatabase, so this is
                // the second run by definition.
                $migration->up();
            } catch (\Throwable $e) {
                $this->fail(sprintf(
                    'Re-running %s failed: %s',
                    basename($path),
                    $e->getMessage(),
                ));
            }
        }

        $this->assertTrue(true, 'Every Google migration survived a second run.');
    }
}
