<?php

namespace Tests\Feature;

use App\Jobs\RemoveOrphanedTaxonomiesFromModelsJob;
use App\Mail\OrphanedTaxonomyRemovedMail;
use App\Services\MeilisearchService;
use App\TaxonomyMap;
use App\TaxonomyMapable;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Mail;
use Mockery;
use Mtc\MercuryDataModels\NewCar;
use Mtc\MercuryDataModels\Tenant;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleOffer;
use Tests\DatabaseTestCase;

class RemoveOrphanedTaxonomyMapsTest extends DatabaseTestCase
{
    public function testJobDispatch()
    {
        Bus::fake();
        Artisan::call('taxonomy:cleanup');
        Bus::assertDispatched(RemoveOrphanedTaxonomiesFromModelsJob::class);
    }

    public function testDelete()
    {
        Mail::fake();

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-fuel_type',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $this->assertCount(1, TaxonomyMap::all());

        Artisan::call('taxonomy:cleanup');

        $this->assertCount(0, TaxonomyMap::all());
    }

    public function testUpdateMakeIds()
    {
        Mail::fake();

        Tenant::query()->update([
            'name' => 'test_tenant',
        ]);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-make',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-model',
            'taxonomy_id' => 456,
            'term' => 'baz',
            'provider' => 'buzz',
        ]);

        Vehicle::factory()->create([
            'make_id' => 123,
            'model_id' => 456,
        ]);

        Vehicle::factory()->create([
            'make_id' => 789,
            'model_id' => 555,
        ]);

        VehicleOffer::factory()->create([
            'make_id' => 123,
            'model_id' => 456,
        ]);

        NewCar::factory()->create([
            'make_id' => 123,
            'model_id' => 456,
        ]);

        $orphaned_maps = TaxonomyMap::all();

        (new RemoveOrphanedTaxonomiesFromModelsJob($orphaned_maps))->handle();

        // assert that we set make_id and model_id to NULL for affected vehicles
        $this->assertCount(
            1,
            Vehicle::query()
                ->whereNull('make_id')
                ->whereNull('model_id')
                ->get()
        );

        // assert that we set make_id and model_id to NULL for affected vehicle offers
        $this->assertCount(
            1,
            VehicleOffer::query()
                ->whereNull('make_id')
                ->whereNull('model_id')
                ->get()
        );

        // assert that we set make_id and model_id to NULL for affected new cars
        $this->assertCount(
            1,
            NewCar::query()
                ->whereNull('make_id')
                ->whereNull('model_id')
                ->get()
        );

        // assert that we left make_id and model_id unchanged for unaffected vehicles
        $this->assertCount(
            1,
            Vehicle::query()
                ->where('make_id', 789)
                ->where('model_id', 555)
                ->get()
        );
    }

    public function testJobDeletesOrphanedMaps()
    {
        Mail::fake();

        Tenant::query()->update([
            'name' => 'test_tenant',
        ]);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'make',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $this->assertCount(1, TaxonomyMap::all());

        $orphaned_maps = TaxonomyMap::all();

        (new RemoveOrphanedTaxonomiesFromModelsJob($orphaned_maps))->handle();

        $this->assertCount(0, TaxonomyMap::all());
    }

    public function testNotificationExcludesTenantsWithNoAffectedRecords()
    {
        Mail::fake();

        Tenant::query()->update([
            'name' => 'test_tenant',
        ]);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'make',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $orphaned_maps = TaxonomyMap::all();

        (new RemoveOrphanedTaxonomiesFromModelsJob($orphaned_maps))->handle();

        Mail::assertSent(OrphanedTaxonomyRemovedMail::class, function (OrphanedTaxonomyRemovedMail $mail) {
            return $mail->assertDontSeeInHtml('test_tenant');
        });
    }

    public function testDeletesNonMakeModelOrphansWithoutNotification()
    {
        Mail::fake();

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-fuel_type',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $this->assertCount(1, TaxonomyMap::all());

        $orphaned_maps = TaxonomyMap::all();

        (new RemoveOrphanedTaxonomiesFromModelsJob($orphaned_maps))->handle();

        $this->assertCount(0, TaxonomyMap::all());
        Mail::assertNotSent(OrphanedTaxonomyRemovedMail::class);
    }

    public function testKeepsOrphanedMapWithLiveMapable()
    {
        Mail::fake();

        Tenant::query()->update(['name' => 'test_tenant']);
        $tenant = Tenant::query()->first();

        $map = TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-fuel_type',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $vehicle = Vehicle::factory()->create();

        $mapable = TaxonomyMapable::query()->create([
            'tenant' => $tenant->getKey(),
            'taxonomy_map_id' => $map->id,
            'mappable_type' => $vehicle->getMorphClass(),
            'mappable_id' => $vehicle->id,
        ]);

        (new RemoveOrphanedTaxonomiesFromModelsJob(TaxonomyMap::all()))->handle();

        // the map is still referenced by a live vehicle, so neither it nor the mapable is removed
        $this->assertNotNull(TaxonomyMap::query()->find($map->id));
        $this->assertNotNull(TaxonomyMapable::query()->find($mapable->id));
    }

    public function testDeletesOrphanedMapWithOnlyDeadMapables()
    {
        Mail::fake();

        Tenant::query()->update(['name' => 'test_tenant']);
        $tenant = Tenant::query()->first();

        $map = TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-fuel_type',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $deadMapable = TaxonomyMapable::query()->create([
            'tenant' => $tenant->getKey(),
            'taxonomy_map_id' => $map->id,
            'mappable_type' => (new Vehicle())->getMorphClass(),
            'mappable_id' => 999999,
        ]);

        (new RemoveOrphanedTaxonomiesFromModelsJob(TaxonomyMap::all()))->handle();

        // no live reference remains, so the map and its dead mapable are removed
        $this->assertNull(TaxonomyMap::query()->find($map->id));
        $this->assertNull(TaxonomyMapable::query()->find($deadMapable->id));
    }

    public function testPrunesDeadMapableButKeepsMapWithLiveMapable()
    {
        Mail::fake();

        Tenant::query()->update(['name' => 'test_tenant']);
        $tenant = Tenant::query()->first();

        $map = TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-fuel_type',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        $vehicle = Vehicle::factory()->create();

        $liveMapable = TaxonomyMapable::query()->create([
            'tenant' => $tenant->getKey(),
            'taxonomy_map_id' => $map->id,
            'mappable_type' => $vehicle->getMorphClass(),
            'mappable_id' => $vehicle->id,
        ]);

        $deadMapable = TaxonomyMapable::query()->create([
            'tenant' => $tenant->getKey(),
            'taxonomy_map_id' => $map->id,
            'mappable_type' => $vehicle->getMorphClass(),
            'mappable_id' => 999999,
        ]);

        (new RemoveOrphanedTaxonomiesFromModelsJob(TaxonomyMap::all()))->handle();

        // the map survives on the strength of its live mapable, but the dead mapable is pruned
        $this->assertNotNull(TaxonomyMap::query()->find($map->id));
        $this->assertNotNull(TaxonomyMapable::query()->find($liveMapable->id));
        $this->assertNull(TaxonomyMapable::query()->find($deadMapable->id));
    }

    public function testRefreshesSearchIndexForAffectedVehiclesInBatches()
    {
        Mail::fake();

        Tenant::query()->update(['name' => 'test_tenant']);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-make',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        // Created while Meilisearch is disabled so the observer doesn't touch the mock.
        $published = Vehicle::factory()->create([
            'make_id' => 123,
            'is_published' => true,
            'stock_provider' => null,
        ]);

        $unpublished = Vehicle::factory()->create([
            'make_id' => 123,
            'is_published' => false,
            'stock_provider' => null,
        ]);

        $providerStock = Vehicle::factory()->create([
            'make_id' => 123,
            'is_published' => true,
            'stock_provider' => 'autotrader',
        ]);

        $meilisearch = Mockery::mock(MeilisearchService::class);
        $meilisearch->shouldReceive('isEnabled')->andReturnTrue();
        $meilisearch->shouldReceive('indexVehicles')
            ->once()
            ->with(Mockery::on(function ($chunk) use ($published, $unpublished, $providerStock) {
                $ids = collect($chunk)->pluck('id');

                return $ids->contains($published->id)
                    && !$ids->contains($unpublished->id)
                    && !$ids->contains($providerStock->id);
            }));
        $meilisearch->shouldReceive('deleteVehicle')->once()->with($unpublished->id);

        $this->app->instance(MeilisearchService::class, $meilisearch);

        (new RemoveOrphanedTaxonomiesFromModelsJob(TaxonomyMap::all()))->handle();

        // the orphaned make is cleared regardless of publish state
        $this->assertNull($published->fresh()->make_id);
        $this->assertNull($unpublished->fresh()->make_id);
        $this->assertNull($providerStock->fresh()->make_id);
    }

    public function testMail()
    {
        Mail::fake();

        Tenant::query()->update([
            'name' => 'test_tenant',
        ]);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-make',
            'taxonomy_id' => 123,
            'term' => 'foo',
            'provider' => 'bar',
        ]);

        TaxonomyMap::query()->create([
            'taxonomy_type' => 'master-model',
            'taxonomy_id' => 456,
            'term' => 'baz',
            'provider' => 'buzz',
        ]);

        Vehicle::factory()->create([
            'make_id' => 123,
            'model_id' => 456,
            'registration_number' => 'ABC123',
        ]);

        Vehicle::factory()->create([
            'make_id' => 789,
            'model_id' => 555,
        ]);

        VehicleOffer::factory()->create([
            'make_id' => 123,
            'model_id' => 456,
            'name' => 'offer_foo',
        ]);

        NewCar::factory()->create([
            'make_id' => 123,
            'model_id' => 456,
            'name' => 'new_car_bar',
        ]);

        $orphaned_maps = TaxonomyMap::all();

        (new RemoveOrphanedTaxonomiesFromModelsJob($orphaned_maps))->handle();

        Mail::assertSent(OrphanedTaxonomyRemovedMail::class, function (OrphanedTaxonomyRemovedMail $mail) {
            return $mail->assertHasSubject('Orphaned taxonomies removed report')
                && $mail->assertSeeInOrderInHtml([
                    'make taxonomy maps were removed',
                    'foo',
                    'model taxonomy maps were removed',
                    'baz',
                    'affects:',
                    'test_tenant',
                    'vehicles',
                    'ABC123',
                    'vehicle_offers',
                    'offer_foo',
                    'new_cars',
                    'new_car_bar',
                ]);
        });
    }
}
