create(); $arrangement = SongArrangement::factory()->create([ 'song_id' => $song->id, 'name' => 'normal', 'is_default' => true, ]); foreach ($labelConfig as $order => $config) { ['label_name' => $labelName, 'slide_count' => $slideCount] = $config; $label = Label::firstOrCreate( ['name' => $labelName], ['color' => '#3B82F6'], ); SongArrangementLabel::create([ 'song_arrangement_id' => $arrangement->id, 'label_id' => $label->id, 'order' => $order + 1, ]); for ($i = 0; $i < $slideCount; $i++) { SongSlide::create([ 'label_id' => $label->id, 'order' => $i + 1, 'text_content' => "Original line $i for $labelName", ]); } } return $song; } test('pairs matching English labels with CCLI sections', function (): void { $song = makeLocalSongForCcliPairing([ ['label_name' => 'Verse 1', 'slide_count' => 2], ['label_name' => 'Chorus', 'slide_count' => 1], ['label_name' => 'Verse 2', 'slide_count' => 2], ]); $ccliText = "Test Song\nTest Artist\n\nVerse 1\nCCLI line 1\nCCLI line 2\n\nChorus\nCCLI chorus\n\nVerse 2\nCCLI v2 line1\nCCLI v2 line2\n\n© 2024 Test\nCCLI # 9999001"; $result = app(CcliTranslationPairingService::class)->pair($song, $ccliText); expect($result['unmatched_labels'])->toBeEmpty('All labels should match') ->and($result['mapping'])->toHaveCount(3) ->and($result['distributed_text'])->not->toBeEmpty(); foreach ($result['mapping'] as $entry) { expect($entry['ccli_label'])->not->toBeNull("Label {$entry['local_label']} should have a CCLI match"); } }); test('pairs German local labels with English CCLI labels via normalization', function (): void { $song = makeLocalSongForCcliPairing([ ['label_name' => 'Strophe 1', 'slide_count' => 2], ['label_name' => 'Refrain', 'slide_count' => 2], ['label_name' => 'Strophe 2', 'slide_count' => 2], ]); $ccliText = "Test Song\nTest Artist\n\nVerse 1\nEN line 1\nEN line 2\n\nChorus\nEN chorus 1\nEN chorus 2\n\nVerse 2\nEN v2 1\nEN v2 2\n\n© 2024 Test\nCCLI # 9999002"; $result = app(CcliTranslationPairingService::class)->pair($song, $ccliText); expect($result['unmatched_labels'])->toBeEmpty('German labels should normalize to match English CCLI labels') ->and($result['mapping'])->toHaveCount(3) ->and($result['mapping'][0]['ccli_label'])->toBe('Verse 1') ->and($result['mapping'][1]['ccli_label'])->toBe('Chorus'); }); test('returns unmatched_labels for sections not in CCLI', function (): void { $song = makeLocalSongForCcliPairing([ ['label_name' => 'Verse 1', 'slide_count' => 2], ['label_name' => 'Chorus', 'slide_count' => 1], ['label_name' => 'Bridge', 'slide_count' => 2], ]); $ccliText = "Test Song\nTest Artist\n\nVerse 1\nLine 1\nLine 2\n\nChorus\nChorus line\n\n© 2024 Test\nCCLI # 9999003"; $result = app(CcliTranslationPairingService::class)->pair($song, $ccliText); expect($result['unmatched_labels'])->toContain('Bridge') ->and($result['mapping'])->toHaveCount(3) ->and($result['mapping'][2]['ccli_label'])->toBeNull() ->and($result['mapping'][2]['distributed_lines'])->toBe(['', '']); }); test('distributes lines preserving local slide count', function (): void { $song = makeLocalSongForCcliPairing([ ['label_name' => 'Verse 1', 'slide_count' => 2], ]); $ccliText = "Test Song\nTest Artist\n\nVerse 1\nLine 1\nLine 2\nLine 3\nLine 4\n\n© 2024\nCCLI # 9999004"; $result = app(CcliTranslationPairingService::class)->pair($song, $ccliText); expect($result['mapping'][0]['distributed_lines'])->toHaveCount(2) ->and($result['distributed_text'])->toContain('Line 4'); }); test('uses linesTranslated from CCLI when available', function (): void { $song = makeLocalSongForCcliPairing([ ['label_name' => 'Verse 1', 'slide_count' => 2], ['label_name' => 'Chorus', 'slide_count' => 1], ]); $ccliText = file_get_contents(base_path('tests/fixtures/ccli/english-german-side-by-side.txt')); $result = app(CcliTranslationPairingService::class)->pair($song, $ccliText); expect($result['song'])->toBeInstanceOf(Song::class) ->and($result['mapping'])->toBeArray() ->and($result['distributed_text'])->toContain('Deutsche Liedzeile') ->and($result['distributed_text'])->toContain('Deutscher Refrain'); });