<?php

namespace Tests\Feature;

use App\Models\Label;
use App\Models\Service;
use App\Models\ServiceAgendaItem;
use App\Models\ServiceSong;
use App\Models\Setting;
use App\Models\Slide;
use App\Models\Song;
use App\Services\PlaylistExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use ProPresenter\Parser\PlaylistArchive;
use ProPresenter\Parser\ProPlaylistReader;
use Rv\Data\Cue\CompletionTargetType;
use Tests\Support\InspectsSlideFillMedia;
use Tests\TestCase;

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

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

        Storage::fake('public');
    }

    public function test_sermon_sequence_is_keyvisual_preacher_nametag_then_uploaded_sermon_slides(): void
    {
        $this->configureNameTagMacro();
        Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');
        Storage::disk('public')->put('slides/sermon-1.jpg', 'sermon-one');
        Storage::disk('public')->put('slides/sermon-2.jpg', 'sermon-two');

        $service = Service::factory()->create([
            'title' => 'Predigt Sequenz',
            'date' => now(),
            'key_visual_filename' => 'slides/keyvisual.jpg',
            'preacher_name_override' => 'Erika Predigt',
        ]);
        $sermonItem = ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Predigt',
            'service_song_id' => null,
            'sort_order' => 1,
            'is_before_event' => false,
        ]);
        $this->createSermonSlide($service, $sermonItem, 'sermon-1.jpg', 0);
        $this->createSermonSlide($service, $sermonItem, 'sermon-2.jpg', 1);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $entries = $playlist->getEntries();

        $names = $this->entryNames($playlist);
        // A moderator name tag may precede the sermon intro when the (factory-random)
        // responsible person resolves to a moderator; skip it if present.
        $offset = str_starts_with((string) ($names[0] ?? ''), 'Moderator') ? 1 : 0;
        // A looping "Abspann" (key-visual) presentation is appended as the last item.
        $this->assertSame(['Prediger - Erika Predigt', 'Predigt', 'Abspann'], array_slice($names, $offset));

        // The sermon intro is ONE presentation with THREE cues: the key-visual
        // alone, the key-visual with the preacher name tag on top (auto-advancing
        // after 10s), then the key-visual alone again.
        $introSlides = $this->slidesForEntry($playlist, $entries[$offset]);
        $this->assertCount(3, $introSlides);

        // Cue 1: key-visual as a BACKGROUND media action (no content element).
        $this->assertTrue($introSlides[0]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $introSlides[0]->getBackgroundMediaUrl());
        $this->assertSame([], $this->fillMediaUrls($introSlides[0]));
        $this->assertFalse($this->hasForegroundImageMediaAction($introSlides[0]));
        $this->assertArrayHasKey('KEY_VISUAL.jpg', $playlist->getEmbeddedMediaFiles());

        // Cue 2: key-visual BACKGROUND media action with the name-tag text element
        // above it, the Namenseinblender macro, and a 10s auto-advance to the next cue.
        $this->assertTrue($introSlides[1]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $introSlides[1]->getBackgroundMediaUrl());
        $this->assertTrue($this->elementHasTextAt($introSlides[1], 0));
        $this->assertFalse($this->hasForegroundImageMediaAction($introSlides[1]));
        // Name and role are split: the name is the main (\fs84) run and the
        // role is a separate smaller, non-bold (\b0\fs50) subtitle run.
        [$name, $subtitle] = $this->nameTagNameAndSubtitle($introSlides[1]);
        $this->assertSame('Erika Predigt', $name);
        $this->assertSame('Predigt', $subtitle);
        $this->assertTrue($introSlides[1]->hasMacro());
        $this->assertSame(10.0, $introSlides[1]->getCue()->getCompletionTime());
        $this->assertSame(
            CompletionTargetType::COMPLETION_TARGET_TYPE_NEXT,
            $introSlides[1]->getCue()->getCompletionTargetType(),
        );

        // Cue 3: key-visual alone again (no content element, no name tag).
        $this->assertTrue($introSlides[2]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $introSlides[2]->getBackgroundMediaUrl());
        $this->assertSame([], $this->fillMediaUrls($introSlides[2]));
        $this->assertFalse($introSlides[2]->hasMacro());

        $sermonSlides = $this->slidesForEntry($playlist, $entries[$offset + 1]);
        $this->assertCount(2, $sermonSlides);
        $this->assertSame('sermon-1.jpg', $sermonSlides[0]->getLabel());
        $this->assertSame('sermon-2.jpg', $sermonSlides[1]->getLabel());
        $this->assertSame(2, Slide::count());

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

    public function test_moderator_nametag_is_first_presentation_for_first_visible_agenda_item(): void
    {
        $this->configureNameTagMacro();

        $service = Service::factory()->create([
            'title' => 'Moderator Sequenz',
            'moderator_name' => 'Max Moderation',
        ]);
        ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Vorprogramm',
            'service_song_id' => null,
            'sort_order' => 1,
            'is_before_event' => true,
            'responsible' => [['name' => 'Versteckte Person']],
        ]);
        $song = $this->createSongWithContent('Erstes sichtbares Lied');
        $serviceSong = ServiceSong::create([
            'service_id' => $service->id,
            'song_id' => $song->id,
            'cts_song_name' => 'Erstes sichtbares Lied',
            'order' => 1,
        ]);
        ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Erstes sichtbares Lied',
            'service_song_id' => $serviceSong->id,
            'sort_order' => 2,
            'is_before_event' => false,
        ]);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $entries = $playlist->getEntries();

        $this->assertSame(['Moderator - Max Moderation', 'Erstes sichtbares Lied'], $this->entryNames($playlist));
        $moderatorSlides = $this->slidesForEntry($playlist, $entries[0]);
        $this->assertCount(1, $moderatorSlides);
        // Name and role are split: the name is the main run, the role the subtitle run.
        [$name, $subtitle] = $this->nameTagNameAndSubtitle($moderatorSlides[0]);
        $this->assertSame('Max Moderation', $name);
        $this->assertSame('Moderation', $subtitle);
        $this->assertTrue($moderatorSlides[0]->hasMacro());

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

    public function test_without_macro_configured_no_nametags_are_added_and_sermon_sequence_keeps_keyvisual_then_slides(): void
    {
        Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');
        Storage::disk('public')->put('slides/sermon-1.jpg', 'sermon-one');
        Storage::disk('public')->put('slides/sermon-2.jpg', 'sermon-two');

        $service = Service::factory()->create([
            'title' => 'Ohne Namenseinblender',
            'date' => now(),
            'key_visual_filename' => 'slides/keyvisual.jpg',
            'moderator_name' => 'Max Moderation',
            'preacher_name_override' => 'Erika Predigt',
        ]);
        $sermonItem = ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Predigt',
            'service_song_id' => null,
            'sort_order' => 1,
            'is_before_event' => false,
        ]);
        $this->createSermonSlide($service, $sermonItem, 'sermon-1.jpg', 0);
        $this->createSermonSlide($service, $sermonItem, 'sermon-2.jpg', 1);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $entries = $playlist->getEntries();

        // A looping "Abspann" (key-visual) presentation is appended as the last item.
        $this->assertSame(['Keyvisual-Predigt', 'Predigt', 'Abspann'], $this->entryNames($playlist));
        $this->assertCount(1, $this->slidesForEntry($playlist, $entries[0]));
        $sermonSlides = $this->slidesForEntry($playlist, $entries[1]);
        $this->assertCount(2, $sermonSlides);

        foreach (array_keys($playlist->getEmbeddedProFiles()) as $filename) {
            foreach ($this->allParserSlides($playlist->getEmbeddedSong($filename)) as $slide) {
                $this->assertFalse($slide->hasMacro());
            }
        }

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

    public function test_without_moderator_name_no_moderator_nametag_is_added(): void
    {
        $this->configureNameTagMacro();

        $service = Service::factory()->create([
            'title' => 'Ohne Moderator',
            'moderator_name' => null,
        ]);
        $song = $this->createSongWithContent('Startlied');
        $serviceSong = ServiceSong::create([
            'service_id' => $service->id,
            'song_id' => $song->id,
            'cts_song_name' => 'Startlied',
            'order' => 1,
        ]);
        ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Startlied',
            'service_song_id' => $serviceSong->id,
            'sort_order' => 1,
            'is_before_event' => false,
            'responsible' => [],
        ]);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);

        $this->assertSame(['Startlied'], $this->entryNames($playlist));
        $this->assertNull($playlist->getEmbeddedSong('Moderator.pro'));

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

    public function test_nametag_presentations_are_named_by_role_and_person(): void
    {
        $this->configureNameTagMacro();
        Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');
        Storage::disk('public')->put('slides/sermon.jpg', 'sermon-image');

        $service = Service::factory()->create([
            'title' => 'Rollen Benennung',
            'date' => now(),
            'key_visual_filename' => 'slides/keyvisual.jpg',
            'moderator_name' => 'Max Moderation',
            'preacher_name_override' => 'Erika Predigt',
        ]);

        $song = $this->createSongWithContent('Eröffnungslied');
        $serviceSong = ServiceSong::create([
            'service_id' => $service->id,
            'song_id' => $song->id,
            'cts_song_name' => 'Eröffnungslied',
            'order' => 1,
        ]);
        ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Eröffnungslied',
            'service_song_id' => $serviceSong->id,
            'sort_order' => 1,
            'is_before_event' => false,
        ]);
        $sermonItem = ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Predigt',
            'service_song_id' => null,
            'sort_order' => 2,
            'is_before_event' => false,
        ]);
        $this->createSermonSlide($service, $sermonItem, 'sermon.jpg', 0);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $names = $this->entryNames($playlist);

        $this->assertContains('Moderator - Max Moderation', $names);
        $this->assertContains('Prediger - Erika Predigt', $names);

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

    public function test_sermon_item_without_uploaded_slides_still_emits_intro_and_no_images_presentation(): void
    {
        $this->configureNameTagMacro();
        Setting::set('agenda_sermon_matching', 'Predigt');
        Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');

        $service = Service::factory()->create([
            'title' => 'Predigt ohne Folien',
            'date' => now(),
            'key_visual_filename' => 'slides/keyvisual.jpg',
            'preacher_name_override' => 'Erika Predigt',
        ]);
        ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Predigt',
            'service_song_id' => null,
            'sort_order' => 1,
            'is_before_event' => false,
            'responsible' => [],
        ]);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $entries = $playlist->getEntries();

        // A sermon item with no uploaded slides emits ONLY the intro presentation,
        // plus the looping "Abspann" (key-visual) presentation appended as the last item.
        $this->assertSame(['Prediger - Erika Predigt', 'Abspann'], $this->entryNames($playlist));

        // The intro .pro carries THREE cues: key-visual alone, key-visual with the
        // name tag (10s auto-next), then the key-visual alone again.
        $introSlides = $this->slidesForEntry($playlist, $entries[0]);
        $this->assertCount(3, $introSlides);
        // Key-visual is a BACKGROUND media action on the flanking cues (no content element).
        $this->assertTrue($introSlides[0]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $introSlides[0]->getBackgroundMediaUrl());
        $this->assertSame([], $this->fillMediaUrls($introSlides[0]));
        $this->assertTrue($introSlides[1]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $introSlides[1]->getBackgroundMediaUrl());
        $this->assertTrue($this->elementHasTextAt($introSlides[1], 0));
        $this->assertFalse($this->hasForegroundImageMediaAction($introSlides[1]));
        [$name, $subtitle] = $this->nameTagNameAndSubtitle($introSlides[1]);
        $this->assertSame('Erika Predigt', $name);
        $this->assertSame('Predigt', $subtitle);
        $this->assertTrue($introSlides[1]->hasMacro());
        $this->assertSame(10.0, $introSlides[1]->getCue()->getCompletionTime());
        // Cue 3: key-visual alone again.
        $this->assertTrue($introSlides[2]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $introSlides[2]->getBackgroundMediaUrl());
        $this->assertSame([], $this->fillMediaUrls($introSlides[2]));
        $this->assertFalse($introSlides[2]->hasMacro());

        // No separate sermon-images presentation is emitted.
        $this->assertNull($playlist->getEmbeddedSong('Predigt.pro'));

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

    public function test_moderator_nametag_with_keyvisual_is_three_cue_sequence(): void
    {
        $this->configureNameTagMacro();
        Storage::disk('public')->put('slides/keyvisual.jpg', 'keyvisual-image');

        $service = Service::factory()->create([
            'title' => 'Moderator mit Keyvisual',
            'date' => now(),
            'key_visual_filename' => 'slides/keyvisual.jpg',
            'moderator_name' => 'Max Moderation',
        ]);
        $song = $this->createSongWithContent('Startlied');
        $serviceSong = ServiceSong::create([
            'service_id' => $service->id,
            'song_id' => $song->id,
            'cts_song_name' => 'Startlied',
            'order' => 1,
        ]);
        ServiceAgendaItem::factory()->create([
            'service_id' => $service->id,
            'title' => 'Startlied',
            'service_song_id' => $serviceSong->id,
            'sort_order' => 1,
            'is_before_event' => false,
            'responsible' => [],
        ]);

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $entries = $playlist->getEntries();

        $this->assertSame('Moderator - Max Moderation', $entries[0]->getName());
        $moderatorSlides = $this->slidesForEntry($playlist, $entries[0]);
        $this->assertCount(3, $moderatorSlides);

        // Cue 1 + cue 3: key-visual only (no content element, no macro).
        $this->assertTrue($moderatorSlides[0]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $moderatorSlides[0]->getBackgroundMediaUrl());
        $this->assertSame([], $this->fillMediaUrls($moderatorSlides[0]));
        $this->assertFalse($moderatorSlides[0]->hasMacro());
        $this->assertTrue($moderatorSlides[2]->hasBackgroundMedia());
        $this->assertSame([], $this->fillMediaUrls($moderatorSlides[2]));
        $this->assertFalse($moderatorSlides[2]->hasMacro());

        // Cue 2: name tag + Namenseinblender macro + 10s auto-next.
        $this->assertTrue($moderatorSlides[1]->hasBackgroundMedia());
        $this->assertTrue($this->elementHasTextAt($moderatorSlides[1], 0));
        [$name, $subtitle] = $this->nameTagNameAndSubtitle($moderatorSlides[1]);
        $this->assertSame('Max Moderation', $name);
        $this->assertSame('Moderation', $subtitle);
        $this->assertTrue($moderatorSlides[1]->hasMacro());
        $this->assertSame(10.0, $moderatorSlides[1]->getCue()->getCompletionTime());
        $this->assertSame(
            CompletionTargetType::COMPLETION_TARGET_TYPE_NEXT,
            $moderatorSlides[1]->getCue()->getCompletionTargetType(),
        );

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

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

        $service = Service::factory()->create([
            'title' => 'Zwei Lieder',
            'date' => now(),
            'key_visual_filename' => 'slides/keyvisual.jpg',
            'moderator_name' => null,
        ]);

        foreach (['Lied A', 'Lied B'] as $i => $title) {
            $song = $this->createSongWithContent($title);
            $serviceSong = ServiceSong::create([
                'service_id' => $service->id,
                'song_id' => $song->id,
                'cts_song_name' => $title,
                'order' => $i + 1,
            ]);
            ServiceAgendaItem::factory()->create([
                'service_id' => $service->id,
                'title' => $title,
                'service_song_id' => $serviceSong->id,
                'sort_order' => $i + 1,
                'is_before_event' => false,
                'responsible' => [],
            ]);
        }

        $result = app(PlaylistExportService::class)->generatePlaylist($service);
        $playlist = ProPlaylistReader::read($result['path']);
        $names = $this->entryNames($playlist);

        // A single key-visual transition sits BETWEEN the two songs; none leads the
        // first song, and the trailing key-visual is provided by the looping Abspann.
        $this->assertSame(['Lied A', 'Keyvisual', 'Lied B', 'Abspann'], $names);

        $entries = $playlist->getEntries();
        $kvSlides = $this->slidesForEntry($playlist, $entries[1]);
        $this->assertCount(1, $kvSlides);
        $this->assertTrue($kvSlides[0]->hasBackgroundMedia());
        $this->assertSame('KEY_VISUAL.jpg', $kvSlides[0]->getBackgroundMediaUrl());
        $this->assertSame([], $this->fillMediaUrls($kvSlides[0]));
        $this->assertFalse($this->hasForegroundImageMediaAction($kvSlides[0]));

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

    private function configureNameTagMacro(): void
    {
        Setting::set('namenseinblender_macro_name', 'Namenseinblender');
        Setting::set('namenseinblender_macro_uuid', '11111111-1111-4111-8111-111111111111');
        Setting::set('namenseinblender_macro_collection_name', 'Service Macros');
        Setting::set('namenseinblender_macro_collection_uuid', '22222222-2222-4222-8222-222222222222');
    }

    private function createSermonSlide(Service $service, ServiceAgendaItem $agendaItem, string $filename, int $sortOrder): Slide
    {
        return Slide::factory()->create([
            'service_id' => $service->id,
            'service_agenda_item_id' => $agendaItem->id,
            'type' => 'sermon',
            'original_filename' => $filename,
            'stored_filename' => 'slides/'.$filename,
            'sort_order' => $sortOrder,
        ]);
    }

    private function createSongWithContent(string $title): Song
    {
        $song = Song::create([
            'title' => $title,
            'ccli_id' => fake()->unique()->numerify('#####'),
            'author' => 'Test Author',
            'copyright_text' => 'Test Publisher',
        ]);

        $label = Label::firstOrCreate(
            ['name' => 'Verse 1 - '.$title],
            ['color' => '#2196F3'],
        );
        $section = $song->sections()->create(['label_id' => $label->id, 'order' => 0]);
        $section->slides()->create(['order' => 0, 'text_content' => 'Erste Zeile']);

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

        return $song;
    }

    /** @return array<int, string> */
    private function entryNames(PlaylistArchive $playlist): array
    {
        return array_map(fn ($entry) => $entry->getName(), $playlist->getEntries());
    }

    private function slidesForEntry(PlaylistArchive $playlist, $entry): array
    {
        $filename = $entry->getDocumentFilename();
        $this->assertNotNull($filename);

        return $this->allParserSlides($playlist->getEmbeddedSong($filename));
    }

    /**
     * Split a name-tag slide into its [name, subtitle] parts. The generator emits
     * the name as the main (\fs84) run and the role as a separate smaller,
     * non-bold (\b0\fs50) subtitle run on a new line inside the same RTF element.
     *
     * @return array{0: string, 1: string}
     */
    private function nameTagNameAndSubtitle(\ProPresenter\Parser\Slide $slide): array
    {
        $elements = $slide->getTextElements();
        $this->assertNotEmpty($elements);
        $rtf = $elements[0]->getRtfData();
        $this->assertStringContainsString('\b0\fs50', $rtf, 'Name-tag slide is missing the subtitle run');

        // getPlainText() returns both runs joined by a newline ("Name\nRole").
        $plain = $slide->getPlainText();
        $parts = explode("\n", $plain, 2);
        $this->assertCount(2, $parts, 'Name-tag slide does not contain a name and a subtitle line');

        return [trim($parts[0]), trim($parts[1])];
    }

    private function allParserSlides(?\ProPresenter\Parser\Song $parserSong): array
    {
        $this->assertNotNull($parserSong);

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

        return $slides;
    }

    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);
    }
}
