<?php

namespace Tests\Tenant;

use App\Services\Configurator\ConfiguratorCopyService;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Mtc\MercuryDataModels\Tenant;
use Stancl\Tenancy\Events\TenancyInitialized;
use Tests\TenantTestCase;

/**
 * Exercises the cross-tenant configurator copy end to end.
 *
 * The standard test harness routes every tenant to a single shared SQLite file,
 * which cannot model a source -> destination copy. This test provisions a
 * dedicated SQLite database per tenant and routes each tenant to its own file so
 * the two tenants are genuinely isolated, mirroring production where every
 * tenant has its own database.
 */
class CopyConfiguratorModelCommandTest extends TenantTestCase
{
    protected bool $needsMasterDatabase = true;

    private const MEDIA_DISK = 'media-test';

    private const MODEL_ID = 100;
    private const COLOUR_ID = 200;
    private const ENGINE_ID = 300;
    private const PACKAGE_ID = 400;
    private const LOCAL_MEDIA_ID = 500;
    private const EXTERNAL_MEDIA_ID = 501;
    private const SECTION_ID = 600;
    private const SECTION_DELETED_ID = 601;
    private const CUSTOM_ID = 700;
    private const CUSTOM_ORPHAN_PARENT_ID = 701;
    private const CUSTOM_MISSING_PARENT_ID = 702;
    private const CUSTOM_SECTION_FQCN = \Mtc\MercuryDataModels\CarConfiguratorCustomSection::class;

    // A media path prefix that is deliberately NOT the source tenant id, matching
    // imported/cloned tenants whose media keeps its origin prefix.
    private const LEGACY_PREFIX = 'origin_site_ZZ99';
    private const MEDIA_SUBPATH = 'cars/orig/ca/car5';

    private Tenant $source;
    private Tenant $destination;

    /** @var array<string, string> tenant id => sqlite file path */
    private array $tenantFiles = [];

    protected function setUp(): void
    {
        parent::setUp();

        tenancy()->end();

        // Route each of our tenants to its own SQLite database so a copy really
        // moves rows between two databases. Registered after the harness's own
        // listener, so this mapping wins.
        Event::listen(TenancyInitialized::class, function (TenancyInitialized $event) {
            $file = $this->tenantFiles[$event->tenancy->tenant->id] ?? null;
            if ($file) {
                Config::set('database.connections.tenant-test.database', $file);
                DB::purge('tenant-test');
            }
        });

        Config::set('filesystems.default_media', self::MEDIA_DISK);
        Storage::fake(self::MEDIA_DISK);

        $this->source = $this->provisionTenant();
        $this->destination = $this->provisionTenant();
    }

    protected function tearDown(): void
    {
        foreach ($this->tenantFiles as $file) {
            @unlink($file);
        }
        $this->tenantFiles = [];

        parent::tearDown();
    }

    public function testCopiesModelWithChildrenRestrictionsAndImagery(): void
    {
        $this->seedTaxonomy($this->source);
        $this->seedTaxonomy($this->destination);
        $this->buildSourceConfigurator();

        $report = $this->service()->copy($this->source, $this->destination, [self::MODEL_ID]);

        $this->assertFalse($report->hasFailures(), implode(' | ', $report->warnings()));
        $this->assertEquals(1, $report->countByStatus('copied'));

        $this->usingTenant($this->destination, function () {
            $model = DB::table('car_configurator_models')->where('slug', 'i4')->first();
            $this->assertNotNull($model);
            $this->assertNotEquals(self::MODEL_ID, $model->id, 'destination model should get a fresh id');
            $this->assertEquals(1, $model->make_id, 'taxonomy ids are copied verbatim');
            $this->assertEquals(1, $model->model_id);

            $colour = DB::table('car_configurator_colours')->where('model_id', $model->id)->first();
            $engine = DB::table('car_configurator_engines')->where('model_id', $model->id)->first();
            $this->assertEquals('Blue', $colour->name);
            $this->assertEquals(1, $engine->fuel_type_id, 'engine taxonomy fk preserved');

            // Shared catalogue resolved-or-created + pivot rebuilt with new ids.
            $package = DB::table('car_configurator_packages')->where('name', 'Winter Pack')->first();
            $this->assertNotNull($package);
            $this->assertTrue(
                DB::table('car_configurator_model_packages')
                    ->where('model_id', $model->id)
                    ->where('package_id', $package->id)
                    ->exists(),
            );

            // Restriction: both polymorphic sides remapped, type strings kept.
            $restriction = DB::table('car_configurator_restrictions')
                ->where('model_id', $model->id)
                ->where('restriction_type', 'engines')
                ->first();
            $this->assertNotNull($restriction);
            $this->assertEquals('car-config-colour', $restriction->functionality_type);
            $this->assertEquals($colour->id, $restriction->functionality_id);
            $this->assertEquals($engine->id, $restriction->restriction_id);

            // Media rows: local rehomed to the DESTINATION prefix (not the legacy
            // origin prefix, and not the source tenant id), external row-only.
            $local = DB::table('media')->where('src', 'car500.webp')->first();
            $external = DB::table('media')->where('image_provider', 'Configurator')->first();
            $this->assertEquals("{$this->destination->id}/" . self::MEDIA_SUBPATH, $local->path);
            $this->assertEquals(1, (int) $external->external);

            // media_uses remapped to the new media + owner ids, owner_type intact.
            $this->assertTrue(
                DB::table('media_uses')
                    ->where('owner_type', 'car-config-model')
                    ->where('owner_id', $model->id)
                    ->where('media_id', $local->id)
                    ->exists(),
            );
            $this->assertTrue(
                DB::table('media_uses')
                    ->where('owner_type', 'car-config-colour')
                    ->where('owner_id', $colour->id)
                    ->where('media_id', $external->id)
                    ->exists(),
            );
        });

        // The local original (and its jpg sibling) landed under the destination prefix.
        $destDir = "{$this->destination->id}/" . self::MEDIA_SUBPATH;
        Storage::disk(self::MEDIA_DISK)->assertExists("{$destDir}/car500.webp");
        Storage::disk(self::MEDIA_DISK)->assertExists("{$destDir}/car500.jpg");
    }

    public function testHandlesFqcnTypeStringsAndSoftDeletedSectionParents(): void
    {
        $this->seedTaxonomy($this->source);
        $this->seedTaxonomy($this->destination);
        $this->buildSourceConfigurator();

        $report = $this->service()->copy($this->source, $this->destination, [self::MODEL_ID]);
        $this->assertFalse($report->hasFailures(), implode(' | ', $report->warnings()));

        $this->usingTenant($this->destination, function () {
            $model = DB::table('car_configurator_models')->where('slug', 'i4')->first();

            // Custom section with a live parent keeps a valid, remapped FK.
            $sixSeats = DB::table('car_configurator_custom_sections')->where('name', '6 Seats')->first();
            $options = DB::table('car_configurator_sections')->where('name', 'Options')->first();
            $this->assertEquals($options->id, $sixSeats->section_id);

            // Soft-deleted parent gets backfilled so its custom section survives.
            $rescued = DB::table('car_configurator_custom_sections')->where('name', 'Rescued')->first();
            $this->assertNotNull($rescued);
            $this->assertTrue(
                DB::table('car_configurator_sections')->where('id', $rescued->section_id)->exists(),
            );

            // Custom section with a missing parent is skipped, not copied.
            $this->assertFalse(
                DB::table('car_configurator_custom_sections')->where('name', 'Orphan')->exists(),
            );

            // media_use stored under the FQCN owner_type is copied and remapped.
            $this->assertTrue(
                DB::table('media_uses')
                    ->where('owner_type', self::CUSTOM_SECTION_FQCN)
                    ->where('owner_id', $sixSeats->id)
                    ->exists(),
            );

            // Restriction whose functionality side is a FQCN is remapped correctly.
            $this->assertTrue(
                DB::table('car_configurator_restrictions')
                    ->where('model_id', $model->id)
                    ->where('functionality_type', self::CUSTOM_SECTION_FQCN)
                    ->where('functionality_id', $sixSeats->id)
                    ->exists(),
            );
        });

        $this->assertNotEmpty($report->warnings(), 'expected a warning for the skipped custom section');
    }

    public function testSkipsExistingByDefaultAndReplacesWithOverwrite(): void
    {
        $this->seedTaxonomy($this->source);
        $this->seedTaxonomy($this->destination);
        $this->buildSourceConfigurator();

        $this->service()->copy($this->source, $this->destination, [self::MODEL_ID]);

        // Second run without --overwrite: skipped, still one model.
        $skip = $this->service()->copy($this->source, $this->destination, [self::MODEL_ID]);
        $this->assertEquals(1, $skip->countByStatus('skipped'));
        $this->usingTenant(
            $this->destination,
            fn () => $this->assertEquals(1, DB::table('car_configurator_models')->count()),
        );

        // With --overwrite: replaced, still one model.
        $overwrite = $this->service()->copy($this->source, $this->destination, [self::MODEL_ID], overwrite: true);
        $this->assertEquals(1, $overwrite->countByStatus('replaced'));
        $this->usingTenant($this->destination, function () {
            $this->assertEquals(1, DB::table('car_configurator_models')->count());
            $this->assertEquals(1, DB::table('car_configurator_colours')->count());
        });
    }

    public function testDryRunWritesNothing(): void
    {
        $this->seedTaxonomy($this->source);
        $this->seedTaxonomy($this->destination);
        $this->buildSourceConfigurator();

        $report = $this->service()->copy($this->source, $this->destination, [self::MODEL_ID], dryRun: true);

        $this->assertEquals(1, $report->countByStatus('copied'));
        $this->usingTenant(
            $this->destination,
            fn () => $this->assertEquals(0, DB::table('car_configurator_models')->count()),
        );
    }

    public function testFailsPreflightWhenDestinationTaxonomyMissing(): void
    {
        $this->seedTaxonomy($this->source);
        // Destination gets make + model but no fuel type, which the engine/model reference.
        $this->usingTenant($this->destination, function () {
            DB::table('vehicle_makes')->insert(['id' => 1, 'name' => 'BMW']);
            DB::table('vehicle_models')->insert(['id' => 1, 'make_id' => 1, 'name' => 'i4']);
        });
        $this->buildSourceConfigurator();

        $report = $this->service()->copy($this->source, $this->destination, [self::MODEL_ID]);

        $this->assertTrue($report->hasFailures());
        $this->assertStringContainsString('fuel_types', $report->models()[0]['message']);
        $this->usingTenant(
            $this->destination,
            fn () => $this->assertEquals(0, DB::table('car_configurator_models')->count()),
        );
    }

    public function testRunsViaArtisanCommandForAllModels(): void
    {
        $this->seedTaxonomy($this->source);
        $this->seedTaxonomy($this->destination);
        $this->buildSourceConfigurator();

        $this->artisan('configurator:copy', [
            '--source' => $this->source->id,
            '--destination' => $this->destination->id,
            '--make' => 1,
            '--model' => ['all'],
            '--no-interaction' => true,
        ])->assertExitCode(0);

        $this->usingTenant(
            $this->destination,
            fn () => $this->assertTrue(DB::table('car_configurator_models')->where('slug', 'i4')->exists()),
        );
    }

    private function service(): ConfiguratorCopyService
    {
        return $this->app->make(ConfiguratorCopyService::class);
    }

    /**
     * Create a tenant backed by its own copy of the tenant SQLite template.
     */
    private function provisionTenant(): Tenant
    {
        $tenant = Tenant::create(['tier' => \App\Tier::LITE->value]);

        $file = base_path('database/testing/configcopy-' . uniqid('', true) . '.sqlite');
        copy(base_path('database/testing/tenant-template.sqlite'), $file);
        $this->tenantFiles[$tenant->id] = $file;

        return $tenant;
    }

    private function usingTenant(Tenant $tenant, callable $callback): void
    {
        tenancy()->initialize($tenant);
        try {
            $callback();
        } finally {
            tenancy()->end();
        }
    }

    /**
     * Identical taxonomy ids in both tenants (mirrors central sync).
     */
    private function seedTaxonomy(Tenant $tenant): void
    {
        $this->usingTenant($tenant, function () {
            DB::table('vehicle_makes')->insert(['id' => 1, 'name' => 'BMW']);
            DB::table('vehicle_models')->insert(['id' => 1, 'make_id' => 1, 'name' => 'i4']);
            DB::table('fuel_types')->insert(['id' => 1, 'name' => 'Electric']);
        });
    }

    /**
     * Build a small but representative configurator in the source tenant: a
     * model, a colour, an engine, a shared package (+ pivot), a restriction
     * spanning two morph conventions, plus one local and one external image.
     */
    private function buildSourceConfigurator(): void
    {
        $this->usingTenant($this->source, function () {
            $now = now();

            DB::table('car_configurator_models')->insert([
                'id' => self::MODEL_ID,
                'name' => 'i4',
                'slug' => 'i4',
                'make_id' => 1,
                'model_id' => 1,
                'fuel_type_id' => 1,
                'active' => 1,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_colours')->insert([
                'id' => self::COLOUR_ID,
                'model_id' => self::MODEL_ID,
                'name' => 'Blue',
                'price' => 0,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_engines')->insert([
                'id' => self::ENGINE_ID,
                'model_id' => self::MODEL_ID,
                'name' => 'eDrive40',
                'fuel_type_id' => 1,
                'created_at' => $now,
                'updated_at' => $now,
            ]);

            // A live parent section and a soft-deleted one, plus custom sections
            // referencing a live parent, a soft-deleted parent (backfilled), and
            // a missing parent (skipped with a warning).
            DB::table('car_configurator_sections')->insert([
                'id' => self::SECTION_ID,
                'model_id' => self::MODEL_ID,
                'name' => 'Options',
                'custom' => 1,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_sections')->insert([
                'id' => self::SECTION_DELETED_ID,
                'model_id' => self::MODEL_ID,
                'name' => 'Retired',
                'custom' => 1,
                'created_at' => $now,
                'updated_at' => $now,
                'deleted_at' => $now,
            ]);
            DB::table('car_configurator_custom_sections')->insert([
                'id' => self::CUSTOM_ID,
                'model_id' => self::MODEL_ID,
                'section_id' => self::SECTION_ID,
                'name' => '6 Seats',
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_custom_sections')->insert([
                'id' => self::CUSTOM_ORPHAN_PARENT_ID,
                'model_id' => self::MODEL_ID,
                'section_id' => self::SECTION_DELETED_ID,
                'name' => 'Rescued',
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_custom_sections')->insert([
                'id' => self::CUSTOM_MISSING_PARENT_ID,
                'model_id' => self::MODEL_ID,
                'section_id' => 999,
                'name' => 'Orphan',
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_packages')->insert([
                'id' => self::PACKAGE_ID,
                'name' => 'Winter Pack',
                'price' => 100,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('car_configurator_model_packages')->insert([
                'model_id' => self::MODEL_ID,
                'package_id' => self::PACKAGE_ID,
                'price' => 100,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            // Restriction: functionality is the colour (morph alias), restricted
            // by the engine (relationship-name string). Both ids must remap.
            DB::table('car_configurator_restrictions')->insert([
                'model_id' => self::MODEL_ID,
                'functionality_type' => 'car-config-colour',
                'functionality_id' => self::COLOUR_ID,
                'restriction_type' => 'engines',
                'restriction_id' => self::ENGINE_ID,
                'condition' => 'is',
                'created_at' => $now,
                'updated_at' => $now,
            ]);

            DB::table('media')->insert([
                'id' => self::LOCAL_MEDIA_ID,
                'type' => 'image',
                'src' => 'car500.webp',
                'path' => self::LEGACY_PREFIX . '/' . self::MEDIA_SUBPATH,
                'external' => 0,
                'upload_date' => '2026-07',
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('media')->insert([
                'id' => self::EXTERNAL_MEDIA_ID,
                'type' => 'image',
                'src' => 'https://images.example/ext.png',
                'path' => '',
                'external' => 1,
                'image_provider' => 'Configurator',
                'source_filename' => 'https://images.example/ext.png',
                'upload_date' => '2026-07',
                'created_at' => $now,
                'updated_at' => $now,
            ]);
            DB::table('media_uses')->insert([
                'media_id' => self::LOCAL_MEDIA_ID,
                'owner_type' => 'car-config-model',
                'owner_id' => self::MODEL_ID,
                'primary' => 1,
                'order' => 0,
            ]);
            DB::table('media_uses')->insert([
                'media_id' => self::EXTERNAL_MEDIA_ID,
                'owner_type' => 'car-config-colour',
                'owner_id' => self::COLOUR_ID,
                'flags' => json_encode(['exterior-blue' => true]),
                'order' => 0,
            ]);
            // owner_type stored as the FQCN (pre-morphMap data) rather than the alias.
            DB::table('media_uses')->insert([
                'media_id' => self::LOCAL_MEDIA_ID,
                'owner_type' => self::CUSTOM_SECTION_FQCN,
                'owner_id' => self::CUSTOM_ID,
                'order' => 0,
            ]);
            // Restriction whose functionality side is stored as the FQCN.
            DB::table('car_configurator_restrictions')->insert([
                'model_id' => self::MODEL_ID,
                'functionality_type' => self::CUSTOM_SECTION_FQCN,
                'functionality_id' => self::CUSTOM_ID,
                'restriction_type' => 'colours',
                'restriction_id' => self::COLOUR_ID,
                'condition' => 'is',
                'created_at' => $now,
                'updated_at' => $now,
            ]);

            $legacyDir = self::LEGACY_PREFIX . '/' . self::MEDIA_SUBPATH;
            Storage::disk(self::MEDIA_DISK)->put("{$legacyDir}/car500.webp", 'WEBP-BYTES');
            Storage::disk(self::MEDIA_DISK)->put("{$legacyDir}/car500.jpg", 'JPG-BYTES');
        });
    }
}
