<?php

namespace Tests\Tenant;

use App\Exports\JsonMapExport;
use App\Models\ExportMap;
use App\ExportMapRepository;
use Illuminate\Support\Facades\Storage;
use Mockery;
use Mtc\ContentManager\Models\Media as MediaModel;
use Mtc\MercuryDataModels\SalesChannelHistory;
use Mtc\MercuryDataModels\Dealership;
use Mtc\MercuryDataModels\Franchise;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleAttribute;
use Mtc\MercuryDataModels\VehicleAttributeValue;
use Mtc\MercuryDataModels\VehicleFinance;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;
use Mtc\MercuryDataModels\VehicleOffer;
use App\Facades\Site;
use Tests\TenantTestCase;
use Carbon\Carbon;

class ExportMapRepositoryTest extends TenantTestCase
{
    public function test_column_count_and_header_mapping_is_correct()
    {
        $vehicle = Vehicle::factory()->create([
            'registration_number' => 'TEST01',
            'vin' => 'VIN12345',
        ]);

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['registration_number', 'vin'],
            'headers' => ['Reg Number', 'VIN Number'],
        ]);

        // 1. Test column count
        $this->assertCount(2, $mappedData);

        // 2. Test that columns match headers in order
        $this->assertEquals($vehicle->registration_number, $mappedData[0]);
        $this->assertEquals($vehicle->vin, $mappedData[1]);
    }

    /**
     * Helper method to run the mapping process for a single entry.
     * It uses Reflection to test the protected 'mapEntries' method directly.
     *
     * @param Vehicle|VehicleOffer|Dealership $entry The data entry to process.
     * @param array $mapConfig The configuration for the ExportMap.
     * @param string $type The export map type.
     * @return array The resulting mapped data row.
     */
    private function runMapProcessing(Vehicle|VehicleOffer|Dealership $entry, array $mapConfig, string $type = 'vehicle'): array
    {
        $config = array_merge([
            'name' => 'Test Map',
            'type' => $type,
            'columns' => [],
            'mapping_rules' => [],
            'headers' => [],
        ], $mapConfig);

        $exportMap = new ExportMap($config);
        $repository = new ExportMapRepository($exportMap);

        $method = new \ReflectionMethod(ExportMapRepository::class, 'mapEntries');

        $resultCollection = $method->invoke($repository, collect([$entry]));

        return $resultCollection->first() ?? [];
    }

    // --- Operator Tests ---

    public function test_operator_equals()
    {
        $vehicle = Vehicle::factory()->create(['type' => 'Car']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Is a Car']],
            ]],
        ]);
        $this->assertEquals('Is a Car', $mappedData[0]);

        $vehicle->type = 'LCV';
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Is a Car']],
            ]],
        ]);
        $this->assertNull($mappedData[0]);
    }

    public function test_operator_not_equals()
    {
        $vehicle = Vehicle::factory()->create(['type' => 'LCV']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'type', 'op' => 'neq', 'value' => 'Car'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Not a Car']],
            ]],
        ]);
        $this->assertEquals('Not a Car', $mappedData[0]);
    }

    public function test_operator_is_any_of()
    {
        $vehicle = Vehicle::factory()->create(['colour' => 'Blue']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'colour', 'op' => 'in', 'value' => 'Red,Green,Blue'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Primary Color']],
            ]],
        ]);
        $this->assertEquals('Primary Color', $mappedData[0]);
    }

    public function test_operator_is_none_of()
    {
        $vehicle = Vehicle::factory()->create(['colour' => 'Black']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'colour', 'op' => 'not_in', 'value' => 'Red,Green,Blue'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Not a Primary Color']],
            ]],
        ]);
        $this->assertEquals('Not a Primary Color', $mappedData[0]);
    }

    public function test_operator_contains()
    {
        $vehicle = Vehicle::factory()->create(['description' => 'A beautiful family car']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'description', 'op' => 'contains', 'value' => 'family'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Contains Keyword']],
            ]],
        ]);
        $this->assertEquals('Contains Keyword', $mappedData[0]);
    }

    public function test_operator_starts_with()
    {
        $vehicle = Vehicle::factory()->create(['vin' => 'VIN12345']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'vin', 'op' => 'starts_with', 'value' => 'VIN'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Prefix Match']],
            ]],
        ]);
        $this->assertEquals('Prefix Match', $mappedData[0]);
    }

    public function test_operator_ends_with()
    {
        $vehicle = Vehicle::factory()->create(['vin' => 'VIN12345XYZ']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'vin', 'op' => 'ends_with', 'value' => 'XYZ'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Suffix Match']],
            ]],
        ]);
        $this->assertEquals('Suffix Match', $mappedData[0]);
    }

    public function test_operator_greater_than()
    {
        $vehicle = Vehicle::factory()->create(['price' => 20000]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'price', 'op' => 'gt', 'value' => '15000'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Expensive']],
            ]],
        ]);
        $this->assertEquals('Expensive', $mappedData[0]);
    }

    public function test_operator_greater_or_equal()
    {
        $vehicle = Vehicle::factory()->create(['manufacture_year' => 2022]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'manufacture_year', 'op' => 'gte', 'value' => '2022'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Modern Car']],
            ]],
        ]);
        $this->assertEquals('Modern Car', $mappedData[0]);
    }

    public function test_operator_less_than()
    {
        $vehicle = Vehicle::factory()->create(['odometer_mi' => 50000]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'odometer_mi', 'op' => 'lt', 'value' => '60000'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Low Mileage']],
            ]],
        ]);
        $this->assertEquals('Low Mileage', $mappedData[0]);
    }

    public function test_operator_less_or_equal()
    {
        $vehicle = Vehicle::factory()->create(['previous_owner_count' => 1]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'previous_owner_count', 'op' => 'lte', 'value' => '1'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Few Owners']],
            ]],
        ]);
        $this->assertEquals('Few Owners', $mappedData[0]);
    }

    public function test_operator_is_empty()
    {
        $vehicle = Vehicle::factory()->create(['trim' => null]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'trim', 'op' => 'is_empty', 'value' => null],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Trim Not Specified']],
            ]],
        ]);
        $this->assertEquals('Trim Not Specified', $mappedData[0]);
    }

    public function test_operator_is_not_empty()
    {
        $vehicle = Vehicle::factory()->create(['trim' => 'Sportline']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'trim', 'op' => 'is_not_empty', 'value' => null],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Trim Is Specified']],
            ]],
        ]);
        $this->assertEquals('Trim Is Specified', $mappedData[0]);
    }

    // --- Other Tests ---

    public function test_value_casting_for_export()
    {
        $date = Carbon::create(2025, 5, 20);
        $vehicle = Vehicle::factory()->create([
            'is_published' => true,
            'is_sold' => false,
            'first_registration_date' => $date,
        ]);

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['is_published', 'is_sold', 'first_registration_date'],
        ]);

        $this->assertEquals('Y', $mappedData[0]);
        $this->assertEquals('N', $mappedData[1]);
        $this->assertEquals('2025-05-20', $mappedData[2]);
    }

    public function test_relationship_and_custom_attribute_mapping()
    {
        $make = VehicleMake::factory()->create(['name' => 'Volkswagen']);
        $model = VehicleModel::factory()->create(['name' => 'Golf']);
        $vehicle = Vehicle::factory()->create(['make_id' => $make->id, 'model_id' => $model->id]);

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['make.name', 'model.name', 'empty'],
        ]);

        $this->assertEquals('Volkswagen', $mappedData[0]);
        $this->assertEquals('Golf', $mappedData[1]);
        $this->assertEquals('', $mappedData[2]);
    }

    // --- Offer Tests ---

    public function test_offer_column_mapping()
    {
        $make = VehicleMake::factory()->create(['name' => 'BMW']);
        $model = VehicleModel::factory()->create(['name' => 'X5']);
        $offer = VehicleOffer::factory()->create([
            'name' => 'Test Offer',
            'make_id' => $make->id,
            'model_id' => $model->id,
            'price' => 29999,
        ]);

        $mappedData = $this->runMapProcessing($offer, [
            'columns' => ['name', 'make.name', 'model.name', 'price'],
        ], 'offer');

        $this->assertCount(4, $mappedData);
        $this->assertEquals('Test Offer', $mappedData[0]);
        $this->assertEquals('BMW', $mappedData[1]);
        $this->assertEquals('X5', $mappedData[2]);
        $this->assertEquals(29999, $mappedData[3]);
    }

    public function test_offer_mapping_rules_apply()
    {
        $offer = VehicleOffer::factory()->create([
            'price' => 35000,
        ]);

        $mappedData = $this->runMapProcessing($offer, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'price', 'op' => 'gt', 'value' => '30000'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Premium']],
            ]],
        ], 'offer');

        $this->assertEquals('Premium', $mappedData[0]);
    }

    // --- Append/Prepend Action Tests ---

    public function test_action_append_adds_suffix_to_field_value()
    {
        $vehicle = Vehicle::factory()->create(['price' => 25000]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['price'],
            'mapping_rules' => [[
                'when' => ['field' => 'price', 'op' => 'is_not_empty', 'value' => null],
                'then' => [['action' => 'append', 'target' => 'price', 'value' => ' GBP']],
            ]],
        ]);
        $this->assertEquals('25000 GBP', $mappedData[0]);
    }

    public function test_action_prepend_adds_prefix_to_field_value()
    {
        $vehicle = Vehicle::factory()->create(['price' => 25000]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['price'],
            'mapping_rules' => [[
                'when' => ['field' => 'price', 'op' => 'is_not_empty', 'value' => null],
                'then' => [['action' => 'prepend', 'target' => 'price', 'value' => '£']],
            ]],
        ]);
        $this->assertEquals('£25000', $mappedData[0]);
    }

    public function test_action_append_does_not_apply_when_condition_not_met()
    {
        $vehicle = Vehicle::factory()->create(['trim' => null]);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['trim'],
            'mapping_rules' => [[
                'when' => ['field' => 'trim', 'op' => 'is_not_empty', 'value' => null],
                'then' => [['action' => 'append', 'target' => 'trim', 'value' => ' Sport']],
            ]],
        ]);
        $this->assertNull($mappedData[0]);
    }

    public function test_action_append_skips_when_wrong_type()
    {
        $vehicle = Vehicle::factory()->create(['type' => 'LCV']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                'then' => [['action' => 'append', 'target' => 'custom1', 'value' => ' suffix']],
            ]],
        ]);
        $this->assertNull($mappedData[0]);
    }

    public function test_action_append_works_with_set_in_sequence()
    {
        $vehicle = Vehicle::factory()->create(['type' => 'Car']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [
                [
                    'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                    'then' => [['action' => 'set', 'target' => 'custom1', 'value' => '25000']],
                ],
                [
                    'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                    'then' => [['action' => 'append', 'target' => 'custom1', 'value' => ' GBP']],
                ],
            ],
        ]);
        $this->assertEquals('25000 GBP', $mappedData[0]);
    }

    public function test_action_prepend_works_after_set()
    {
        $vehicle = Vehicle::factory()->create(['type' => 'Car']);
        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => [
                [
                    'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                    'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Hello']],
                ],
                [
                    'when' => ['field' => 'type', 'op' => 'eq', 'value' => 'Car'],
                    'then' => [['action' => 'prepend', 'target' => 'custom1', 'value' => 'Say: ']],
                ],
            ],
        ]);
        $this->assertEquals('Say: Hello', $mappedData[0]);
    }

    // --- Individual Image Column Tests ---

    public function test_image_1_returns_primary_image()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);
        $media3 = MediaModel::factory()->create(['src' => 'img3.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->mediaUses()->create(['media_id' => $media3->id, 'order' => 3]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_1'],
        ]);

        $this->assertEquals($media1->getOriginalUrlAttribute(true), $mappedData[0]);
    }

    public function test_image_2_returns_second_image()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);
        $media3 = MediaModel::factory()->create(['src' => 'img3.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->mediaUses()->create(['media_id' => $media3->id, 'order' => 3]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_2'],
        ]);

        $this->assertEquals($media2->getOriginalUrlAttribute(true), $mappedData[0]);
    }

    public function test_image_column_returns_empty_when_no_image_at_index()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_5'],
        ]);

        $this->assertEquals('', $mappedData[0]);
    }

    public function test_multiple_individual_image_columns()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);
        $media3 = MediaModel::factory()->create(['src' => 'img3.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->mediaUses()->create(['media_id' => $media3->id, 'order' => 3]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_1', 'image_2', 'image_3'],
        ]);

        $this->assertCount(3, $mappedData);
        $this->assertEquals($media1->getOriginalUrlAttribute(true), $mappedData[0]);
        $this->assertEquals($media2->getOriginalUrlAttribute(true), $mappedData[1]);
        $this->assertEquals($media3->getOriginalUrlAttribute(true), $mappedData[2]);
    }

    public function test_image_columns_respect_media_use_order()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'first.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'second.jpg']);
        $media3 = MediaModel::factory()->create(['src' => 'third.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 3]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media3->id, 'order' => 2]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_1', 'image_2', 'image_3'],
        ]);

        // Sorted by order: media2 (order 1), media3 (order 2), media1 (order 3)
        $this->assertEquals($media2->getOriginalUrlAttribute(true), $mappedData[0]);
        $this->assertEquals($media3->getOriginalUrlAttribute(true), $mappedData[1]);
        $this->assertEquals($media1->getOriginalUrlAttribute(true), $mappedData[2]);
    }

    // --- Image Count Tests ---

    public function test_image_count_returns_number_of_images()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);
        $media3 = MediaModel::factory()->create(['src' => 'img3.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->mediaUses()->create(['media_id' => $media3->id, 'order' => 3]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_count'],
        ]);

        $this->assertEquals(3, $mappedData[0]);
    }

    public function test_image_count_returns_zero_when_no_images()
    {
        $vehicle = Vehicle::factory()->create();
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_count'],
        ]);

        $this->assertEquals(0, $mappedData[0]);
    }

    public function test_image_count_with_other_columns()
    {
        $vehicle = Vehicle::factory()->create(['registration_number' => 'IMG01']);
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['registration_number', 'image_count'],
        ]);

        $this->assertEquals($vehicle->registration_number, $mappedData[0]);
        $this->assertEquals(2, $mappedData[1]);
    }

    public function test_offer_image_count_returns_number_of_images()
    {
        $offer = VehicleOffer::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);

        $offer->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $offer->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $offer->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($offer, [
            'columns' => ['image_count'],
        ], 'offer');

        $this->assertEquals(2, $mappedData[0]);
    }

    public function test_image_count_can_be_used_in_mapping_rules()
    {
        $vehicle = Vehicle::factory()->create();
        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);

        $vehicle->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicle->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_count', 'custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'image_count', 'op' => 'gt', 'value' => '0'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Has Images']],
            ]],
        ]);

        $this->assertEquals(2, $mappedData[0]);
        $this->assertEquals('Has Images', $mappedData[1]);
    }

    public function test_image_count_mapping_rule_not_triggered_when_no_images()
    {
        $vehicle = Vehicle::factory()->create();
        $vehicle->load('mediaUses.media');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['image_count', 'custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'image_count', 'op' => 'gt', 'value' => '0'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Has Images']],
            ]],
        ]);

        $this->assertEquals(0, $mappedData[0]);
        $this->assertNull($mappedData[1]);
    }

    // --- Image Count Query Tests ---

    public function test_image_count_query_filters_vehicles_with_images()
    {
        $vehicleWithImages = Vehicle::factory()->create();
        $vehicleWithoutImages = Vehicle::factory()->create();

        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);
        $vehicleWithImages->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);
        $vehicleWithImages->mediaUses()->create(['media_id' => $media2->id, 'order' => 2]);

        $exportMap = ExportMap::factory()->create([
            'type' => 'vehicle',
            'columns' => ['registration_number', 'image_count'],
            'conditions' => [
                [
                    'value' => [
                        'type' => 'and',
                        'column' => 'image_count',
                        'operator' => '>',
                        'value' => '0',
                    ],
                ],
            ],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $query = $repository->getQuery();
        $results = $query->get();

        $this->assertTrue($results->contains('id', $vehicleWithImages->id));
        $this->assertFalse($results->contains('id', $vehicleWithoutImages->id));
    }

    public function test_image_count_query_filters_by_minimum_count()
    {
        $vehicleWith1Image = Vehicle::factory()->create();
        $vehicleWith3Images = Vehicle::factory()->create();

        $media1 = MediaModel::factory()->create(['src' => 'img1.jpg']);
        $vehicleWith1Image->mediaUses()->create(['media_id' => $media1->id, 'order' => 1]);

        $media2 = MediaModel::factory()->create(['src' => 'img2.jpg']);
        $media3 = MediaModel::factory()->create(['src' => 'img3.jpg']);
        $media4 = MediaModel::factory()->create(['src' => 'img4.jpg']);
        $vehicleWith3Images->mediaUses()->create(['media_id' => $media2->id, 'order' => 1]);
        $vehicleWith3Images->mediaUses()->create(['media_id' => $media3->id, 'order' => 2]);
        $vehicleWith3Images->mediaUses()->create(['media_id' => $media4->id, 'order' => 3]);

        $exportMap = ExportMap::factory()->create([
            'type' => 'vehicle',
            'columns' => ['registration_number', 'image_count'],
            'conditions' => [
                [
                    'value' => [
                        'type' => 'and',
                        'column' => 'image_count',
                        'operator' => '>=',
                        'value' => '3',
                    ],
                ],
            ],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $query = $repository->getQuery();
        $results = $query->get();

        $this->assertFalse($results->contains('id', $vehicleWith1Image->id));
        $this->assertTrue($results->contains('id', $vehicleWith3Images->id));
    }

    // --- Export Map Column Definition Tests ---

    public function test_vehicle_export_map_includes_image_count_column()
    {
        $map = new \App\ExportMaps\VehicleExportMap();
        $columns = $map->columns();

        $this->assertArrayHasKey('image_count', $columns);
        $this->assertEquals('Image Count', $columns['image_count']);
    }

    public function test_vehicle_export_map_image_count_is_queryable()
    {
        $map = new \App\ExportMaps\VehicleExportMap();
        $queryable = $map->queryableColumns();

        $this->assertArrayHasKey('image_count', $queryable);
    }

    public function test_offer_export_map_includes_image_count_column()
    {
        $map = new \App\ExportMaps\OfferExportMap();
        $columns = $map->columns();

        $this->assertArrayHasKey('image_count', $columns);
        $this->assertEquals('Image Count', $columns['image_count']);
    }

    public function test_offer_export_map_image_count_is_queryable()
    {
        $map = new \App\ExportMaps\OfferExportMap();
        $queryable = $map->queryableColumns();

        $this->assertArrayHasKey('image_count', $queryable);
    }

    // --- Finance Examples Count Tests ---

    public function test_vehicle_export_map_includes_finance_examples_count_column()
    {
        $map = new \App\ExportMaps\VehicleExportMap();
        $columns = $map->columns();

        $this->assertArrayHasKey('finance_examples_count', $columns);
        $this->assertEquals('Finance Examples Count', $columns['finance_examples_count']);
    }

    public function test_vehicle_export_map_finance_examples_count_is_queryable()
    {
        $map = new \App\ExportMaps\VehicleExportMap();
        $queryable = $map->queryableColumns();

        $this->assertArrayHasKey('finance_examples_count', $queryable);
    }

    public function test_finance_examples_count_returns_number_of_finance_examples()
    {
        $vehicle = Vehicle::factory()->create();
        VehicleFinance::factory()->count(2)->create(['vehicle_id' => $vehicle->id]);
        $vehicle->load('financeExamples');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['finance_examples_count'],
        ]);

        $this->assertEquals(2, $mappedData[0]);
    }

    public function test_finance_examples_count_returns_zero_when_no_finance_examples()
    {
        $vehicle = Vehicle::factory()->create();
        $vehicle->load('financeExamples');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['finance_examples_count'],
        ]);

        $this->assertEquals(0, $mappedData[0]);
    }

    public function test_finance_examples_count_query_filters_vehicles_without_finance_examples()
    {
        Vehicle::query()->delete();
        $vehicleWithoutFinance = Vehicle::factory()->create();
        $vehicleWithFinance = Vehicle::factory()->create();
        VehicleFinance::factory()->create(['vehicle_id' => $vehicleWithFinance->id]);

        $exportMap = ExportMap::factory()->create([
            'type' => 'vehicle',
            'columns' => ['registration_number', 'finance_examples_count'],
            'conditions' => [
                [
                    'value' => [
                        'type' => 'and',
                        'column' => 'finance_examples_count',
                        'operator' => '=',
                        'value' => '0',
                    ],
                ],
            ],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $results = $repository->getQuery()->get();

        $this->assertTrue($results->contains('id', $vehicleWithoutFinance->id));
        $this->assertFalse($results->contains('id', $vehicleWithFinance->id));
    }

    public function test_finance_examples_count_query_filters_vehicles_with_finance_examples()
    {
        Vehicle::query()->delete();
        $vehicleWithoutFinance = Vehicle::factory()->create();
        $vehicleWithFinance = Vehicle::factory()->create();
        VehicleFinance::factory()->create(['vehicle_id' => $vehicleWithFinance->id]);

        $exportMap = ExportMap::factory()->create([
            'type' => 'vehicle',
            'columns' => ['registration_number', 'finance_examples_count'],
            'conditions' => [
                [
                    'value' => [
                        'type' => 'and',
                        'column' => 'finance_examples_count',
                        'operator' => '>',
                        'value' => '0',
                    ],
                ],
            ],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $results = $repository->getQuery()->get();

        $this->assertTrue($results->contains('id', $vehicleWithFinance->id));
        $this->assertFalse($results->contains('id', $vehicleWithoutFinance->id));
    }

    public function test_finance_examples_count_query_filters_by_minimum_count()
    {
        Vehicle::query()->delete();
        $vehicleWith1Finance = Vehicle::factory()->create();
        VehicleFinance::factory()->create(['vehicle_id' => $vehicleWith1Finance->id]);

        $vehicleWith3Finance = Vehicle::factory()->create();
        VehicleFinance::factory()->count(3)->create(['vehicle_id' => $vehicleWith3Finance->id]);

        $exportMap = ExportMap::factory()->create([
            'type' => 'vehicle',
            'columns' => ['registration_number', 'finance_examples_count'],
            'conditions' => [
                [
                    'value' => [
                        'type' => 'and',
                        'column' => 'finance_examples_count',
                        'operator' => '>=',
                        'value' => '3',
                    ],
                ],
            ],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $results = $repository->getQuery()->get();

        $this->assertFalse($results->contains('id', $vehicleWith1Finance->id));
        $this->assertTrue($results->contains('id', $vehicleWith3Finance->id));
    }

    // --- Site URL Tests ---

    public function test_offer_site_url_column_returns_generated_url()
    {
        $offer = VehicleOffer::factory()->create();

        $expectedUrl = Site::offerUrl($offer);

        $mappedData = $this->runMapProcessing($offer, [
            'columns' => ['site_url'],
        ], 'offer');

        $this->assertEquals($expectedUrl, $mappedData[0]);
    }

    public function test_offer_export_map_includes_site_url_column()
    {
        $map = new \App\ExportMaps\OfferExportMap();
        $columns = $map->columns();

        $this->assertArrayHasKey('site_url', $columns);
        $this->assertEquals('Site URL', $columns['site_url']);
    }

    public function test_offer_export_map_site_url_is_not_queryable()
    {
        $map = new \App\ExportMaps\OfferExportMap();
        $queryable = $map->queryableColumns();

        $this->assertArrayNotHasKey('site_url', $queryable);
    }

    public function test_offer_description_plain_strips_html_tags()
    {
        $offer = VehicleOffer::factory()->create([
            'description' => '<p>Offer valid only on vehicles registered by 30th June.</p>',
        ]);

        $mappedData = $this->runMapProcessing($offer, [
            'columns' => ['description', 'description_plain'],
        ], 'offer');

        $this->assertEquals('<p>Offer valid only on vehicles registered by 30th June.</p>', $mappedData[0]);
        $this->assertEquals('Offer valid only on vehicles registered by 30th June.', $mappedData[1]);
    }

    public function test_offer_description_plain_returns_null_when_description_is_null()
    {
        $offer = VehicleOffer::factory()->create([
            'description' => null,
        ]);

        $mappedData = $this->runMapProcessing($offer, [
            'columns' => ['description_plain'],
        ], 'offer');

        $this->assertNull($mappedData[0]);
    }

    public function test_offer_export_map_description_plain_is_not_queryable()
    {
        $map = new \App\ExportMaps\OfferExportMap();
        $queryable = $map->queryableColumns();

        $this->assertArrayNotHasKey('description_plain', $queryable);
    }

    public function test_mapping_rule_value_resolves_field_placeholders()
    {
        $vehicle = Vehicle::factory()->create([
            'monthly_price' => 200,
            'description' => 'Great car.',
        ]);

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['description', 'monthly_price'],
            'mapping_rules' => [[
                'when' => ['field' => 'monthly_price', 'op' => 'is_not_empty', 'value' => null],
                'then' => [['action' => 'prepend', 'target' => 'description', 'value' => 'Monthly price ${{monthly_price}} per month. ']],
            ]],
        ]);

        $this->assertEquals('Monthly price $200 per month. Great car.', $mappedData[0]);
    }

    public function test_export_resolves_single_value_vehicle_attribute()
    {
        $vehicle = Vehicle::factory()->create();
        $attribute = VehicleAttribute::factory()->create([
            'name' => 'Battery Type',
            'slug' => 'battery_type',
            'type' => 'text',
            'model' => 'vehicle',
            'count' => 1,
        ]);
        VehicleAttributeValue::create([
            'owner_type' => $vehicle->getMorphClass(),
            'owner_id' => $vehicle->id,
            'attribute_id' => $attribute->id,
            'slug' => $attribute->slug,
            'type' => $attribute->type,
            'value_text' => 'Lithium-Ion',
            'index' => 0,
        ]);

        $mappedData = $this->runMapProcessing($vehicle->fresh(), [
            'columns' => ['attribute.battery_type'],
        ]);

        $this->assertEquals('Lithium-Ion', $mappedData[0]);
    }

    public function test_export_implodes_multi_value_vehicle_attribute_with_comma()
    {
        $vehicle = Vehicle::factory()->create();
        $attribute = VehicleAttribute::factory()->create([
            'name' => 'Selling Points',
            'slug' => 'selling_points',
            'type' => 'text',
            'model' => 'vehicle',
            'count' => 3,
        ]);
        foreach (['Heated seats', 'Reverse camera', 'Sat nav'] as $index => $value) {
            VehicleAttributeValue::create([
                'owner_type' => $vehicle->getMorphClass(),
                'owner_id' => $vehicle->id,
                'attribute_id' => $attribute->id,
                'slug' => $attribute->slug,
                'type' => $attribute->type,
                'value_text' => $value,
                'index' => $index,
            ]);
        }

        $mappedData = $this->runMapProcessing($vehicle->fresh(), [
            'columns' => ['attribute.selling_points'],
        ]);

        $this->assertEquals('Heated seats,Reverse camera,Sat nav', $mappedData[0]);
    }

    public function test_export_returns_null_for_unset_vehicle_attribute()
    {
        $vehicle = Vehicle::factory()->create();
        VehicleAttribute::factory()->create([
            'name' => 'Battery Type',
            'slug' => 'battery_type',
            'type' => 'text',
            'model' => 'vehicle',
            'count' => 1,
        ]);

        $mappedData = $this->runMapProcessing($vehicle->fresh(), [
            'columns' => ['attribute.battery_type'],
        ]);

        $this->assertNull($mappedData[0]);
    }

    public function test_vehicle_export_map_lists_only_vehicle_attributes()
    {
        VehicleAttribute::factory()->create([
            'name' => 'Battery Type',
            'slug' => 'battery_type',
            'type' => 'text',
            'model' => 'vehicle',
            'count' => 1,
        ]);
        VehicleAttribute::factory()->create([
            'name' => 'Offer Promo',
            'slug' => 'offer_promo',
            'type' => 'text',
            'model' => 'offer',
            'count' => 1,
        ]);

        $columns = (new \App\ExportMaps\VehicleExportMap())->columns(false);

        $this->assertArrayHasKey('attribute.battery_type', $columns);
        $this->assertArrayNotHasKey('attribute.offer_promo', $columns);
    }

    // --- Dealership Tests ---

    public function test_dealership_column_mapping()
    {
        $dealership = Dealership::factory()->create([
            'name' => 'Acme Motors',
            'email' => 'sales@acme.test',
            'postcode' => 'AB1 2CD',
            'address1' => '1 High Street',
            'contact_no' => '01234 567890',
        ]);

        $mappedData = $this->runMapProcessing($dealership, [
            'columns' => ['name', 'email', 'postcode', 'address1', 'contact_no'],
        ], 'dealership');

        $this->assertCount(5, $mappedData);
        $this->assertEquals('Acme Motors', $mappedData[0]);
        $this->assertEquals('sales@acme.test', $mappedData[1]);
        $this->assertEquals('AB1 2CD', $mappedData[2]);
        $this->assertEquals('1 High Street', $mappedData[3]);
        $this->assertEquals('01234 567890', $mappedData[4]);
    }

    public function test_dealership_relationship_mapping_franchise_name()
    {
        $franchise = Franchise::factory()->create(['name' => 'Volkswagen Group']);
        $dealership = Dealership::factory()->create(['franchise_id' => $franchise->id]);

        $mappedData = $this->runMapProcessing($dealership->fresh(), [
            'columns' => ['franchise.name'],
        ], 'dealership');

        $this->assertEquals('Volkswagen Group', $mappedData[0]);
    }

    public function test_dealership_mapping_rules_apply()
    {
        $dealership = Dealership::factory()->create(['active' => true]);

        $mappedData = $this->runMapProcessing($dealership, [
            'columns' => ['custom1'],
            'mapping_rules' => [[
                'when' => ['field' => 'active', 'op' => 'eq', 'value' => 'Y'],
                'then' => [['action' => 'set', 'target' => 'custom1', 'value' => 'Live']],
            ]],
        ], 'dealership');

        $this->assertEquals('Live', $mappedData[0]);
    }

    public function test_dealership_export_map_query_returns_dealership_builder()
    {
        $map = new \App\ExportMaps\DealershipExportMap();
        $builder = $map->query();

        $this->assertInstanceOf(Dealership::class, $builder->getModel());
    }

    public function test_dealership_export_map_includes_core_columns()
    {
        $columns = (new \App\ExportMaps\DealershipExportMap())->columns(false);

        $this->assertArrayHasKey('name', $columns);
        $this->assertArrayHasKey('email', $columns);
        $this->assertArrayHasKey('postcode', $columns);
        $this->assertArrayHasKey('franchise.name', $columns);
        $this->assertArrayHasKey('custom1', $columns);
        $this->assertArrayHasKey('empty', $columns);
    }

    public function test_dealership_export_map_excludes_custom_and_empty_from_queryable()
    {
        $queryable = (new \App\ExportMaps\DealershipExportMap())->queryableColumns(false);

        $this->assertArrayNotHasKey('custom1', $queryable);
        $this->assertArrayNotHasKey('custom6', $queryable);
        $this->assertArrayNotHasKey('empty', $queryable);
    }

    public function test_dealership_export_map_includes_integration_data_fields()
    {
        $columns = (new \App\ExportMaps\DealershipExportMap())->columns(false);

        // HeyCar is registered as a sales channel and exposes 'hey-car-dealer-id'
        // via dealershipAdditionalDataFields() — auto-discovery should surface it.
        $this->assertArrayHasKey('data.hey-car-dealer-id', $columns);
    }

    // --- Eager loading derived from mapping-rule field paths ---

    public function test_relationships_in_columns_are_pulled_from_mapping_rule_sources()
    {
        $exportMap = new ExportMap([
            'name' => 'Test Map',
            'type' => 'vehicle',
            'columns' => ['uuid', 'custom2', 'custom1'],
            'mapping_rules' => [
                [
                    'when' => ['field' => 'uuid', 'op' => 'is_not_empty', 'value' => null],
                    'then' => [
                        $this->mergeAction('custom2', 'dealership.address1', ', '),
                        $this->mergeAction('custom1', 'make.name', '; '),
                        $this->mergeAction('custom1', 'model.name', '; '),
                        $this->mergeAction('custom1', 'bodyStyle.name', '; '),
                        $this->mergeAction('custom1', 'condition', '; '),
                    ],
                ],
            ],
            'headers' => ['ID', 'Item address', 'Contextual keywords'],
        ]);
        $repository = new ExportMapRepository($exportMap);

        $method = new \ReflectionMethod(ExportMapRepository::class, 'relationshipsInColumns');
        $relationships = $method->invoke($repository);

        // Relation paths discovered from the dotted rule sources.
        $this->assertContains('dealership', $relationships);
        $this->assertContains('make', $relationships);
        $this->assertContains('model', $relationships);
        $this->assertContains('bodyStyle', $relationships);

        // Scalar source ('condition') and custom output slots are not relationships.
        $this->assertNotContains('condition', $relationships);
        $this->assertNotContains('custom1', $relationships);
    }

    // --- Computed columns built via the merge action ---

    public function test_merge_joins_address_parts_filtering_empty_components()
    {
        $dealership = Dealership::factory()->create([
            'address1' => '1 High Street',
            'address2' => null,
            'city' => 'Dublin',
            'county' => null,
            'postcode' => null,
            'country' => 'Ireland',
        ]);
        $vehicle = Vehicle::factory()->create([
            'uuid' => 'web-merge-address',
            'dealership_id' => $dealership->id,
        ]);
        $vehicle->load('dealership');

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom2'],
            'mapping_rules' => [
                [
                    'when' => ['field' => 'uuid', 'op' => 'is_not_empty', 'value' => null],
                    'then' => [
                        $this->mergeAction('custom2', 'dealership.address1', ', '),
                        $this->mergeAction('custom2', 'dealership.address2', ', '),
                        $this->mergeAction('custom2', 'dealership.city', ', '),
                        $this->mergeAction('custom2', 'dealership.county', ', '),
                        $this->mergeAction('custom2', 'dealership.postcode', ', '),
                        $this->mergeAction('custom2', 'dealership.country', ', '),
                    ],
                ],
            ],
        ]);

        // Empty address2/county/postcode dropped — no dangling separators.
        $this->assertEquals('1 High Street, Dublin, Ireland', $mappedData[0]);
    }

    public function test_merge_builds_contextual_keywords_with_conditional_for_sale_token()
    {
        $make = VehicleMake::factory()->create(['name' => 'Volkswagen']);
        $model = VehicleModel::factory()->create(['name' => 'Golf']);
        $vehicle = Vehicle::factory()->create([
            'uuid' => 'web-merge-keywords',
            'make_id' => $make->id,
            'model_id' => $model->id,
            'is_new' => false,
        ]);
        $vehicle->load(['make', 'model', 'bodyStyle']);

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => $this->contextualKeywordRules(),
        ]);

        // condition is a computed accessor: is_new ? 'new' : 'used'.
        $this->assertStringContainsString('Volkswagen', $mappedData[0]);
        $this->assertStringContainsString('Golf', $mappedData[0]);
        $this->assertStringContainsString('used', $mappedData[0]);
        $this->assertStringContainsString('Volkswagen Golf for sale', $mappedData[0]);
        $this->assertStringContainsString('; ', $mappedData[0]);
    }

    public function test_merge_omits_for_sale_token_when_make_and_model_absent()
    {
        // No make/model: the original derived column produced no "for sale" token at all.
        // The scratch-slot guard (when kw_make_model is_not_empty) preserves that behaviour.
        $vehicle = Vehicle::factory()->create([
            'uuid' => 'web-merge-no-make-model',
            'make_id' => null,
            'model_id' => null,
            'is_new' => false,
        ]);
        $vehicle->load(['make', 'model', 'bodyStyle']);

        $mappedData = $this->runMapProcessing($vehicle, [
            'columns' => ['custom1'],
            'mapping_rules' => $this->contextualKeywordRules(),
        ]);

        // The "<make> <model> for sale" token is omitted entirely when both are absent.
        $this->assertStringNotContainsString('for sale', $mappedData[0]);
        // condition still resolves, and there is no leading separator from the skipped parts.
        $this->assertStringContainsString('used', $mappedData[0]);
        $this->assertFalse(str_starts_with($mappedData[0], ';'));
    }

    // --- Export delivery: file verification & logging ---

    public function test_send_via_ftp_verifies_file_exists_and_records_success()
    {
        Storage::fake('export-map');

        $exportMap = $this->deliveryExportMap();
        $repository = new ExportMapRepository($exportMap);
        $repository->data = collect([]);

        $this->invokeSendViaFpt($repository, new JsonMapExport(collect([]), $exportMap));

        Storage::disk('export-map')->assertExists('feed.json');

        $history = SalesChannelHistory::query()->where('channel', $exportMap->salesChannelKey())->first();
        $this->assertNotNull($history);
        $this->assertTrue($history->was_successful);
        $this->assertStringContainsString('records exported to feed.json', $history->details);
        $this->assertTrue($history->data['file_verified']);
        $this->assertEquals('feed.json', $history->data['filename']);
    }

    public function test_send_via_ftp_records_failure_when_file_missing_after_upload()
    {
        $exportMap = $this->deliveryExportMap();

        $disk = Mockery::mock();
        $disk->shouldReceive('put')->andReturnTrue();
        $disk->shouldReceive('exists')->andReturnFalse();
        Storage::shouldReceive('disk')->with('export-map')->andReturn($disk);

        $repository = new ExportMapRepository($exportMap);
        $repository->data = collect([]);

        $this->invokeSendViaFpt($repository, new JsonMapExport(collect([]), $exportMap));

        $history = SalesChannelHistory::query()->where('channel', $exportMap->salesChannelKey())->first();
        $this->assertNotNull($history);
        $this->assertFalse($history->was_successful);
        $this->assertStringContainsString('was not found on the destination disk', $history->details);
        $this->assertFalse($history->data['file_verified']);
    }

    public function test_send_via_ftp_diverts_real_destination_to_local_disk_off_production()
    {
        Storage::fake('export-map');

        // A live client SFTP destination — exactly what must never be contacted off-production.
        $exportMap = ExportMap::factory()->create([
            'name' => 'JSON Feed',
            'type' => 'vehicle',
            'format' => 'json',
            'driver' => 'sftp',
            'host' => 'sftp.live-client.example',
            'root' => '/live/feeds',
            'username' => 'client',
            'password' => 'secret',
            'filename' => 'feed.json',
            'ftp_export_enabled' => true,
            'columns' => [],
            'headers' => [],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $repository->data = collect([]);

        $this->invokeSendViaFpt($repository, new JsonMapExport(collect([]), $exportMap));

        // The disk was rebuilt as a local disk; the real host/credentials are gone.
        $disk = config('filesystems.disks.export-map');
        $this->assertSame('local', $disk['driver']);
        $this->assertArrayNotHasKey('host', $disk);
        $this->assertArrayNotHasKey('username', $disk);
        $this->assertStringEndsWith('fake-exports', $disk['root']);

        Storage::disk('export-map')->assertExists('feed.json');

        $history = SalesChannelHistory::query()->where('channel', $exportMap->salesChannelKey())->first();
        $this->assertTrue($history->data['diverted']);
        $this->assertSame('local', $history->data['driver']);
        $this->assertStringContainsString('diverted to a local disk', $history->details);
    }

    public function test_send_via_ftp_uses_configured_destination_on_production()
    {
        $this->app['env'] = 'production';
        Storage::fake('export-map');

        // Driver 'local' keeps the test off the network while still proving the map's own
        // destination config is what gets used on production (no diversion).
        $exportMap = ExportMap::factory()->create([
            'name' => 'JSON Feed',
            'type' => 'vehicle',
            'format' => 'json',
            'driver' => 'local',
            'host' => 'sftp.live-client.example',
            'root' => '/live/feeds',
            'filename' => 'feed.json',
            'ftp_export_enabled' => true,
            'columns' => [],
            'headers' => [],
        ]);

        $repository = new ExportMapRepository($exportMap);
        $repository->data = collect([]);

        $this->invokeSendViaFpt($repository, new JsonMapExport(collect([]), $exportMap));

        $disk = config('filesystems.disks.export-map');
        $this->assertSame('sftp.live-client.example', $disk['host']);
        $this->assertSame('/live/feeds', $disk['root']);

        $history = SalesChannelHistory::query()->where('channel', $exportMap->salesChannelKey())->first();
        $this->assertFalse($history->data['diverted']);
        $this->assertStringNotContainsString('diverted', $history->details);
    }

    public function test_send_via_ftp_records_success_when_ssl_shutdown_error_follows_successful_transfer()
    {
        $this->app['env'] = 'production';

        $exportMap = $this->deliveryExportMap();

        // ftp_fput() surfaces the OpenSSL 3.x "unexpected eof" during TLS teardown as a thrown
        // write failure, even though the bytes already landed; the re-verification confirms delivery.
        $disk = Mockery::mock();
        $disk->shouldReceive('put')->andThrow(new \Exception(
            'ftp_fput(): SSL_read on shutdown: error:0A000126:SSL routines::unexpected eof while reading'
        ));
        $disk->shouldReceive('exists')->andReturnTrue();
        Storage::shouldReceive('disk')->with('export-map')->andReturn($disk);

        $repository = new ExportMapRepository($exportMap);
        $repository->data = collect([]);

        $this->invokeSendViaFpt($repository, new JsonMapExport(collect([]), $exportMap));

        $history = SalesChannelHistory::query()->where('channel', $exportMap->salesChannelKey())->first();
        $this->assertNotNull($history);
        $this->assertTrue($history->was_successful);
        $this->assertStringContainsString('delivered despite a transport error', $history->details);
        $this->assertTrue($history->data['file_verified']);
    }

    public function test_send_via_ftp_records_failure_when_write_throws_and_file_absent()
    {
        $this->app['env'] = 'production';

        $exportMap = $this->deliveryExportMap();

        $disk = Mockery::mock();
        $disk->shouldReceive('put')->andThrow(new \Exception('Connection refused'));
        $disk->shouldReceive('exists')->andReturnFalse();
        Storage::shouldReceive('disk')->with('export-map')->andReturn($disk);

        $repository = new ExportMapRepository($exportMap);
        $repository->data = collect([]);

        $this->invokeSendViaFpt($repository, new JsonMapExport(collect([]), $exportMap));

        $history = SalesChannelHistory::query()->where('channel', $exportMap->salesChannelKey())->first();
        $this->assertNotNull($history);
        $this->assertFalse($history->was_successful);
        $this->assertStringContainsString('Connection refused', $history->details);
        $this->assertFalse($history->data['file_verified']);
    }

    private function deliveryExportMap(): ExportMap
    {
        return ExportMap::factory()->create([
            'name' => 'JSON Feed',
            'type' => 'vehicle',
            'format' => 'json',
            'driver' => 'local',
            'filename' => 'feed.json',
            'ftp_export_enabled' => true,
            'columns' => [],
            'headers' => [],
        ]);
    }

    private function invokeSendViaFpt(ExportMapRepository $repository, $export): void
    {
        (new \ReflectionMethod(ExportMapRepository::class, 'sendViaFpt'))->invoke($repository, $export);
    }

    private function mergeAction(string $target, string $source, string $separator): array
    {
        return ['action' => 'merge', 'target' => $target, 'source' => $source, 'separator' => $separator];
    }

    /**
     * The contextual-keywords rule chain used by the Google Display template:
     * semicolon-joined make/model/body/condition, plus a scratch "<make> <model> for sale"
     * token that is only appended/folded in when make or model produced content.
     */
    private function contextualKeywordRules(): array
    {
        return [
            [
                'when' => ['field' => 'uuid', 'op' => 'is_not_empty', 'value' => null],
                'then' => [
                    $this->mergeAction('custom1', 'make.name', '; '),
                    $this->mergeAction('custom1', 'model.name', '; '),
                    $this->mergeAction('custom1', 'bodyStyle.name', '; '),
                    $this->mergeAction('custom1', 'condition', '; '),
                    $this->mergeAction('kw_make_model', 'make.name', ' '),
                    $this->mergeAction('kw_make_model', 'model.name', ' '),
                ],
            ],
            [
                'when' => ['field' => 'kw_make_model', 'op' => 'is_not_empty', 'value' => null],
                'then' => [
                    ['action' => 'append', 'target' => 'kw_make_model', 'value' => ' for sale'],
                    $this->mergeAction('custom1', 'kw_make_model', '; '),
                ],
            ],
        ];
    }
}
