<?php

namespace Tests\Tenant;

use App\Events\VehicleUpdatedFromImport;
use App\Imports\AutoTraderApiToVehicleImport;
use Carbon\Carbon;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Mtc\ContentManager\Facades\Media as MediaFacade;
use Mtc\MercuryDataModels\Vehicle;
use Tests\Support\InteractsWithAutoTraderMedia;
use Tests\TenantTestCase;

class AutoTraderApiToVehicleImportTest extends TenantTestCase
{
    use InteractsWithAutoTraderMedia;

    protected AutoTraderApiToVehicleImport $importer;

    #[\Override]
    protected function setUp(): void
    {
        parent::setUp();
        $this->importer = new AutoTraderApiToVehicleImport();
        $this->importer->setDealershipId(1); // example dealership
    }

    /**
     * A feed where ONLY the image order changed must still be picked up by the
     * image-checksum gate (shouldSyncImages), and applying it must leave both
     * the order and the primary matching the new AutoTrader order. The stored
     * checksum must then match the new order, so the next identical feed does
     * not re-trigger the sync.
     */
    public function testReorderOnlyFeedIsDetectedByChecksumAndAppliedCorrectly()
    {
        Queue::fake();

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

        // Initial sync in the original AT order.
        $this->runAutoTraderImageSync($vehicle, [$first, $second, $third]);

        $originalOrder = [$first, $second, $third];
        $newOrder = [$second, $first, $third];

        $this->assertFalse(
            $this->importer->imageChecksumMismatch($this->feedUrls($originalOrder), $vehicle->fresh()),
            'An unchanged feed must not re-trigger the image sync.'
        );
        $this->assertTrue(
            $this->importer->imageChecksumMismatch($this->feedUrls($newOrder), $vehicle->fresh()),
            'A feed where only the order changed must trigger the image sync.'
        );

        // The reorder-only feed is applied.
        $this->runAutoTraderImageSync($vehicle, $newOrder);

        $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 new AT order must be the primary.'
        );

        $this->assertFalse(
            $this->importer->imageChecksumMismatch($this->feedUrls($newOrder), $vehicle->fresh()),
            'After the reorder is applied, the same feed must not re-trigger the sync.'
        );
    }

    private function feedUrls(array $mediaInAutoTraderOrder): array
    {
        return collect($mediaInAutoTraderOrder)
            ->map(fn ($media) => $media->source_filename)
            ->toArray();
    }

    /**
     * A full sync of stock whose images have not changed (checksum matches, so the image job is
     * not dispatched) must still repair a primary that has drifted off AutoTrader's first image.
     */
    public function testReconcileFromFeedMovesADriftedPrimaryToTheFeedsFirstImage(): void
    {
        Queue::fake();

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

        $this->runAutoTraderImageSync($vehicle, [$first, $second, $third]);

        // Simulate a vehicle whose primary drifted onto the wrong image.
        $this->useFor($vehicle, $first->id)->update(['primary' => false]);
        $this->useFor($vehicle, $third->id)->update(['primary' => true]);

        $this->invokeReconcile($vehicle, [$first, $second, $third]);

        $primaryUses = $this->primaryUses($vehicle);
        $this->assertCount(1, $primaryUses);
        $this->assertSame($first->id, $primaryUses->first()->media_id, 'The feed first image becomes primary.');
    }

    /**
     * When AutoTrader's first image is not held locally (e.g. it failed to import), the reconcile
     * must promote the first feed image we do hold, not leave the primary on the wrong use.
     */
    public function testReconcileFromFeedSkipsFeedImagesNotHeldLocally(): void
    {
        Queue::fake();

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

        $this->runAutoTraderImageSync($vehicle, [$second, $third]);
        $this->useFor($vehicle, $second->id)->update(['primary' => false]);
        $this->useFor($vehicle, $third->id)->update(['primary' => true]);

        // Feed lists AT-1 first, but only AT-2 and AT-3 are held.
        $this->invokeReconcile($vehicle, [
            ['imageId' => 'AT-1'],
            ['imageId' => 'AT-2'],
            ['imageId' => 'AT-3'],
        ]);

        $primaryUses = $this->primaryUses($vehicle);
        $this->assertCount(1, $primaryUses);
        $this->assertSame(
            $second->id,
            $primaryUses->first()->media_id,
            'The first held feed image becomes primary when the feed first is missing.'
        );
    }

    /**
     * The reconcile must write nothing when the primary already matches the feed, so a correct
     * vehicle is not needlessly re-sequenced on every full sync. Asserted by spying on the write
     * methods rather than the end state (which would look identical whether or not it wrote).
     */
    public function testReconcileFromFeedWritesNothingWhenPrimaryAlreadyMatches(): void
    {
        Queue::fake();

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

        $this->runAutoTraderImageSync($vehicle, [$first, $second]);
        $this->assertSame($first->id, $this->primaryUses($vehicle)->first()->media_id);

        MediaFacade::spy();

        $this->invokeReconcile($vehicle, [$first, $second]);

        MediaFacade::shouldNotHaveReceived('setUseOrdering');
        MediaFacade::shouldNotHaveReceived('setPrimaryUseByOrder');
    }

    /**
     * The repair is wired into a full update(): a matching (unchanged) image feed must not
     * dispatch the image import job, yet must still correct a drifted primary.
     */
    public function testUpdateRepairsDriftedPrimaryWithoutDispatchingImageImport(): void
    {
        Queue::fake();

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

        $this->runAutoTraderImageSync($vehicle, [$first, $second, $third]);
        $this->useFor($vehicle, $first->id)->update(['primary' => false]);
        $this->useFor($vehicle, $third->id)->update(['primary' => true]);

        $payload = $this->stockPayload();
        $payload['media']['images'] = [
            ['imageId' => 'AT-1', 'href' => $first->source_filename],
            ['imageId' => 'AT-2', 'href' => $second->source_filename],
            ['imageId' => 'AT-3', 'href' => $third->source_filename],
        ];

        $this->importer->update($payload, $vehicle->id, false);

        Queue::assertNotPushed(ImportImageUrlList::class);

        $primaryUses = $this->primaryUses($vehicle);
        $this->assertCount(1, $primaryUses);
        $this->assertSame($first->id, $primaryUses->first()->media_id);
    }

    /**
     * @param Vehicle $vehicle
     * @param array $feedImages media objects (AT order) or raw media.images entries
     */
    private function invokeReconcile(Vehicle $vehicle, array $feedImages): void
    {
        $imageList = collect($feedImages)
            ->map(fn ($image) => is_array($image) ? $image : ['imageId' => $image->auto_trader_id])
            ->all();

        $reflection = new \ReflectionMethod($this->importer, 'reconcilePrimaryFromFeed');
        $reflection->setAccessible(true);
        $reflection->invoke($this->importer, $vehicle, $imageList);
    }

    public function testExistsReturnsTrueWhenVehicleWithUuidExists()
    {
        Vehicle::factory()->create([
            'stock_provider' => 'auto-trader',
            'uuid' => 'autotrader-uuid-123',
        ]);

        $vehicleData = [
            'metadata' => ['stockId' => 'autotrader-uuid-123'],
            'vehicle' => ['registration' => 'AB12CDE', 'vin' => 'VIN123']
        ];

        $this->assertTrue($this->importer->exists($vehicleData));
    }

    public function testExistsReturnsTrueWhenVehicleWithVinExists()
    {
        Vehicle::factory()->create([
            'stock_provider' => 'auto-trader',
            'vin' => 'VIN123'
        ]);

        $vehicleData = [
            'metadata' => ['stockId' => 'non-matching-id'],
            'vehicle' => ['registration' => 'AB12CDE', 'vin' => 'VIN123']
        ];

        $this->assertTrue($this->importer->exists($vehicleData));
    }

    public function testMapChargeTimesExtractsSlowAndQuickChargeData(): void
    {
        $chargeTimes = [
            [
                'chargerType' => 'Fast Charge',
                'chargerDescription' => '22 kW',
                'chargerSupplyType' => 'AC Three Phase',
                'connectorType' => 'Type 2',
                'startBatteryPercentage' => 0,
                'endBatteryPercentage' => 100,
                'durationMinutes' => 305,
            ],
            [
                'chargerType' => 'Fast Charge',
                'chargerDescription' => '22 kW',
                'chargerSupplyType' => 'AC Three Phase',
                'connectorType' => 'Type 2',
                'startBatteryPercentage' => 10,
                'endBatteryPercentage' => 100,
                'durationMinutes' => 275,
            ],
            [
                'chargerType' => 'Rapid Charge',
                'chargerDescription' => '50 kW',
                'chargerSupplyType' => 'DC',
                'connectorType' => 'CCS',
                'startBatteryPercentage' => 0,
                'endBatteryPercentage' => 80,
                'durationMinutes' => 66,
            ],
            [
                'chargerType' => 'Rapid Charge',
                'chargerDescription' => '50 kW',
                'chargerSupplyType' => 'DC',
                'connectorType' => 'CCS',
                'startBatteryPercentage' => 10,
                'endBatteryPercentage' => 80,
                'durationMinutes' => 58,
            ],
        ];

        $reflection = new \ReflectionMethod($this->importer, 'mapChargeTimes');
        $reflection->setAccessible(true);
        $result = $reflection->invoke($this->importer, $chargeTimes);

        // Slow charge (AC) - picks lowest duration (275 over 305)
        $this->assertEquals(275, $result['battery_charge_time']);
        $this->assertEquals('Type 2', $result['plug_type']);
        $this->assertEquals('22 kW AC Three Phase', $result['battery_slow_charge_description']);

        // Quick charge (DC) - picks lowest duration (58 over 66)
        $this->assertEquals(58, $result['battery_quick_charge_time']);
        $this->assertEquals('CCS', $result['rapid_charge_plug_type']);
        $this->assertEquals(80, $result['battery_quick_charge_level']);
        $this->assertEquals(10, $result['battery_quick_charge_start_level']);
        $this->assertEquals('50 kW DC', $result['battery_quick_charge_description']);
    }

    public function testMapChargeTimesReturnsEmptyArrayWhenNoData(): void
    {
        $reflection = new \ReflectionMethod($this->importer, 'mapChargeTimes');
        $reflection->setAccessible(true);

        $this->assertEmpty($reflection->invoke($this->importer, []));
    }

    /**
     * AUTO-1653: a SOLD vehicle whose AutoTrader record has not been updated for over a week
     * is stale and should be skipped on update.
     */
    public function testIsSoldAndStaleReturnsTrueForSoldVehicleNotUpdatedForOverAWeek(): void
    {
        $this->assertTrue($this->invokeIsSoldAndStale([
            'metadata' => [
                'lifecycleState' => 'SOLD',
                'lastUpdated' => Carbon::now()->subDays(8)->toIso8601String(),
            ],
        ]));
    }

    public function testIsSoldAndStaleReturnsFalseForSoldVehicleUpdatedWithinAWeek(): void
    {
        $this->assertFalse($this->invokeIsSoldAndStale([
            'metadata' => [
                'lifecycleState' => 'SOLD',
                'lastUpdated' => Carbon::now()->subDays(6)->toIso8601String(),
            ],
        ]));
    }

    public function testIsSoldAndStaleReturnsFalseForNonSoldVehicleEvenWhenStale(): void
    {
        $this->assertFalse($this->invokeIsSoldAndStale([
            'metadata' => [
                'lifecycleState' => 'FORECOURT',
                'lastUpdated' => Carbon::now()->subDays(30)->toIso8601String(),
            ],
        ]));
    }

    public function testIsSoldAndStaleReturnsFalseWhenLastUpdatedMissingOrUnparseable(): void
    {
        $this->assertFalse($this->invokeIsSoldAndStale([
            'metadata' => ['lifecycleState' => 'SOLD'],
        ]));

        $this->assertFalse($this->invokeIsSoldAndStale([
            'metadata' => ['lifecycleState' => 'SOLD', 'lastUpdated' => 'not-a-date'],
        ]));
    }

    /**
     * AUTO-1653: the guard is wired into update() - a stale SOLD payload must leave the
     * existing vehicle untouched (no fields changed, no update event dispatched).
     */
    public function testUpdateSkipsStaleSoldVehicle(): void
    {
        Event::fake([VehicleUpdatedFromImport::class]);

        $vehicle = Vehicle::factory()->create([
            'stock_provider' => 'auto-trader',
            'price' => 12345,
            'was_recently_synced' => false,
        ]);

        $this->importer->update([
            'metadata' => [
                'stockId' => 'stock-1',
                'lifecycleState' => 'SOLD',
                'lastUpdated' => Carbon::now()->subDays(8)->toIso8601String(),
            ],
            'adverts' => ['retailAdverts' => ['suppliedPrice' => ['amountGBP' => 99999]]],
        ], $vehicle->id);

        $vehicle->refresh();

        $this->assertSame(12345, (int) $vehicle->price);
        $this->assertFalse((bool) $vehicle->was_recently_synced);
        Event::assertNotDispatched(VehicleUpdatedFromImport::class);
    }

    public function testUpdateSetsStockArrivalDateFromDateOnForecourt(): void
    {
        $vehicle = Vehicle::factory()->create([
            'stock_provider' => 'auto-trader',
            'stock_arrival_date' => null,
        ]);

        $this->importer->update($this->stockPayload(), $vehicle->id, false);

        $vehicle->refresh();

        // metadata.dateOnForecourt in the fixture is 2020-09-04
        $this->assertNotNull($vehicle->stock_arrival_date);
        $this->assertSame('2020-09-04', $vehicle->stock_arrival_date->format('Y-m-d'));
    }

    public function testUpdateDoesNotOverwriteExistingStockArrivalDate(): void
    {
        $vehicle = Vehicle::factory()->create([
            'stock_provider' => 'auto-trader',
            'stock_arrival_date' => Carbon::parse('2026-01-15'),
        ]);

        $this->importer->update($this->stockPayload(), $vehicle->id, false);

        $vehicle->refresh();

        $this->assertSame('2026-01-15', $vehicle->stock_arrival_date->format('Y-m-d'));
    }

    public function testUpdateLeavesStockArrivalDateNullWhenDateOnForecourtMissing(): void
    {
        $vehicle = Vehicle::factory()->create([
            'stock_provider' => 'auto-trader',
            'stock_arrival_date' => null,
        ]);

        $payload = $this->stockPayload();
        unset($payload['metadata']['dateOnForecourt']);

        $this->importer->update($payload, $vehicle->id, false);

        $vehicle->refresh();

        $this->assertNull($vehicle->stock_arrival_date);
    }

    private function stockPayload(): array
    {
        $response = json_decode(
            file_get_contents(base_path('tests/data/stock/autotrader-stock-response.json')),
            true
        );

        $payload = $response['results'][0];

        // The number-plate validator reads the global countries table, which tenant
        // tests do not migrate; matching here is by id, so registration is not needed.
        unset($payload['vehicle']['registration']);

        return $payload;
    }

    private function invokeIsSoldAndStale(array $vehicleData): bool
    {
        $reflection = new \ReflectionMethod($this->importer, 'isSoldAndStale');
        $reflection->setAccessible(true);

        return $reflection->invoke($this->importer, $vehicleData);
    }
}
