<?php

namespace Tests\Tenant;

use App\Events\AutoTraderIdAssignedToVehicle;
use App\Facades\Settings;
use App\Jobs\HandleAutoTraderHubNotification;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Mtc\ContentManager\Facades\Media as MediaFacade;
use Mtc\MercuryDataModels\ApiNotification;
use Mtc\MercuryDataModels\Vehicle;
use Tests\Support\InteractsWithAutoTraderMedia;
use Tests\TenantTestCase;

class HandleAutoTraderHubNotificationTest extends TenantTestCase
{
    use InteractsWithAutoTraderMedia;

    /**
     * When the upsert ack carries a new image order (auto-trader-upsert-sort-images
     * enabled), both the order and the primary flag must follow the AutoTrader order:
     * the first image of the ack becomes the primary, not whichever image was primary
     * under the old order.
     */
    public function testUpsertImageOrderingMovesPrimaryToTheNewFirstImage(): void
    {
        Queue::fake();
        Event::fake([AutoTraderIdAssignedToVehicle::class]);
        Settings::make([
            'tab' => 'foo',
            'section' => 'foo',
            'group' => 'foo',
            'name' => 'foo',
            'config_key' => 'auto-trader-upsert-sort-images',
            'value' => true,
            'type' => 'boolean',
        ]);

        $vehicle = Vehicle::factory()->create();
        $first = $this->autoTraderMedia('AT-1');
        $second = $this->autoTraderMedia('AT-2');
        $third = $this->autoTraderMedia('AT-3');

        // Initial state: images in the original AT order, first one primary.
        MediaFacade::setUsesForModel(
            [$first->id, $second->id, $third->id],
            $vehicle,
            ['primary' => true],
            false
        );

        // The upsert ack returns the images with the second one moved to the front.
        $input = [
            'type' => 'stock-upsert',
            'result' => 'success',
            'vehicle_id' => $vehicle->id,
            'stock_id' => 'STK-1',
            'data' => [
                'metadata' => ['stockId' => 'STK-1'],
                'media' => [
                    'images' => [
                        ['imageId' => 'AT-2'],
                        ['imageId' => 'AT-1'],
                        ['imageId' => 'AT-3'],
                    ],
                ],
            ],
        ];
        $notification = ApiNotification::query()->create([
            'provider' => 'auto-trader-hub',
            'data' => $input,
            'data_model' => 'vehicle',
        ]);

        (new HandleAutoTraderHubNotification($input, $notification))->handle();

        $this->assertSame(0, $this->useFor($vehicle, $second->id)->order);
        $this->assertSame(1, $this->useFor($vehicle, $first->id)->order);
        $this->assertSame(2, $this->useFor($vehicle, $third->id)->order);

        $primaryUses = $this->primaryUses($vehicle);

        $this->assertCount(1, $primaryUses);
        $this->assertSame(
            $second->id,
            $primaryUses->first()->media_id,
            'The first image of the AT order must be the primary.'
        );
    }


    /**
     * AUTO-1640: AutoTrader fires two STOCK_UPDATE webhooks for the same vehicle in the
     * same second. Two queue workers race the check-then-act in exists()/add() and create
     * duplicate rows. WithoutOverlapping keyed on stock_id serialises execution per
     * vehicle, so the second job sees the row committed by the first.
     */
    public function testMiddlewareSerialisesPerStockId(): void
    {
        $job = new HandleAutoTraderHubNotification(
            ['type' => 'STOCK_UPDATE', 'stock_id' => 'abc-123', 'data' => []],
            new ApiNotification(),
        );

        $middleware = $job->middleware();

        $this->assertCount(1, $middleware);
        $this->assertInstanceOf(WithoutOverlapping::class, $middleware[0]);
        $this->assertSame('at-hub-abc-123', $middleware[0]->key);
        $this->assertSame(5, $middleware[0]->releaseAfter);
        $this->assertSame(60, $middleware[0]->expiresAfter);
    }

    public function testMiddlewareFallsBackToGlobalKeyWhenStockIdMissing(): void
    {
        $job = new HandleAutoTraderHubNotification(
            ['type' => 'STOCK_UPDATE', 'data' => []],
            new ApiNotification(),
        );

        $middleware = $job->middleware();

        $this->assertSame('at-hub-no-stock-id', $middleware[0]->key);
    }

    /**
     * AUTO-1640: simulates a realistic burst of incoming webhooks where some vehicles
     * receive duplicate notifications and others do not. Proves the lock blocks duplicates
     * for the same stockId (so the race that creates duplicate vehicle rows is closed)
     * while leaving unrelated vehicles free to process in parallel.
     */
    public function testLockBlocksDuplicatesPerStockIdButAllowsDifferentVehiclesInParallel(): void
    {
        $heldLocks = [];

        // First webhook arrives for each of four vehicles - each acquires its own lock.
        foreach (['STOCK-A', 'STOCK-B', 'STOCK-C', 'STOCK-D'] as $stockId) {
            $job = new HandleAutoTraderHubNotification(
                ['type' => 'STOCK_UPDATE', 'stock_id' => $stockId, 'data' => []],
                new ApiNotification(),
            );
            $middleware = $job->middleware()[0];

            $lock = Cache::lock($middleware->getLockKey($job), 60);

            $this->assertTrue(
                $lock->get(),
                "First webhook for {$stockId} should acquire its lock"
            );
            $heldLocks[] = $lock;
        }

        try {
            // Duplicate webhooks arrive for STOCK-A and STOCK-C while their first webhooks
            // are still in flight. Both should be blocked (would be released back to queue
            // by the middleware and retried after releaseAfter seconds).
            foreach (['STOCK-A', 'STOCK-C'] as $stockId) {
                $duplicateJob = new HandleAutoTraderHubNotification(
                    ['type' => 'STOCK_UPDATE', 'stock_id' => $stockId, 'data' => []],
                    new ApiNotification(),
                );
                $duplicateMiddleware = $duplicateJob->middleware()[0];

                $duplicateLock = Cache::lock(
                    $duplicateMiddleware->getLockKey($duplicateJob),
                    60,
                );

                $this->assertFalse(
                    $duplicateLock->get(),
                    "Duplicate webhook for {$stockId} should be blocked while the first is in flight"
                );
            }

            // A genuinely new vehicle (STOCK-E) arriving at the same time is NOT blocked.
            // Different stockIds get independent locks, so parallelism across vehicles is
            // preserved - only same-vehicle webhooks serialise.
            $newJob = new HandleAutoTraderHubNotification(
                ['type' => 'STOCK_UPDATE', 'stock_id' => 'STOCK-E', 'data' => []],
                new ApiNotification(),
            );
            $newMiddleware = $newJob->middleware()[0];

            $newLock = Cache::lock($newMiddleware->getLockKey($newJob), 60);

            $this->assertTrue(
                $newLock->get(),
                'A different stockId should acquire its own independent lock and run in parallel'
            );
            $newLock->release();
        } finally {
            foreach ($heldLocks as $lock) {
                $lock->release();
            }
        }
    }
}
