<?php

namespace Tests\Unit\Services\Video;

use App\Services\Video\SpokenCaptionBurner;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class SpokenCaptionBurnerTest extends TestCase
{
    public function test_it_splits_the_spoken_line_into_short_phrases(): void
    {
        $burner = new SpokenCaptionBurner;

        $this->assertSame(
            ['switch settings.', 'Grab it'],
            $burner->phrases('switch settings. Grab it'),
        );
        $this->assertSame(
            ['one two three four', 'five six'],
            $burner->phrases('one two three four five six'),
        );
    }

    public function test_it_starts_captions_when_speech_starts_and_hides_them_during_a_pause(): void
    {
        $burner = new SpokenCaptionBurner;
        $intervals = $burner->speechIntervals("silence_start: 0\nsilence_end: 1.2 | silence_duration: 1.2\n", 8.0);

        $this->assertEqualsWithDelta(1.2, $intervals[0][0], 0.001);
        $this->assertEqualsWithDelta(8.0, $intervals[0][1], 0.001);

        $cues = $burner->cues('switch settings. Grab it', [[0.0, 1.0], [3.0, 5.0]]);

        $this->assertEqualsWithDelta(0.0, $cues[0]['start'], 0.001);
        foreach ($cues as $cue) {
            $this->assertFalse($cue['start'] < 2.0 && $cue['end'] > 2.0);
        }
        $this->assertTrue(collect($cues)->contains(fn (array $cue): bool => $cue['start'] >= 3.0));
    }

    public function test_it_times_captions_to_heard_words_and_ignores_speech_after_the_line(): void
    {
        $cues = (new SpokenCaptionBurner)->cuesFromHeardWords('switch settings. Grab it', [
            ['word' => 'switch', 'start' => 0.2, 'end' => 0.6],
            ['word' => 'settings', 'start' => 0.6, 'end' => 1.1],
            ['word' => 'Grab', 'start' => 1.2, 'end' => 1.5],
            ['word' => 'it', 'start' => 1.5, 'end' => 1.8],
            ['word' => 'thanks', 'start' => 4.0, 'end' => 4.4],
        ]);

        $this->assertSame('switch settings.', $cues[0]['text']);
        $this->assertEqualsWithDelta(0.2, $cues[0]['start'], 0.001);
        $this->assertEqualsWithDelta(1.1, $cues[0]['end'], 0.001);
        $this->assertSame('Grab it', $cues[1]['text']);
        $this->assertEqualsWithDelta(1.2, $cues[1]['start'], 0.001);
        $this->assertEqualsWithDelta(1.8, $cues[1]['end'], 0.001);
        $this->assertCount(2, $cues);
    }

    public function test_it_stops_the_fallback_captions_when_the_scripted_line_should_end(): void
    {
        config(['creative.spoken_words_per_second' => 2]);

        $intervals = (new SpokenCaptionBurner)->limitSpeechToLine([[0.0, 15.0]], 'one two three four');

        $this->assertEqualsWithDelta(0.0, $intervals[0][0], 0.001);
        $this->assertEqualsWithDelta(2.35, $intervals[0][1], 0.001);
    }

    public function test_it_writes_bottom_centered_white_outlined_captions(): void
    {
        $document = (new SpokenCaptionBurner)->assDocument([
            ['text' => 'Grab {it}', 'start' => 0.2, 'end' => 1.0],
        ], 1080, 1920);

        $this->assertStringContainsString('PlayResX: 1080', $document);
        $this->assertStringContainsString('PlayResY: 1920', $document);
        $this->assertStringContainsString('&H00FFFFFF', $document);
        $this->assertStringContainsString('&H00000000', $document);
        $this->assertStringContainsString(',2,40,40,', $document);
        $this->assertStringContainsString('Grab \\{it\\}', $document);
    }

    public function test_it_leaves_the_file_unchanged_when_ffmpeg_is_missing(): void
    {
        Storage::fake('public');
        Storage::disk('public')->put('creatives/videos/plain.mp4', 'not-a-video');
        config(['creative.ffmpeg_binary' => '/tmp/missing-ffmpeg-binary']);

        $burned = (new SpokenCaptionBurner)->apply('public', 'creatives/videos/plain.mp4', 'switch settings. Grab it');

        $this->assertFalse($burned);
        $this->assertSame('not-a-video', Storage::disk('public')->get('creatives/videos/plain.mp4'));
    }

    public function test_it_burns_white_bottom_captions_into_the_video(): void
    {
        $ffmpeg = $this->ffmpegBinary();
        if ($ffmpeg === null) {
            $this->markTestSkipped('ffmpeg is not installed');
        }

        Storage::fake('public');
        config(['creative.ffmpeg_binary' => $ffmpeg]);
        $relative = 'creatives/videos/caption.mp4';
        Storage::disk('public')->put($relative, 'placeholder');
        $absolute = Storage::disk('public')->path($relative);

        $generated = Process::timeout(30)->run([
            $ffmpeg, '-y', '-hide_banner', '-loglevel', 'error',
            '-f', 'lavfi', '-i', 'color=c=black:s=320x180:d=2',
            '-f', 'lavfi', '-i', 'sine=frequency=440:sample_rate=44100:duration=2',
            '-shortest', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac',
            $absolute,
        ]);
        $this->assertTrue($generated->successful());

        $burned = (new SpokenCaptionBurner)->apply('public', $relative, 'switch settings. Grab it');

        $this->assertTrue($burned);
        $frame = tempnam(sys_get_temp_dir(), 'caption-frame-');
        $extracted = Process::timeout(30)->run([
            $ffmpeg, '-y', '-hide_banner', '-loglevel', 'error',
            '-ss', '0.4', '-i', $absolute,
            '-frames:v', '1', '-f', 'rawvideo', '-pix_fmt', 'rgb24',
            $frame,
        ]);
        $this->assertTrue($extracted->successful());
        $pixels = file_get_contents($frame);
        @unlink($frame);

        $this->assertIsString($pixels);
        $this->assertSame(0, $this->brightPixels($pixels, 320, 0, 80));
        $this->assertGreaterThan(20, $this->brightPixels($pixels, 320, 100, 180));
    }

    private function ffmpegBinary(): ?string
    {
        foreach (['/home/mehak/bin/ffmpeg', '/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg'] as $candidate) {
            if (is_executable($candidate)) {
                return $candidate;
            }
        }

        return null;
    }

    private function brightPixels(string $frame, int $width, int $top, int $bottom): int
    {
        $bright = 0;
        for ($y = $top; $y < $bottom; $y++) {
            for ($x = 0; $x < $width; $x++) {
                $offset = ($y * $width + $x) * 3;
                if (ord($frame[$offset]) > 200 && ord($frame[$offset + 1]) > 200 && ord($frame[$offset + 2]) > 200) {
                    $bright++;
                }
            }
        }

        return $bright;
    }
}
