<?php

namespace Tests\Tenant\Services;

use App\Facades\Settings;
use App\Imports\AutoTraderApiToVehicleImport;
use App\Jobs\HandleAutoTraderHubNotification;
use App\Services\AutoTraderHub;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Mtc\MercuryDataModels\ApiNotification;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Media;
use Mtc\MercuryDataModels\MediaUse;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleFeature;
use Tests\TenantTestCase;

class AutoTraderHubTest extends TenantTestCase
{
    private AutoTraderHub $hub;

    #[\Override]
    protected function setUp(): void
    {
        parent::setUp();
        $this->hub = new AutoTraderHub();
    }

    public function testSetToken()
    {
        $instance = $this->hub->setToken('my-token');
        $this->assertInstanceOf(AutoTraderHub::class, $instance);

        $this->assertEquals('my-token', $this->invokeMethod($this->hub, 'getToken'));
    }

    public function testInitExportSuccess()
    {
        Http::fake([
            '*' => Http::response(['reference' => 'ref-123'], 200),
        ]);

        $this->hub->setToken('my-token');
        $reference = $this->hub->initExport();

        $this->assertEquals('ref-123', $reference);
    }

    public function testInitExportFails()
    {
        Http::fake([
            '*' => Http::response(['error' => 'fail'], 500),
        ]);

        Log::shouldReceive('error')->once();

        $this->hub->setToken('my-token');

        $this->expectException(\Exception::class);
        $this->hub->initExport();
    }

    public function testBulkExport()
    {
        $vehicles = collect([$this->mockVehicle()]);

        Http::fake([
            '*' => Http::response([], 200),
        ]);

        $this->hub->setToken('my-token');
        $result = $this->hub->bulkExport('ref-001', $vehicles);

        $this->assertTrue($result);
    }

    public function testExportSingleVehicle()
    {
        $vehicle = $this->mockVehicle();

        Http::fake([
            '*' => Http::response([], 200),
        ]);

        $this->hub->setToken('my-token');
        $result = $this->hub->export($vehicle);

        $this->assertTrue($result);
    }

    public function testCreateSiteSuccess()
    {
        Settings::shouldReceive('get')->with('auto-trader-advertiser-id')->andReturn('12345');
        Settings::shouldReceive('update')->with('auto-trader-hub-token', 'abc123')->once();

        config(['services.auto-trader-hub.api_key' => 'key']);
        config(['app.url' => 'https://tenant.example.com']);

        Http::fake([
            '*' => Http::response(['api_token' => 'abc123'], 200),
        ]);

        $response = $this->hub->createSite();
        $this->assertNull($response);
    }

    public function testMapVehicleIncludesAllFieldsWhenExportSettingIsUnconfigured()
    {
        $this->mockExportedFieldsSetting(null);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        foreach (['make', 'model', 'trim', 'body_type', 'fuel_type', 'co2', 'features', 'images'] as $key) {
            $this->assertArrayHasKey($key, $payload, "Expected optional field '{$key}' to be exported");
        }
    }

    public function testMapVehicleFiltersOptionalFieldsToAllowlist()
    {
        $this->mockExportedFieldsSetting(['make', 'model']);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        $this->assertArrayHasKey('make', $payload);
        $this->assertArrayHasKey('model', $payload);

        $filtered = ['trim', 'body_type', 'co2', 'features', 'images', 'video_url', 'vat_scheme',
            'description', 'attention_grabber'];
        foreach ($filtered as $key) {
            $this->assertArrayNotHasKey($key, $payload, "Field '{$key}' should be filtered out");
        }
    }

    public function testMapVehicleIncludesNewlyOptionalFieldsWhenWhitelisted()
    {
        $this->mockExportedFieldsSetting(['description', 'attention_grabber']);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        $this->assertArrayHasKey('description', $payload);
        $this->assertArrayHasKey('attention_grabber', $payload);
        $this->assertArrayNotHasKey('make', $payload);
    }

    public function testMapVehicleAlwaysRetainsMandatoryFieldsEvenIfNotInAllowlist()
    {
        $this->mockExportedFieldsSetting(['co2']);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        foreach (AutoTraderHub::MANDATORY_EXPORT_FIELDS as $key) {
            $this->assertArrayHasKey($key, $payload, "Mandatory field '{$key}' must always be exported");
        }

        $this->assertArrayHasKey('co2', $payload);
        $this->assertArrayNotHasKey('make', $payload);
        $this->assertArrayNotHasKey('features', $payload);
        // description / attention_grabber are now optional, so a whitelist that omits them drops them.
        $this->assertArrayNotHasKey('description', $payload);
        $this->assertArrayNotHasKey('attention_grabber', $payload);
    }

    public function testMapVehicleExcludeListDropsFieldsWhenNoWhitelistSet()
    {
        $this->mockFieldSettings(null, ['colour', 'video_url']);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        $this->assertArrayNotHasKey('colour', $payload);
        $this->assertArrayNotHasKey('video_url', $payload);
        // Everything else still exported.
        $this->assertArrayHasKey('make', $payload);
        $this->assertArrayHasKey('description', $payload);
    }

    public function testExcludeListIsIgnoredWhenWhitelistIsSet()
    {
        // Mutually exclusive: a non-empty include whitelist wins, exclude is ignored.
        $this->mockFieldSettings(['make', 'model'], ['make']);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        $this->assertArrayHasKey('make', $payload);
        $this->assertArrayHasKey('model', $payload);
        $this->assertArrayNotHasKey('colour', $payload);
    }

    public function testExcludeListCannotRemoveMandatoryFields()
    {
        $this->mockFieldSettings(null, ['id', 'price', 'colour']);

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$this->mockVehicle()]);

        $this->assertArrayHasKey('id', $payload, 'Mandatory fields must never be excludable');
        $this->assertArrayHasKey('price', $payload);
        $this->assertArrayNotHasKey('colour', $payload);
    }

    public function testEveryMappedFieldIsClassifiedAsMandatoryOrOptional()
    {
        // Guard against future orphans: a field added to mapVehicle() but to neither
        // list would silently drop the moment a whitelist is set.
        $this->mockFieldSettings(null, null);

        $vehicle = $this->mockVehicle();
        $vehicle->is_vat_qualifying = true; // ensure the conditional vat_status key is produced
        $vehicle->is_vat_applicable = true;

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$vehicle]);

        $expected = array_merge(
            AutoTraderHub::MANDATORY_EXPORT_FIELDS,
            array_keys(AutoTraderHub::OPTIONAL_EXPORT_FIELDS)
        );
        sort($expected);
        $actual = array_keys($payload);
        sort($actual);

        $this->assertSame($expected, $actual, 'Every mapVehicle() field must be mandatory or optional');
    }

    public function testMapVehicleUsesStandardDescriptionWhenFeedDescriptionSettingDisabled(): void
    {
        Settings::shouldReceive('get')
            ->andReturnUsing(fn (string $key, $default = null) => match ($key) {
                'sales-channels-feed-description-enabled' => false,
                default => $default,
            });

        $vehicle = $this->mockVehicle();
        $vehicle->description = 'Standard description';
        $vehicle->description_for_feeds = 'Feed-only description';

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$vehicle]);

        $this->assertSame('Standard description', $payload['description']);
    }

    public function testMapVehicleUsesFeedDescriptionWhenFeedDescriptionSettingEnabled(): void
    {
        Settings::shouldReceive('get')
            ->andReturnUsing(fn (string $key, $default = null) => match ($key) {
                'sales-channels-feed-description-enabled' => true,
                default => $default,
            });

        $vehicle = $this->mockVehicle();
        $vehicle->description = 'Standard description';
        $vehicle->description_for_feeds = 'Feed-only description';

        $payload = $this->invokeMethod($this->hub, 'mapVehicle', [$vehicle]);

        $this->assertSame('Feed-only description', $payload['description']);
    }

    private function mockExportedFieldsSetting($value): void
    {
        $this->mockFieldSettings($value, null);
    }

    private function mockFieldSettings($included, $excluded): void
    {
        Settings::shouldReceive('get')
            ->andReturnUsing(fn (string $key, $default = null) => match ($key) {
                'auto-trader-hub-exported-optional-fields' => $included,
                'auto-trader-hub-excluded-optional-fields' => $excluded,
                default => $default,
            });
    }

    public function testCreateSiteWithoutApiToken()
    {
        Settings::shouldReceive('get')->with('auto-trader-advertiser-id')->andReturn('12345');
        Settings::shouldReceive('update')->never();

        config(['services.auto-trader-hub.api_key' => 'key']);
        config(['app.url' => 'https://example.com']);

        Http::fake([
            '*' => Http::response(['data' => 'no-token'], 200),
        ]);

        // Asserts it doesn't break even when token is missing
        $response = $this->hub->createSite();
        $this->assertNull($response);
    }

    public function testStockUpsertAssignsNewImageIdAndClearsMissingReference()
    {
        // Re-uploaded image: AutoTrader returned a fresh id.
        $reuploaded = Media::factory()->create(['src' => 'reuploaded.jpg', 'auto_trader_id' => 'old-id']);
        // Missing image: re-upload failed, so the microservice reports a null id to clear the reference.
        $missing = Media::factory()->create(['src' => 'missing.jpg', 'auto_trader_id' => 'stale-id']);
        // Untouched image: not referenced in the notification at all.
        $untouched = Media::factory()->create(['src' => 'untouched.jpg', 'auto_trader_id' => 'keep-id']);

        $job = $this->makeNotificationJob([
            'uploaded_images' => [
                ['media_id' => $reuploaded->id, 'auto_trader_id' => 'fresh-id'],
                ['media_id' => $missing->id, 'auto_trader_id' => null],
            ],
        ]);

        $this->invokeMethod($job, 'assignUploadedImages');

        $this->assertEquals('fresh-id', $reuploaded->refresh()->auto_trader_id);
        $this->assertNull($missing->refresh()->auto_trader_id);
        $this->assertEquals('keep-id', $untouched->refresh()->auto_trader_id);
    }

    public function testStockUpsertWithoutUploadedImagesLeavesMediaUntouched()
    {
        $media = Media::factory()->create(['src' => 'untouched.jpg', 'auto_trader_id' => 'keep-id']);

        $job = $this->makeNotificationJob([]);
        $this->invokeMethod($job, 'assignUploadedImages');

        $this->assertEquals('keep-id', $media->refresh()->auto_trader_id);
    }

    public function testAssignsDealershipFromAdvertiserId(): void
    {
        $dealership = Dealership::factory()->create([
            'data' => ['auto-trader-location-id' => '10047694'],
        ]);

        $job = $this->makeNotificationJob([
            'advertiser' => ['advertiserId' => '10047694'],
        ]);

        $this->invokeMethod($job, 'assignDealershipFromAdvertiser', [[]]);

        $this->assertEquals($dealership->id, $this->importDealershipId($job));
    }

    public function testUnknownAdvertiserDoesNotAssignDealership(): void
    {
        Dealership::factory()->create([
            'data' => ['auto-trader-location-id' => '10047694'],
        ]);

        $job = $this->makeNotificationJob([
            'advertiser' => ['advertiserId' => '99999999'],
        ]);

        $this->invokeMethod($job, 'assignDealershipFromAdvertiser', [[]]);

        $this->assertNull($this->importDealershipId($job));
    }

    /**
     * Read the dealership id the job set on its (private) importer instance.
     */
    private function importDealershipId(HandleAutoTraderHubNotification $job): ?int
    {
        $import = (new \ReflectionProperty(HandleAutoTraderHubNotification::class, 'import'))->getValue($job);

        return (new \ReflectionProperty(AutoTraderApiToVehicleImport::class, 'dealershipId'))->getValue($import);
    }

    private function makeNotificationJob(array $input): HandleAutoTraderHubNotification
    {
        $notification = ApiNotification::query()->create([
            'provider' => 'auto-trader-hub',
            'data' => $input,
            'data_model' => 'vehicle',
        ]);

        return new HandleAutoTraderHubNotification($input, $notification);
    }

    private function mockVehicle(): Vehicle
    {
        $vehicle = new Vehicle();
        $vehicle->id = 1;
        $vehicle->auto_trader_id = 'STK123';
        $vehicle->is_new = true;
        $vehicle->registration_number = 'AB12 CDE';
        $vehicle->description = 'Nice car';
        $vehicle->attention_grabber = 'Super quick!';
        $vehicle->is_published = true;
        $vehicle->vin = 'VIN123456';
        $vehicle->make = (object)['name' => 'Ford'];
        $vehicle->model = (object)['name' => 'Fiesta'];
        $vehicle->derivative = 'ST-Line';
        $vehicle->odometer_mi = 12345;
        $vehicle->type = 'car';
        $vehicle->trim = 'Luxury';
        $vehicle->bodyStyle = (object)['name' => 'Hatchback'];
        $vehicle->fuelType = (object)['name' => 'Petrol'];
        $vehicle->transmission = (object)['name' => 'Manual'];
        $vehicle->drivetrain = (object)['name' => 'FWD'];
        $vehicle->seats = 5;
        $vehicle->price = 10995;
        $vehicle->door_count = 5;
        $vehicle->co2 = 99;
        $vehicle->colour = 'Red';
        $vehicle->manufacture_year = 2020;
        $vehicle->first_registration_date = '2020-01-01';
        $vehicle->previous_owner_count = 1;
        $vehicle->engine_size_cc = 1498;
        $vehicle->main_video_url = 'https://video.com/demo';

        $feature1 = new VehicleFeature();
        $feature1->name = 'Air Conditioning';
        $feature1->type = 'comfort';

        $mediaMock = $this->createMock(Media::class);
        $mediaMock->id = 123;
        $mediaMock->auto_trader_id = 'AT456';
        $mediaMock->method('getOriginalUrlAttribute')->willReturn('https://example.com/image.jpg');

        $mediaUseMock = $this->createMock(MediaUse::class);
        $mediaUseMock->method('__get')->with('media')->willReturn($mediaMock);

        $vehicle->features = collect([$feature1]);
        $vehicle->mediaUses = collect([$mediaUseMock]);

        $vehicle->dealership = (object)['data' => ['auto-trader-location-id' => 'dealer123']];

        return $vehicle;
    }

    private function invokeMethod(&$object, $methodName, array $parameters = [])
    {
        $reflection = new \ReflectionClass($object::class);
        $method = $reflection->getMethod($methodName);
        return $method->invokeArgs($object, $parameters);
    }
}
