"""Tests for card_renderer: ffmpeg card generation and concat pipeline.

Tests verify command construction, concat list file format, and template
config parsing — no actual ffmpeg execution required.
"""

from __future__ import annotations

import subprocess
import textwrap
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from pipeline.card_renderer import (
    DEFAULT_ACCENT_COLOR,
    DEFAULT_FONT_FAMILY,
    DEFAULT_INTRO_DURATION,
    DEFAULT_OUTRO_DURATION,
    build_concat_list,
    concat_segments,
    parse_template_config,
    render_card,
    render_card_to_file,
)


# ── render_card tests ────────────────────────────────────────────────────────

class TestRenderCard:
    """Tests for render_card() ffmpeg command generation."""

    def test_returns_list_of_strings(self):
        cmd = render_card("Hello", 2.0, 1080, 1920)
        assert isinstance(cmd, list)
        assert all(isinstance(s, str) for s in cmd)

    def test_contains_ffmpeg_and_lavfi(self):
        cmd = render_card("Test", 3.0, 1080, 1920)
        assert cmd[0] == "ffmpeg"
        assert "-f" in cmd
        lavfi_idx = cmd.index("-f")
        assert cmd[lavfi_idx + 1] == "lavfi"

    def test_contains_correct_dimensions_in_filtergraph(self):
        cmd = render_card("Test", 2.0, 1920, 1080)
        # The filtergraph is the arg after -i for lavfi
        filtergraph = None
        for i, arg in enumerate(cmd):
            if arg == "-i" and i > 0 and cmd[i - 1] == "lavfi":
                filtergraph = cmd[i + 1]
                break
        assert filtergraph is not None
        assert "s=1920x1080" in filtergraph

    def test_contains_duration_in_filtergraph(self):
        cmd = render_card("Test", 5.5, 1080, 1920)
        filtergraph = None
        for i, arg in enumerate(cmd):
            if arg == "-i" and i > 0 and cmd[i - 1] == "lavfi":
                filtergraph = cmd[i + 1]
                break
        assert "d=5.5" in filtergraph

    def test_contains_drawtext_with_text(self):
        cmd = render_card("My Creator", 2.0, 1080, 1920)
        filtergraph = None
        for i, arg in enumerate(cmd):
            if arg == "-i" and i > 0 and cmd[i - 1] == "lavfi":
                filtergraph = cmd[i + 1]
                break
        assert "drawtext=" in filtergraph
        assert "My Creator" in filtergraph

    def test_codec_settings(self):
        cmd = render_card("Test", 2.0, 1080, 1920)
        assert "-c:v" in cmd
        assert "libx264" in cmd
        assert "-c:a" in cmd
        assert "aac" in cmd

    def test_silent_audio_track(self):
        """Card includes anullsrc so concat with audio segments works."""
        cmd = render_card("Test", 2.0, 1080, 1920)
        # Should have a second -f lavfi -i anullsrc input
        cmd_str = " ".join(cmd)
        assert "anullsrc" in cmd_str

    def test_rejects_zero_duration(self):
        with pytest.raises(ValueError, match="positive"):
            render_card("Test", 0, 1080, 1920)

    def test_rejects_negative_duration(self):
        with pytest.raises(ValueError, match="positive"):
            render_card("Test", -1.0, 1080, 1920)

    def test_rejects_zero_dimensions(self):
        with pytest.raises(ValueError, match="positive"):
            render_card("Test", 2.0, 0, 1920)

    def test_custom_accent_color(self):
        cmd = render_card("Test", 2.0, 1080, 1920, accent_color="#ff0000")
        filtergraph = None
        for i, arg in enumerate(cmd):
            if arg == "-i" and i > 0 and cmd[i - 1] == "lavfi":
                filtergraph = cmd[i + 1]
                break
        assert "#ff0000" in filtergraph

    def test_escapes_colons_in_text(self):
        cmd = render_card("Hello: World", 2.0, 1080, 1920)
        filtergraph = None
        for i, arg in enumerate(cmd):
            if arg == "-i" and i > 0 and cmd[i - 1] == "lavfi":
                filtergraph = cmd[i + 1]
                break
        # Colons should be escaped for ffmpeg
        assert "Hello\\: World" in filtergraph


# ── render_card_to_file tests ────────────────────────────────────────────────

class TestRenderCardToFile:
    """Tests for render_card_to_file() — mocked ffmpeg execution."""

    @patch("pipeline.card_renderer.subprocess.run")
    def test_calls_ffmpeg_and_returns_path(self, mock_run, tmp_path):
        mock_run.return_value = MagicMock(returncode=0)
        out = tmp_path / "card.mp4"
        out.write_bytes(b"fake")  # stat().st_size needs the file

        result = render_card_to_file("Hi", 2.0, 1080, 1920, out)
        assert result == out
        mock_run.assert_called_once()
        # Output path should be the last arg
        call_args = mock_run.call_args[0][0]
        assert call_args[-1] == str(out)

    @patch("pipeline.card_renderer.subprocess.run")
    def test_raises_on_ffmpeg_failure(self, mock_run, tmp_path):
        mock_run.return_value = MagicMock(
            returncode=1,
            stderr=b"error: something failed",
        )
        out = tmp_path / "card.mp4"
        with pytest.raises(subprocess.CalledProcessError):
            render_card_to_file("Hi", 2.0, 1080, 1920, out)


# ── build_concat_list tests ─────────────────────────────────────────────────

class TestBuildConcatList:
    """Tests for build_concat_list() file content."""

    def test_writes_correct_format(self, tmp_path):
        seg1 = tmp_path / "intro.mp4"
        seg2 = tmp_path / "main.mp4"
        seg1.touch()
        seg2.touch()

        list_file = tmp_path / "concat.txt"
        result = build_concat_list([seg1, seg2], list_file)

        assert result == list_file
        content = list_file.read_text()
        lines = content.strip().split("\n")
        assert len(lines) == 2
        assert lines[0] == f"file '{seg1.resolve()}'"
        assert lines[1] == f"file '{seg2.resolve()}'"

    def test_three_segments(self, tmp_path):
        segs = [tmp_path / f"seg{i}.mp4" for i in range(3)]
        for s in segs:
            s.touch()

        list_file = tmp_path / "list.txt"
        build_concat_list(segs, list_file)

        content = list_file.read_text()
        lines = content.strip().split("\n")
        assert len(lines) == 3


# ── concat_segments tests ────────────────────────────────────────────────────

class TestConcatSegments:
    """Tests for concat_segments() — mocked ffmpeg execution."""

    @patch("pipeline.card_renderer.subprocess.run")
    def test_calls_ffmpeg_concat_demuxer(self, mock_run, tmp_path):
        mock_run.return_value = MagicMock(returncode=0)
        seg1 = tmp_path / "seg1.mp4"
        seg2 = tmp_path / "seg2.mp4"
        seg1.touch()
        seg2.touch()
        out = tmp_path / "output.mp4"
        out.write_bytes(b"fakemp4")

        result = concat_segments([seg1, seg2], out)
        assert result == out
        mock_run.assert_called_once()

        call_args = mock_run.call_args[0][0]
        assert "concat" in call_args
        assert "-safe" in call_args
        assert "0" in call_args
        assert "-c" in call_args
        assert "copy" in call_args

    def test_rejects_empty_segments(self):
        with pytest.raises(ValueError, match="empty"):
            concat_segments([], Path("/tmp/out.mp4"))

    @patch("pipeline.card_renderer.subprocess.run")
    def test_raises_on_ffmpeg_failure(self, mock_run, tmp_path):
        mock_run.return_value = MagicMock(
            returncode=1, stderr=b"concat error",
        )
        seg1 = tmp_path / "s.mp4"
        seg1.touch()
        out = tmp_path / "out.mp4"

        with pytest.raises(subprocess.CalledProcessError):
            concat_segments([seg1], out)


# ── parse_template_config tests ──────────────────────────────────────────────

class TestParseTemplateConfig:
    """Tests for parse_template_config() defaults and overrides."""

    def test_none_returns_all_defaults_disabled(self):
        cfg = parse_template_config(None)
        assert cfg["show_intro"] is False
        assert cfg["show_outro"] is False
        assert cfg["accent_color"] == DEFAULT_ACCENT_COLOR
        assert cfg["font_family"] == DEFAULT_FONT_FAMILY
        assert cfg["intro_duration"] == DEFAULT_INTRO_DURATION
        assert cfg["outro_duration"] == DEFAULT_OUTRO_DURATION

    def test_empty_dict_returns_defaults_disabled(self):
        cfg = parse_template_config({})
        assert cfg["show_intro"] is False
        assert cfg["show_outro"] is False

    def test_full_config_preserves_values(self):
        raw = {
            "show_intro": True,
            "intro_text": "Welcome!",
            "intro_duration": 3.0,
            "show_outro": True,
            "outro_text": "Bye!",
            "outro_duration": 1.5,
            "accent_color": "#ff0000",
            "font_family": "Roboto",
        }
        cfg = parse_template_config(raw)
        assert cfg["show_intro"] is True
        assert cfg["intro_text"] == "Welcome!"
        assert cfg["intro_duration"] == 3.0
        assert cfg["show_outro"] is True
        assert cfg["outro_text"] == "Bye!"
        assert cfg["outro_duration"] == 1.5
        assert cfg["accent_color"] == "#ff0000"
        assert cfg["font_family"] == "Roboto"

    def test_partial_config_fills_defaults(self):
        raw = {"show_intro": True, "intro_text": "Hi"}
        cfg = parse_template_config(raw)
        assert cfg["show_intro"] is True
        assert cfg["intro_text"] == "Hi"
        assert cfg["intro_duration"] == DEFAULT_INTRO_DURATION
        assert cfg["show_outro"] is False
        assert cfg["outro_text"] == ""
        assert cfg["accent_color"] == DEFAULT_ACCENT_COLOR

    def test_truthy_coercion(self):
        """Non-bool truthy values should coerce to bool."""
        cfg = parse_template_config({"show_intro": 1, "show_outro": 0})
        assert cfg["show_intro"] is True
        assert cfg["show_outro"] is False

    def test_duration_coercion_from_int(self):
        cfg = parse_template_config({"intro_duration": 5})
        assert cfg["intro_duration"] == 5.0
        assert isinstance(cfg["intro_duration"], float)


# ── extract_clip_with_template tests ─────────────────────────────────────────

class TestExtractClipWithTemplate:
    """Tests for the shorts_generator.extract_clip_with_template function."""

    @patch("pipeline.shorts_generator.extract_clip")
    def test_no_cards_delegates_to_extract_clip(self, mock_extract):
        from pipeline.shorts_generator import extract_clip_with_template
        extract_clip_with_template(
            input_path=Path("/fake/input.mp4"),
            output_path=Path("/fake/output.mp4"),
            start_secs=10.0,
            end_secs=20.0,
            vf_filter="scale=1080:-2",
        )
        mock_extract.assert_called_once()

    @patch("pipeline.card_renderer.concat_segments")
    @patch("pipeline.shorts_generator.extract_clip")
    def test_with_intro_concats_two_segments(self, mock_extract, mock_concat, tmp_path):
        from pipeline.shorts_generator import extract_clip_with_template

        intro = tmp_path / "intro.mp4"
        intro.touch()
        out = tmp_path / "final.mp4"
        main_tmp = Path(str(out) + ".main.mp4")
        # Create the main clip temp file so cleanup doesn't error
        main_tmp.touch()

        mock_concat.return_value = out

        extract_clip_with_template(
            input_path=Path("/fake/input.mp4"),
            output_path=out,
            start_secs=10.0,
            end_secs=20.0,
            vf_filter="scale=1080:-2",
            intro_path=intro,
        )
        mock_extract.assert_called_once()
        mock_concat.assert_called_once()
        # Segments should be [intro, main_clip]
        segments = mock_concat.call_args[1]["segments"]
        assert len(segments) == 2
        assert segments[0] == intro

    @patch("pipeline.card_renderer.concat_segments")
    @patch("pipeline.shorts_generator.extract_clip")
    def test_with_intro_and_outro_concats_three_segments(
        self, mock_extract, mock_concat, tmp_path,
    ):
        from pipeline.shorts_generator import extract_clip_with_template

        intro = tmp_path / "intro.mp4"
        outro = tmp_path / "outro.mp4"
        intro.touch()
        outro.touch()
        out = tmp_path / "final.mp4"
        main_tmp = Path(str(out) + ".main.mp4")
        main_tmp.touch()

        mock_concat.return_value = out

        extract_clip_with_template(
            input_path=Path("/fake/input.mp4"),
            output_path=out,
            start_secs=10.0,
            end_secs=20.0,
            vf_filter="scale=1080:-2",
            intro_path=intro,
            outro_path=outro,
        )
        segments = mock_concat.call_args[1]["segments"]
        assert len(segments) == 3
        assert segments[0] == intro
        assert segments[2] == outro
