<?php

namespace Tests\Tenant;

use App\BookingRepository;
use App\TaxonomyMap;
use Illuminate\Support\Facades\App;
use Mtc\MercuryDataModels\Booking;
use Mtc\MercuryDataModels\ServicePackage;
use Mtc\MercuryDataModels\VehicleMake;
use Mtc\MercuryDataModels\VehicleModel;
use Mtc\VehicleLookup\VehicleLookupService;
use Ramsey\Uuid\Uuid;
use Tests\DatabaseTestCase;

class ServiceBookingPackageFilterTest extends DatabaseTestCase
{
    private const PROVIDER = 'auto-trader';
    private const FORD_MAKE_ID = 1;
    private const BMW_MAKE_ID = 2;
    private const FOCUS_MODEL_ID = 10;
    private const FIESTA_MODEL_ID = 11;

    public function testReturnsUniversalAndMatchingPackagesForBookingMake(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->mapMake('Ford', self::FORD_MAKE_ID);

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);
        $this->createPackage('BMW Tyres', self::BMW_MAKE_ID);

        $booking = $this->createBookingWithMake('Ford');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testReturnsModelSpecificPackageForMatchingMakeAndModel(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');
        $this->createVehicleModel(self::FOCUS_MODEL_ID, self::FORD_MAKE_ID, 'Focus');

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);
        $focusOnly = $this->createPackage('Focus Brakes', self::FORD_MAKE_ID, self::FOCUS_MODEL_ID);

        $booking = $this->createBookingWithMakeAndModel('Ford', 'Focus');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id, $focusOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testHidesModelSpecificPackageWhenModelDiffers(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');
        $this->createVehicleModel(self::FOCUS_MODEL_ID, self::FORD_MAKE_ID, 'Focus');
        $this->createVehicleModel(self::FIESTA_MODEL_ID, self::FORD_MAKE_ID, 'Fiesta');

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);
        $this->createPackage('Focus Brakes', self::FORD_MAKE_ID, self::FOCUS_MODEL_ID);

        $booking = $this->createBookingWithMakeAndModel('Ford', 'Fiesta');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testExactVehicleModelMatchIsScopedByMake(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');
        $this->createVehicleMake(self::BMW_MAKE_ID, 'BMW');
        // Same model name under two makes - only the Ford one should be resolved.
        $this->createVehicleModel(self::FIESTA_MODEL_ID, self::BMW_MAKE_ID, 'Focus');
        $this->createVehicleModel(self::FOCUS_MODEL_ID, self::FORD_MAKE_ID, 'Focus');

        $universal = $this->createPackage('Universal Service', null);
        $fordFocus = $this->createPackage('Ford Focus Brakes', self::FORD_MAKE_ID, self::FOCUS_MODEL_ID);
        $this->createPackage('BMW Focus Brakes', self::BMW_MAKE_ID, self::FIESTA_MODEL_ID);

        $booking = $this->createBookingWithMakeAndModel('Ford', 'Focus');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordFocus->id],
            $packages->pluck('id')->all()
        );
    }

    public function testFallsBackToTaxonomyMapForUnmatchedModel(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');
        $this->mapModel('FOCUS 1.0 ECOBOOST', self::FOCUS_MODEL_ID);

        $universal = $this->createPackage('Universal Service', null);
        $focusOnly = $this->createPackage('Focus Brakes', self::FORD_MAKE_ID, self::FOCUS_MODEL_ID);
        $this->createPackage('Fiesta Brakes', self::FORD_MAKE_ID, self::FIESTA_MODEL_ID);

        $booking = $this->createBookingWithMakeAndModel('Ford', 'FOCUS 1.0 ECOBOOST');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $focusOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testHidesModelSpecificPackagesWhenBookingHasNoModel(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);
        // No model on the booking, so the model-specific package must not be returned.
        $this->createPackage('Focus Brakes', self::FORD_MAKE_ID, self::FOCUS_MODEL_ID);

        $booking = $this->createBookingWithMake('Ford');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testExactVehicleMakeMatchIsPreferredOverTaxonomyMap(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');
        $this->mapMake('Ford', self::BMW_MAKE_ID);

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);
        $this->createPackage('BMW Tyres', self::BMW_MAKE_ID);

        $booking = $this->createBookingWithMake('Ford');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testFallsBackToTaxonomyMapWhenNoExactVehicleMakeMatch(): void
    {
        $this->forceLookupProvider(self::PROVIDER);
        $this->createVehicleMake(self::FORD_MAKE_ID, 'Ford');
        $this->mapMake('FORD MOTOR CO', self::FORD_MAKE_ID);

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);
        $this->createPackage('BMW Tyres', self::BMW_MAKE_ID);

        $booking = $this->createBookingWithMake('FORD MOTOR CO');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testReturnsAllPackagesWhenBookingHasNoVehicleMake(): void
    {
        $this->forceLookupProvider(self::PROVIDER);

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);

        $booking = Booking::factory()->create([
            'uuid' => Uuid::uuid4()->toString(),
            'vehicle_data' => [],
        ]);

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testReturnsAllPackagesWhenMakeIsUnmapped(): void
    {
        $this->forceLookupProvider(self::PROVIDER);

        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);

        $booking = $this->createBookingWithMake('UnknownMake');

        $packages = App::make(BookingRepository::class)->getPackages($booking);

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    public function testReturnsAllPackagesWhenNoBookingProvided(): void
    {
        $universal = $this->createPackage('Universal Service', null);
        $fordOnly = $this->createPackage('Ford Tyres', self::FORD_MAKE_ID);

        $packages = App::make(BookingRepository::class)->getPackages();

        $this->assertEqualsCanonicalizing(
            [$universal->id, $fordOnly->id],
            $packages->pluck('id')->all()
        );
    }

    private function forceLookupProvider(string $provider): void
    {
        $this->app->singleton(VehicleLookupService::class);
        App::make(VehicleLookupService::class)->setDriver($provider);
    }

    private function mapMake(string $term, int $makeId): TaxonomyMap
    {
        return TaxonomyMap::query()->create([
            'provider' => self::PROVIDER,
            'taxonomy_type' => TaxonomyMap::MAKE,
            'term' => $term,
            'taxonomy_id' => $makeId,
        ]);
    }

    private function createPackage(
        string $name,
        ?int $applicableMakeId,
        ?int $applicableModelId = null
    ): ServicePackage {
        return ServicePackage::query()->create([
            'name' => $name,
            'applicable_make_id' => $applicableMakeId,
            'applicable_model_id' => $applicableModelId,
            'order' => 0,
            'price' => 100,
        ]);
    }

    private function createBookingWithMake(string $makeName): Booking
    {
        return Booking::factory()->create([
            'uuid' => Uuid::uuid4()->toString(),
            'vehicle_data' => ['make' => $makeName],
        ]);
    }

    private function createBookingWithMakeAndModel(string $makeName, string $modelName): Booking
    {
        return Booking::factory()->create([
            'uuid' => Uuid::uuid4()->toString(),
            'vehicle_data' => ['make' => $makeName, 'model' => $modelName],
        ]);
    }

    private function createVehicleMake(int $id, string $name): VehicleMake
    {
        return VehicleMake::query()->create([
            'id' => $id,
            'name' => $name,
        ]);
    }

    private function createVehicleModel(int $id, int $makeId, string $name): VehicleModel
    {
        return VehicleModel::query()->create([
            'id' => $id,
            'make_id' => $makeId,
            'name' => $name,
        ]);
    }

    private function mapModel(string $term, int $modelId): TaxonomyMap
    {
        return TaxonomyMap::query()->create([
            'provider' => self::PROVIDER,
            'taxonomy_type' => TaxonomyMap::MODEL,
            'term' => $term,
            'taxonomy_id' => $modelId,
        ]);
    }
}
