<?php

namespace Tests\Feature;

use App\Models\Label;
use App\Models\Macro;
use App\Models\MacroAssignment;
use App\Models\MacroCollection;
use App\Models\Service;
use App\Models\ServiceAgendaItem;
use App\Models\Slide;
use App\Models\Song;
use App\Models\User;
use App\Services\PlaylistExportService;
use App\Services\ProBundleExportService;
use App\Services\ProExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use ProPresenter\Parser\ProBundleReader;
use ProPresenter\Parser\ProPlaylistReader;
use Tests\Support\InspectsSlideFillMedia;
use Tests\TestCase;

final class ProFileExportTest extends TestCase
{
    use InspectsSlideFillMedia;
    use RefreshDatabase;

    private function createSongWithContent(): Song
    {
        $song = Song::create([
            'title' => 'Export Test Song',
            'ccli_id' => '54321',
            'author' => 'Test Author',
            'copyright_text' => 'Test Publisher',
            'copyright_year' => 2024,
            'publisher' => 'Test Publisher',
        ]);

        $verse = Label::firstOrCreate(
            ['name' => 'Verse 1 - Export Test Song'],
            ['color' => '#2196F3'],
        );
        $verseSection = $song->sections()->create(['label_id' => $verse->id, 'order' => 0]);
        $verseSection->slides()->create(['order' => 0, 'text_content' => 'First line of verse']);
        $verseSection->slides()->create(['order' => 1, 'text_content' => 'Second line of verse']);

        $chorus = Label::firstOrCreate(
            ['name' => 'Chorus - Export Test Song'],
            ['color' => '#F44336'],
        );
        $chorusSection = $song->sections()->create(['label_id' => $chorus->id, 'order' => 1]);
        $chorusSection->slides()->create(['order' => 0, 'text_content' => 'Chorus text']);

        $arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
        $arrangement->arrangementSections()->create(['song_section_id' => $verseSection->id, 'order' => 0]);
        $arrangement->arrangementSections()->create(['song_section_id' => $chorusSection->id, 'order' => 1]);
        $arrangement->arrangementSections()->create(['song_section_id' => $verseSection->id, 'order' => 2]);

        return $song;
    }

    public function test_download_pro_gibt_datei_zurueck(): void
    {
        $user = User::factory()->create();
        $song = $this->createSongWithContent();

        $response = $this->actingAs($user)->get("/api/songs/{$song->id}/download-pro");

        $response->assertOk();
        $response->assertHeader('content-disposition');
        $this->assertStringContains('Export Test Song.pro', $response->headers->get('content-disposition'));
    }

    public function test_download_pro_song_ohne_gruppen_gibt_422(): void
    {
        $user = User::factory()->create();
        $song = Song::factory()->create();

        $response = $this->actingAs($user)->get("/api/songs/{$song->id}/download-pro");

        $response->assertStatus(422);
    }

    public function test_download_pro_erfordert_authentifizierung(): void
    {
        $song = Song::factory()->create();

        $response = $this->getJson("/api/songs/{$song->id}/download-pro");

        $response->assertUnauthorized();
    }

    public function test_download_pro_roundtrip_import_export(): void
    {
        $user = User::factory()->create();

        $sourcePath = base_path('tests/fixtures/propresenter/Test.pro');
        $file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true);

        $importResponse = $this->actingAs($user)->postJson(route('api.songs.import-pro'), ['file' => $file]);
        $importResponse->assertOk();

        $songId = $importResponse->json('songs.0.id');
        $song = Song::find($songId);

        $this->assertNotNull($song);

        $labelCount = $song->arrangements()->withCount('arrangementLabels')->get()->sum('arrangement_labels_count');
        $this->assertGreaterThan(0, $labelCount);

        $exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
        $exportResponse->assertOk();
    }

    public function test_download_pro_roundtrip_preserves_content(): void
    {
        $user = User::factory()->create();

        $sourcePath = base_path('tests/fixtures/propresenter/Test.pro');
        $file = new \Illuminate\Http\UploadedFile($sourcePath, 'Test.pro', 'application/octet-stream', null, true);

        $importResponse = $this->actingAs($user)->postJson(route('api.songs.import-pro'), ['file' => $file]);
        $importResponse->assertOk();

        $songId = $importResponse->json('songs.0.id');
        $originalSong = Song::with(['arrangements.arrangementSections.section.slides', 'arrangements.arrangementSections.section.label'])->find($songId);
        $this->assertNotNull($originalSong);

        $defaultArr = $originalSong->arrangements->firstWhere('is_default', true) ?? $originalSong->arrangements->first();
        $this->assertNotNull($defaultArr);

        $originalArrangementSections = $defaultArr->arrangementSections->sortBy('order')->values();
        $originalArrangements = $originalSong->arrangements;

        $exportResponse = $this->actingAs($user)->get("/api/songs/{$songId}/download-pro");
        $exportResponse->assertOk();

        $tempPath = sys_get_temp_dir().'/roundtrip-test-'.uniqid().'.pro';
        /** @var \Symfony\Component\HttpFoundation\BinaryFileResponse $baseResponse */
        $baseResponse = $exportResponse->baseResponse;
        copy($baseResponse->getFile()->getPathname(), $tempPath);

        $reImported = \ProPresenter\Parser\ProFileReader::read($tempPath);
        @unlink($tempPath);

        $this->assertSame($originalSong->title, $reImported->getName());

        $reImportedGroups = $reImported->getGroups();

        $uniqueOriginalSections = $originalArrangementSections
            ->map(fn ($arrangementSection) => $arrangementSection->section)
            ->filter()
            ->unique('id')
            ->values();

        $this->assertCount($uniqueOriginalSections->count(), $reImportedGroups, 'Group count mismatch');

        foreach ($uniqueOriginalSections as $index => $originalSection) {
            $reImportedGroup = $reImportedGroups[$index];
            $this->assertSame($originalSection->label->name, $reImportedGroup->getName(), "Group name mismatch at index {$index}");

            $originalSlides = $originalSection->slides->sortBy('order')->values();
            $reImportedSlides = $reImported->getSlidesForGroup($reImportedGroup);
            $this->assertCount($originalSlides->count(), $reImportedSlides, "Slide count mismatch for group '{$originalSection->label->name}'");

            foreach ($originalSlides as $slideIndex => $originalSlide) {
                $reImportedSlide = $reImportedSlides[$slideIndex];

                $this->assertSame(
                    $originalSlide->text_content,
                    $reImportedSlide->getPlainText(),
                    "Slide text mismatch for group '{$originalSection->label->name}' slide {$slideIndex}"
                );

                if ($originalSlide->text_content_translated) {
                    $this->assertTrue($reImportedSlide->hasTranslation(), "Expected translation for group '{$originalSection->label->name}' slide {$slideIndex}");
                    $this->assertSame(
                        $originalSlide->text_content_translated,
                        $reImportedSlide->getTranslation()?->getPlainText(),
                        "Translation text mismatch for group '{$originalSection->label->name}' slide {$slideIndex}"
                    );
                }
            }
        }

        $reImportedArrangements = $reImported->getArrangements();
        $this->assertCount($originalArrangements->count(), $reImportedArrangements, 'Arrangement count mismatch');

        foreach ($originalArrangements as $originalArrangement) {
            $reImportedArrangement = $reImported->getArrangementByName($originalArrangement->name);
            $this->assertNotNull($reImportedArrangement, "Arrangement '{$originalArrangement->name}' not found in re-import");

            $originalGroupNames = $originalArrangement->arrangementSections
                ->sortBy('order')
                ->map(fn ($arrangementSection) => $arrangementSection->section?->label?->name)
                ->filter()
                ->values()
                ->toArray();

            $reImportedGroupNames = array_map(
                fn ($group) => $group->getName(),
                $reImported->getGroupsForArrangement($reImportedArrangement)
            );

            $this->assertSame(
                $originalGroupNames,
                $reImportedGroupNames,
                "Arrangement '{$originalArrangement->name}' group order mismatch"
            );
        }

        if ($originalSong->ccli_id) {
            $this->assertSame((int) $originalSong->ccli_id, $reImported->getCcliSongNumber());
        }
        if ($originalSong->author) {
            $this->assertSame($originalSong->author, $reImported->getCcliAuthor());
        }
    }

    public function test_export_ohne_service_context_enthaelt_keine_macros(): void
    {
        $song = $this->createSongWithContent();
        $macro = $this->createMacroForExport('Service Macro');
        MacroAssignment::create([
            'part_type' => 'song',
            'macro_id' => $macro->id,
            'position' => 'all_slides',
            'order' => 0,
        ]);

        $parserSong = app(ProExportService::class)->generateParserSong($song);

        foreach ($this->allParserSlides($parserSong) as $slide) {
            $this->assertFalse($slide->hasMacro());
        }
    }

    public function test_export_mit_globaler_song_zuweisung_enthaelt_macro_auf_allen_slides(): void
    {
        $service = Service::factory()->create();
        $song = $this->createSongWithContent();
        $macro = $this->createMacroForExport('Alle Folien Macro');
        MacroAssignment::create([
            'part_type' => 'song',
            'macro_id' => $macro->id,
            'position' => 'all_slides',
            'order' => 0,
        ]);

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);
        $slides = $this->allParserSlides($parserSong);

        $this->assertNotEmpty($slides);
        foreach ($slides as $slide) {
            $this->assertTrue($slide->hasMacro());
            $this->assertSame('Alle Folien Macro', $slide->getMacroName());
            $this->assertSame($macro->uuid, $slide->getMacroUuid());
            $this->assertSame('Export Collection', $slide->getMacroCollectionName());
        }
    }

    public function test_export_mit_ausgeblendeter_macro_enthaelt_keine_macro(): void
    {
        $service = Service::factory()->create();
        $song = $this->createSongWithContent();
        $macro = $this->createMacroForExport('Ausgeblendete Macro', ['hidden_at' => now()]);
        MacroAssignment::create([
            'part_type' => 'song',
            'macro_id' => $macro->id,
            'position' => 'all_slides',
            'order' => 0,
        ]);

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);

        foreach ($this->allParserSlides($parserSong) as $slide) {
            $this->assertFalse($slide->hasMacro());
        }
    }

    public function test_export_mit_service_background_enthaelt_background_auf_allen_song_folien(): void
    {
        Storage::fake('public');
        Storage::disk('public')->put('slides/background.jpg', 'background-image');

        $service = Service::factory()->create([
            'background_filename' => 'slides/background.jpg',
        ]);
        $song = $this->createSongWithContent();

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);
        $slides = $this->allParserSlides($parserSong);

        $this->assertNotEmpty($slides);
        foreach ($slides as $slide) {
            // Service background stays a BACKGROUND media action behind the song text.
            $this->assertTrue($slide->hasBackgroundMedia());
            $this->assertSame('BACKGROUND.jpg', $slide->getBackgroundMediaUrl());
            // Text is a content element; no image content element, no foreground action.
            $this->assertTrue($this->elementHasTextAt($slide, 0));
            $this->assertSame([], $this->fillMediaUrls($slide));
            $this->assertFalse($this->hasForegroundImageMediaAction($slide));
        }
    }

    public function test_export_first_slide_macro_nur_auf_erster_folie_des_ganzen_songs(): void
    {
        $service = Service::factory()->create();
        // Song: Verse(2 slides) + Chorus(1 slide) → 3 slides total
        $song = $this->createSongWithContent();
        $macro = $this->createMacroForExport('Erste Folie Macro');
        MacroAssignment::create([
            'part_type' => 'song',
            'macro_id' => $macro->id,
            'position' => 'first_slide',
            'order' => 0,
        ]);

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);
        $slides = $this->allParserSlides($parserSong);

        // Exactly 3 slides: Verse-0, Verse-1, Chorus-0
        $this->assertCount(3, $slides);

        // Only the very first slide of the whole song gets the macro
        $this->assertTrue($slides[0]->hasMacro(), 'Erste Folie (global index 0) muss Macro haben');
        $this->assertSame('Erste Folie Macro', $slides[0]->getMacroName());

        // Chorus slide 0 is the first slide of its section but NOT the first of the song → no macro
        $this->assertFalse($slides[1]->hasMacro(), 'Verse-Folie 1 darf kein first_slide-Macro haben');
        $this->assertFalse($slides[2]->hasMacro(), 'Chorus-Folie 0 darf kein first_slide-Macro haben (nicht erste Folie des Songs)');
    }

    public function test_export_last_slide_macro_nur_auf_letzter_folie_des_ganzen_songs(): void
    {
        $service = Service::factory()->create();
        // Song: Verse(2 slides) + Chorus(1 slide) → 3 slides total; last = Chorus-0
        $song = $this->createSongWithContent();
        $macro = $this->createMacroForExport('Letzte Folie Macro');
        MacroAssignment::create([
            'part_type' => 'song',
            'macro_id' => $macro->id,
            'position' => 'last_slide',
            'order' => 0,
        ]);

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);
        $slides = $this->allParserSlides($parserSong);

        $this->assertCount(3, $slides);

        // Only the very last slide of the whole song gets the macro
        $this->assertFalse($slides[0]->hasMacro(), 'Verse-Folie 0 darf kein last_slide-Macro haben');
        $this->assertFalse($slides[1]->hasMacro(), 'Verse-Folie 1 darf kein last_slide-Macro haben (letzte Folie der Sektion, aber nicht des Songs)');
        $this->assertTrue($slides[2]->hasMacro(), 'Letzte Folie des Songs (global index 2) muss Macro haben');
        $this->assertSame('Letzte Folie Macro', $slides[2]->getMacroName());
    }

    public function test_export_kombiniert_all_first_last_platziert_spezifischste_macro(): void
    {
        $service = Service::factory()->create();
        // Song: Verse(2 slides) + Chorus(1 slide) → 3 slides total.
        $song = $this->createSongWithContent();

        // all_slides has the LOWEST order, yet first/last must still win on the
        // first/last slide because position specificity beats `order`.
        $this->makeAssignedMacro('song', 'all_slides', 'ALLE', 0);
        $this->makeAssignedMacro('song', 'first_slide', 'ERSTE', 1);
        $this->makeAssignedMacro('song', 'last_slide', 'LETZTE', 2);

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);
        $slides = $this->allParserSlides($parserSong);

        $this->assertCount(3, $slides);
        $this->assertSame('ERSTE', $slides[0]->getMacroName(), 'Erste Folie: first_slide schlägt all_slides');
        $this->assertSame('ALLE', $slides[1]->getMacroName(), 'Mittlere Folie: nur all_slides trifft');
        $this->assertSame('LETZTE', $slides[2]->getMacroName(), 'Letzte Folie: last_slide schlägt all_slides');
    }

    public function test_bundle_sermon_first_last_macro_ueberlebt_uebersprungene_folie(): void
    {
        Storage::fake('public');
        Storage::disk('public')->put('slides/s1.jpg', 'a');
        Storage::disk('public')->put('slides/s3.jpg', 'c');
        // s2.jpg is intentionally NOT written → skipped during export.

        $service = Service::factory()->create();
        $this->makeAssignedMacro('sermon', 'all_slides', 'ALLE', 0);
        $this->makeAssignedMacro('sermon', 'first_slide', 'ERSTE', 1);
        $this->makeAssignedMacro('sermon', 'last_slide', 'LETZTE', 2);

        foreach ([['s1.jpg', 0], ['s2.jpg', 1], ['s3.jpg', 2]] as [$file, $sort]) {
            Slide::factory()->create([
                'service_id' => $service->id,
                'type' => 'sermon',
                'original_filename' => $file,
                'stored_filename' => 'slides/'.$file,
                'cover_mode' => false,
                'sort_order' => $sort,
            ]);
        }

        $bundlePath = app(ProBundleExportService::class)->generateBundle($service, 'sermon');
        $slides = $this->allParserSlides(ProBundleReader::read($bundlePath)->getSong());

        // s2 skipped (missing file). first_slide lands on the first SURVIVING slide
        // and last_slide on the last SURVIVING slide: the total is the emitted count,
        // not the raw slide count (which would make last_slide never match).
        $this->assertCount(2, $slides);
        $this->assertSame('ERSTE', $slides[0]->getMacroName());
        $this->assertSame('LETZTE', $slides[1]->getMacroName());

        @unlink($bundlePath);
    }

    public function test_export_ohne_background_enthaelt_keine_background_actions(): void
    {
        Storage::fake('public');

        $service = Service::factory()->create();
        $song = $this->createSongWithContent();

        $parserSong = app(ProExportService::class)->generateParserSong($song, $service);

        foreach ($this->allParserSlides($parserSong) as $slide) {
            // No background configured → no background fill.media element and no media ACTION.
            $this->assertFalse($slide->hasBackgroundMedia());
            $this->assertNotContains('BACKGROUND.jpg', $this->fillMediaUrls($slide));
            $this->assertFalse($this->hasImageMediaAction($slide));
        }
    }

    public function test_sermon_export_ueberspringt_background_bei_full_cover_folien(): void
    {
        Storage::fake('public');
        Storage::disk('public')->put('slides/background.jpg', 'background-image');
        Storage::disk('public')->put('slides/contain.jpg', 'contain-image');
        Storage::disk('public')->put('slides/cover.jpg', 'cover-image');

        $service = Service::factory()->create([
            'background_filename' => 'slides/background.jpg',
        ]);

        Slide::factory()->create([
            'service_id' => $service->id,
            'type' => 'sermon',
            'original_filename' => 'contain.jpg',
            'stored_filename' => 'slides/contain.jpg',
            'cover_mode' => false,
            'sort_order' => 0,
        ]);
        Slide::factory()->create([
            'service_id' => $service->id,
            'type' => 'sermon',
            'original_filename' => 'cover.jpg',
            'stored_filename' => 'slides/cover.jpg',
            'cover_mode' => true,
            'sort_order' => 1,
        ]);

        $bundlePath = app(ProBundleExportService::class)->generateBundle($service, 'sermon');
        $bundle = ProBundleReader::read($bundlePath);
        $slides = $this->allParserSlides($bundle->getSong());

        $this->assertCount(2, $slides);
        // contain slide: the uploaded image is a content element with the service
        // background behind it as a BACKGROUND media action.
        $this->assertTrue($slides[0]->hasBackgroundMedia());
        $this->assertSame('BACKGROUND.jpg', $slides[0]->getBackgroundMediaUrl());
        $this->assertSame('contain.jpg', $this->fillMediaUrlAt($slides[0], 0));
        $this->assertTrue($this->isFullFrameAt($slides[0], 0));
        $this->assertFalse($this->hasForegroundImageMediaAction($slides[0]));
        // cover slide: uploaded content element only, no background layer.
        $this->assertFalse($slides[1]->hasBackgroundMedia());
        $this->assertNotContains('BACKGROUND.jpg', $this->fillMediaUrls($slides[1]));
        $this->assertSame('cover.jpg', $this->fillMediaUrlAt($slides[1], 0));
        $this->assertFalse($this->hasForegroundImageMediaAction($slides[1]));
        $this->assertTrue($bundle->hasMediaFile('BACKGROUND.jpg'), 'Background image must be embedded under fixed name');
        $this->assertSame('background-image', $bundle->getMediaFile('BACKGROUND.jpg'));

        @unlink($bundlePath);
    }

    public function test_information_und_moderation_exports_erhalten_keinen_background(): void
    {
        Storage::fake('public');
        Storage::disk('public')->put('slides/background.jpg', 'background-image');
        Storage::disk('public')->put('slides/information.jpg', 'information-image');
        Storage::disk('public')->put('slides/moderation.jpg', 'moderation-image');

        $service = Service::factory()->create([
            'background_filename' => 'slides/background.jpg',
        ]);

        Slide::factory()->create([
            'service_id' => $service->id,
            'type' => 'information',
            'original_filename' => 'information.jpg',
            'stored_filename' => 'slides/information.jpg',
            'sort_order' => 0,
        ]);
        Slide::factory()->create([
            'service_id' => $service->id,
            'type' => 'moderation',
            'original_filename' => 'moderation.jpg',
            'stored_filename' => 'slides/moderation.jpg',
            'sort_order' => 0,
        ]);

        foreach (['information', 'moderation'] as $blockType) {
            $bundlePath = app(ProBundleExportService::class)->generateBundle($service, $blockType);

            foreach ($this->allParserSlides(ProBundleReader::read($bundlePath)->getSong()) as $slide) {
                // Info/moderation slides carry only the foreground image element, never a background layer.
                $this->assertFalse($slide->hasBackgroundMedia());
                $this->assertNotContains('BACKGROUND.jpg', $this->fillMediaUrls($slide));
            }

            @unlink($bundlePath);
        }
    }

    public function test_playlist_export_setzt_background_auf_sermon_folien_und_nicht_auf_informationen(): void
    {
        Storage::fake('public');
        Storage::disk('public')->put('slides/background.jpg', 'background-image');
        Storage::disk('public')->put('slides/information.jpg', 'information-image');
        Storage::disk('public')->put('slides/sermon-contain.jpg', 'sermon-contain-image');
        Storage::disk('public')->put('slides/sermon-cover.jpg', 'sermon-cover-image');

        $service = Service::factory()->create([
            'title' => 'Playlist Background',
            'date' => now(),
            'background_filename' => 'slides/background.jpg',
        ]);

        Slide::factory()->create([
            'service_id' => null,
            'type' => 'information',
            'original_filename' => 'information.jpg',
            'stored_filename' => 'slides/information.jpg',
            'uploaded_at' => now()->subDay(),
            'sort_order' => 0,
        ]);

        $sermonItem = ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Predigt',
            'service_song_id' => null,
            'sort_order' => 1,
            'is_before_event' => false,
        ]);

        Slide::factory()->create([
            'service_id' => $service->id,
            'service_agenda_item_id' => $sermonItem->id,
            'type' => 'sermon',
            'original_filename' => 'sermon-contain.jpg',
            'stored_filename' => 'slides/sermon-contain.jpg',
            'cover_mode' => false,
            'sort_order' => 0,
        ]);
        Slide::factory()->create([
            'service_id' => $service->id,
            'service_agenda_item_id' => $sermonItem->id,
            'type' => 'sermon',
            'original_filename' => 'sermon-cover.jpg',
            'stored_filename' => 'slides/sermon-cover.jpg',
            'cover_mode' => true,
            'sort_order' => 1,
        ]);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $informationSong = $playlist->getEmbeddedSong('Informationen.pro');
        $sermonSong = $playlist->getEmbeddedSong('Predigt.pro');

        $this->assertNotNull($informationSong);
        $this->assertNotNull($sermonSong);

        foreach ($this->allParserSlides($informationSong) as $slide) {
            $this->assertFalse($slide->hasBackgroundMedia());
            $this->assertNotContains('BACKGROUND.jpg', $this->fillMediaUrls($slide));
        }

        $sermonSlides = $this->allParserSlides($sermonSong);
        $this->assertCount(2, $sermonSlides);
        // contain slide: uploaded content element with the service background behind it
        // as a BACKGROUND media action (background is NOT a content element).
        $this->assertTrue($sermonSlides[0]->hasBackgroundMedia());
        $this->assertSame('BACKGROUND.jpg', $sermonSlides[0]->getBackgroundMediaUrl());
        $this->assertNotEmpty($this->fillMediaUrls($sermonSlides[0]));
        $this->assertNotContains('BACKGROUND.jpg', $this->fillMediaUrls($sermonSlides[0]));
        $this->assertFalse($this->hasForegroundImageMediaAction($sermonSlides[0]));
        // cover slide: uploaded content element only, no background layer.
        $this->assertFalse($sermonSlides[1]->hasBackgroundMedia());
        $this->assertNotContains('BACKGROUND.jpg', $this->fillMediaUrls($sermonSlides[1]));

        $this->cleanupTempDir($result['temp_dir']);
    }

    private function makeAssignedMacro(string $partType, string $position, string $name, int $order): Macro
    {
        $macro = Macro::factory()->create(['name' => $name]);
        $collection = MacroCollection::create([
            'uuid' => strtoupper(\Illuminate\Support\Str::uuid()->toString()),
            'name' => 'Coll '.$name,
        ]);
        $collection->macros()->attach($macro->id, ['order' => 0]);
        MacroAssignment::create([
            'part_type' => $partType,
            'macro_id' => $macro->id,
            'position' => $position,
            'order' => $order,
        ]);

        return $macro;
    }

    private function createMacroForExport(string $name, array $attributes = []): Macro
    {
        $macro = Macro::factory()->create(array_merge([
            'uuid' => '11111111-2222-4333-8444-555555555555',
            'name' => $name,
        ], $attributes));

        $collection = MacroCollection::create([
            'uuid' => '99999999-8888-4777-8666-555555555555',
            'name' => 'Export Collection',
        ]);
        $collection->macros()->attach($macro->id, ['order' => 0]);

        return $macro;
    }

    private function allParserSlides(\ProPresenter\Parser\Song $parserSong): array
    {
        $slides = [];

        foreach ($parserSong->getGroups() as $group) {
            foreach ($parserSong->getSlidesForGroup($group) as $slide) {
                $slides[] = $slide;
            }
        }

        return $slides;
    }

    private function assertStringContains(string $needle, ?string $haystack): void
    {
        $this->assertNotNull($haystack);
        $this->assertTrue(
            str_contains($haystack, $needle),
            "Failed asserting that '{$haystack}' contains '{$needle}'"
        );
    }

    private function cleanupTempDir(string $dir): void
    {
        if (! is_dir($dir)) {
            return;
        }

        $items = scandir($dir);
        if ($items === false) {
            return;
        }

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $path = $dir.'/'.$item;
            is_dir($path) ? $this->cleanupTempDir($path) : unlink($path);
        }

        rmdir($dir);
    }
}
