"""concat_engine_pure 单元测试."""

from pathlib import Path

from apps.worker.video_processing.concat_engine_pure import (
    build_concat_filter,
    build_fps_filter,
    build_scale_pad_filter,
    build_setpts_filter,
    build_single_segment_filter_chain,
    calculate_scaled_size,
    can_use_stream_copy,
    count_valid_segments,
    estimate_total_duration,
    format_fps_filter,
    generate_concat_file_list,
    parse_fps,
    resolve_output_params,
    validate_concat_config,
    validate_video_path,
)

# ── parse_fps ────────────────────────────────────────────────────────────────


class TestParseFps:
    def test_none_returns_default(self):
        assert parse_fps(None) == 30.0

    def test_integer_value(self):
        assert parse_fps(30) == 30.0
        assert parse_fps(24) == 24.0

    def test_float_value(self):
        assert parse_fps(29.97) == 29.97

    def test_string_integer(self):
        assert parse_fps("30") == 30.0
        assert parse_fps("  60  ") == 60.0  # 带空格

    def test_string_fraction(self):
        assert parse_fps("30/1") == 30.0
        assert abs(parse_fps("24000/1001") - 23.976) < 0.01

    def test_zero_denominator(self):
        assert parse_fps("30/0") == 30.0

    def test_empty_string(self):
        assert parse_fps("") == 30.0
        assert parse_fps("   ") == 30.0

    def test_invalid_string(self):
        assert parse_fps("abc") == 30.0
        assert parse_fps("30fps") == 30.0

    def test_negative_fps(self):
        assert parse_fps(-30) == -30.0

    def test_zero_fps(self):
        assert parse_fps(0) == 0.0


# ── format_fps_filter ───────────────────────────────────────────────────────


class TestFormatFpsFilter:
    def test_integer_fps(self):
        assert format_fps_filter(30.0) == "fps=30"

    def test_near_integer_fps(self):
        # 接近整数时用整数形式（注意：int(fps)是截断不是四舍五入）
        assert format_fps_filter(30.0001) == "fps=30"
        assert format_fps_filter(30.0005) == "fps=30"  # int(30.0005)=30

    def test_non_integer_fps(self):
        result = format_fps_filter(23.976)
        assert result.startswith("fps=")
        assert "23.976" in result

    def test_float_precision(self):
        result = format_fps_filter(29.97)
        assert result.startswith("fps=")
        # 三位小数
        parts = result.split("=")[1]
        assert len(parts.split(".")[1]) == 3

    def test_one_fps(self):
        assert format_fps_filter(1.0) == "fps=1"


# ── resolve_output_params ───────────────────────────────────────────────────


class TestResolveOutputParams:
    def test_config_specified(self):
        w, h, fps = resolve_output_params(1920, 1080, 60.0)
        assert w == 1920
        assert h == 1080
        assert fps == 60.0

    def test_fallback_to_first_video_info(self):
        info = {"width": 1280, "height": 720, "r_frame_rate": "24/1"}
        w, h, fps = resolve_output_params(0, 0, 0, info)
        assert w == 1280
        assert h == 720
        assert fps == 24.0

    def test_fallback_to_defaults(self):
        w, h, fps = resolve_output_params(0, 0, 0)
        assert w == 1080  # default_width
        assert h == 1920  # default_height
        assert fps == 30.0

    def test_partial_config(self):
        # 宽度配置了，高度和帧率用探测的
        info = {"width": 1280, "height": 720, "r_frame_rate": "24/1"}
        w, h, fps = resolve_output_params(1920, 0, 0, info)
        assert w == 1920
        assert h == 720
        assert fps == 24.0

    def test_custom_defaults(self):
        w, h, fps = resolve_output_params(
            0,
            0,
            0,
            default_width=640,
            default_height=480,
            default_fps=25.0,
        )
        assert w == 640
        assert h == 480
        assert fps == 25.0

    def test_minimum_size(self):
        w, h, fps = resolve_output_params(0, 0, 0, {"width": 0, "height": 0, "r_frame_rate": "0/1"})
        assert w >= 1
        assert h >= 1
        assert fps >= 1.0

    def test_fps_fraction_in_info(self):
        info = {"width": 1920, "height": 1080, "r_frame_rate": "24000/1001"}
        _, _, fps = resolve_output_params(0, 0, 0, info)
        assert abs(fps - 23.976) < 0.01


# ── calculate_scaled_size ───────────────────────────────────────────────────


class TestCalculateScaledSize:
    def test_same_ratio(self):
        sw, sh, ox, oy = calculate_scaled_size(1920, 1080, 1920, 1080)
        assert sw == 1920
        assert sh == 1080
        assert ox == 0
        assert oy == 0

    def test_wider_source_pad_top_bottom(self):
        # 源是16:9，目标是9:16竖屏 → 上下填黑边
        sw, sh, ox, oy = calculate_scaled_size(1920, 1080, 1080, 1920)
        assert sw == 1080  # 以宽度为准
        assert sh == 607  # 1080 * 1080 / 1920 = 607.5 → 607
        assert ox == 0
        assert oy > 0  # 垂直居中

    def test_taller_source_pad_left_right(self):
        # 源是9:16竖屏，目标是16:9横屏 → 左右填黑边
        sw, sh, ox, oy = calculate_scaled_size(1080, 1920, 1920, 1080)
        assert sh == 1080  # 以高度为准
        assert sw == 607  # 1080 * 1080 / 1920 = 607.5 → 607
        assert ox > 0  # 水平居中
        assert oy == 0

    def test_zero_source_size(self):
        sw, sh, ox, oy = calculate_scaled_size(0, 0, 1920, 1080)
        assert sw == 1920
        assert sh == 1080
        assert ox == 0
        assert oy == 0

    def test_negative_source_size(self):
        sw, sh, ox, oy = calculate_scaled_size(-1, -1, 1920, 1080)
        assert sw == 1920
        assert sh == 1080
        assert ox == 0
        assert oy == 0

    def test_target_same_ratio_different_size(self):
        # 比例相同，尺寸不同 → 直接缩放到目标大小
        sw, sh, ox, oy = calculate_scaled_size(640, 360, 1920, 1080)
        assert sw == 1920
        assert sh == 1080
        assert ox == 0
        assert oy == 0


# ── can_use_stream_copy ─────────────────────────────────────────────────────


class TestCanUseStreamCopy:
    def test_force_reencode_false(self):
        assert can_use_stream_copy([], 1920, 1080, 30.0, force_reencode=True) is False

    def test_empty_segments(self):
        assert can_use_stream_copy([], 1920, 1080, 30.0) is False

    def test_single_segment_matching_params(self):
        segs = [{"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"}]
        assert can_use_stream_copy(segs, 1920, 1080, 30.0) is True

    def test_multiple_segments_same_params(self):
        segs = [
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
        ]
        assert can_use_stream_copy(segs, 1920, 1080, 30.0) is True

    def test_different_codec(self):
        segs = [
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
            {"codec_name": "hevc", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
        ]
        assert can_use_stream_copy(segs, 1920, 1080, 30.0) is False

    def test_different_resolution(self):
        segs = [
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
            {"codec_name": "h264", "width": 1280, "height": 720, "r_frame_rate": "30/1"},
        ]
        assert can_use_stream_copy(segs, 1920, 1080, 30.0) is False

    def test_different_fps(self):
        segs = [
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"},
            {"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "60/1"},
        ]
        assert can_use_stream_copy(segs, 1920, 1080, 30.0) is False

    def test_target_differs_from_source(self):
        segs = [{"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "30/1"}]
        # 目标分辨率不同
        assert can_use_stream_copy(segs, 1280, 720, 30.0) is False
        # 目标帧率不同
        assert can_use_stream_copy(segs, 1920, 1080, 60.0) is False

    def test_fps_fraction_match(self):
        segs = [{"codec_name": "h264", "width": 1920, "height": 1080, "r_frame_rate": "24000/1001"}]
        assert can_use_stream_copy(segs, 1920, 1080, 23.976) is True


# ── generate_concat_file_list ───────────────────────────────────────────────


class TestGenerateConcatFileList:
    def test_single_file(self):
        result = generate_concat_file_list(["/tmp/video.mp4"])
        assert result == "file '/tmp/video.mp4'\n"

    def test_multiple_files(self):
        result = generate_concat_file_list(["/a.mp4", "/b.mp4", "/c.mp4"])
        lines = result.strip().split("\n")
        assert len(lines) == 3
        assert lines[0] == "file '/a.mp4'"
        assert lines[1] == "file '/b.mp4'"
        assert lines[2] == "file '/c.mp4'"
        assert result.endswith("\n")

    def test_escapes_single_quotes(self):
        result = generate_concat_file_list(["/path/with'quote.mp4"])
        # 单引号转义: '\''
        assert "'\\''" in result

    def test_empty_list(self):
        result = generate_concat_file_list([])
        assert result == "\n"

    def test_path_with_spaces(self):
        result = generate_concat_file_list(["/path/to/video file.mp4"])
        assert "file '/path/to/video file.mp4'" in result


# ── build_scale_pad_filter ──────────────────────────────────────────────────


class TestBuildScalePadFilter:
    def test_basic_filter(self):
        result = build_scale_pad_filter(1920, 1080)
        assert "scale=1920:1080" in result
        assert "force_original_aspect_ratio=decrease" in result
        assert "pad=1920:1080" in result
        assert "black" in result
        assert "(ow-iw)/2" in result
        assert "(oh-ih)/2" in result

    def test_different_resolution(self):
        result = build_scale_pad_filter(1080, 1920)
        assert "scale=1080:1920" in result
        assert "pad=1080:1920" in result

    def test_ignores_source_size(self):
        # src_w/src_h 目前不影响输出，都是用表达式
        result1 = build_scale_pad_filter(1920, 1080)
        result2 = build_scale_pad_filter(1920, 1080, src_w=1280, src_h=720)
        assert result1 == result2


# ── build_fps_filter ────────────────────────────────────────────────────────


class TestBuildFpsFilter:
    def test_integer_fps(self):
        assert build_fps_filter(30.0) == "fps=30"

    def test_float_fps(self):
        result = build_fps_filter(29.97)
        assert result.startswith("fps=")


# ── build_setpts_filter ─────────────────────────────────────────────────────


class TestBuildSetptsFilter:
    def test_returns_correct_string(self):
        assert build_setpts_filter() == "setpts=PTS-STARTPTS"


# ── build_concat_filter ─────────────────────────────────────────────────────


class TestBuildConcatFilter:
    def test_zero_inputs(self):
        assert build_concat_filter(0) == ""

    def test_single_input_with_audio(self):
        result = build_concat_filter(1)
        assert "[0:v][0:a]" in result
        assert "concat=n=1:v=1:a=1" in result
        assert "[concat_v][concat_a]" in result

    def test_single_input_no_audio(self):
        result = build_concat_filter(1, has_audio=False)
        assert "[0:v]" in result
        assert "concat=n=1:v=1:a=0" in result
        assert "[concat_v]" in result
        assert "[concat_a]" not in result

    def test_multiple_inputs_with_audio(self):
        result = build_concat_filter(3)
        assert "[0:v][0:a][1:v][1:a][2:v][2:a]" in result
        assert "concat=n=3:v=1:a=1" in result

    def test_multiple_inputs_no_audio(self):
        result = build_concat_filter(3, has_audio=False)
        assert "[0:v][1:v][2:v]" in result
        assert "concat=n=3:v=1:a=0" in result

    def test_negative_inputs(self):
        assert build_concat_filter(-1) == ""


# ── build_single_segment_filter_chain ───────────────────────────────────────


class TestBuildSingleSegmentFilterChain:
    def test_with_audio(self):
        result = build_single_segment_filter_chain(1920, 1080, 30.0, 0)
        # 视频链
        assert "[0:v]" in result
        assert "[v0]" in result
        assert "scale=1920:1080" in result
        assert "fps=30" in result
        assert "setpts=PTS-STARTPTS" in result
        # 音频链
        assert "[0:a]" in result
        assert "[a0]" in result
        assert "asetpts=PTS-STARTPTS" in result
        # 用分号分隔
        assert ";" in result

    def test_without_audio(self):
        result = build_single_segment_filter_chain(1920, 1080, 30.0, 2, has_audio=False)
        assert "[2:v]" in result
        assert "[v2]" in result
        assert "[2:a]" not in result
        assert ";" not in result  # 没有音频就没有分号

    def test_segment_index_propagated(self):
        for idx in [0, 5, 10]:
            result = build_single_segment_filter_chain(1920, 1080, 30.0, idx)
            assert f"[{idx}:v]" in result
            assert f"[v{idx}]" in result


# ── validate_concat_config ──────────────────────────────────────────────────


class TestValidateConcatConfig:
    def test_valid_config(self):
        config = {
            "segments": [
                {"video_path": "/a.mp4"},
                {"video_path": "/b.mp4"},
            ],
            "output_width": 1920,
            "output_height": 1080,
            "output_fps": 30,
        }
        valid, errors = validate_concat_config(config)
        assert valid is True
        assert errors == []

    def test_no_segments(self):
        valid, errors = validate_concat_config({})
        assert valid is False
        assert any("至少需要一个" in e for e in errors)

    def test_empty_segments(self):
        valid, errors = validate_concat_config({"segments": []})
        assert valid is False
        assert len(errors) >= 1

    def test_missing_video_path(self):
        config = {"segments": [{"video_path": ""}]}
        valid, errors = validate_concat_config(config)
        assert valid is False
        assert any("video_path" in e for e in errors)

    def test_multiple_missing_paths(self):
        config = {
            "segments": [
                {"video_path": "/a.mp4"},
                {"video_path": ""},
                {"video_path": ""},
            ]
        }
        valid, errors = validate_concat_config(config)
        assert valid is False
        path_errors = [e for e in errors if "video_path" in e]
        assert len(path_errors) == 2

    def test_negative_output_width(self):
        config = {"segments": [{"video_path": "/a.mp4"}], "output_width": -1}
        valid, errors = validate_concat_config(config)
        assert valid is False
        assert any("output_width" in e for e in errors)

    def test_negative_output_height(self):
        config = {"segments": [{"video_path": "/a.mp4"}], "output_height": -1}
        valid, errors = validate_concat_config(config)
        assert valid is False
        assert any("output_height" in e for e in errors)

    def test_negative_output_fps(self):
        config = {"segments": [{"video_path": "/a.mp4"}], "output_fps": -1}
        valid, errors = validate_concat_config(config)
        assert valid is False
        assert any("output_fps" in e for e in errors)

    def test_zero_output_params_valid(self):
        # 0值表示未指定，是合法的
        config = {
            "segments": [{"video_path": "/a.mp4"}],
            "output_width": 0,
            "output_height": 0,
            "output_fps": 0,
        }
        valid, errors = validate_concat_config(config)
        assert valid is True


# ── validate_video_path ─────────────────────────────────────────────────────


class TestValidateVideoPath:
    def test_empty_path(self):
        valid, err = validate_video_path("", "/work")
        assert valid is False
        assert "不能为空" in err

    def test_relative_path_valid(self):
        valid, err = validate_video_path("video.mp4", "/work")
        assert valid is True
        assert err == ""

    def test_relative_path_with_subdir(self):
        valid, err = validate_video_path("sub/video.mp4", "/work")
        assert valid is True

    def test_path_traversal_rejected(self):
        valid, err = validate_video_path("../secret.mp4", "/work")
        assert valid is False
        assert ".." in err

    def test_nested_path_traversal_rejected(self):
        valid, err = validate_video_path("sub/../../secret.mp4", "/work")
        assert valid is False

    def test_absolute_path_inside_workdir(self):
        valid, err = validate_video_path("/work/sub/video.mp4", "/work")
        assert valid is True

    def test_absolute_path_outside_workdir(self):
        valid, err = validate_video_path("/etc/passwd", "/work")
        assert valid is False
        assert "工作目录内" in err

    def test_path_object_input(self):
        valid, err = validate_video_path(Path("video.mp4"), Path("/work"))
        assert valid is True


# ── estimate_total_duration ─────────────────────────────────────────────────


class TestEstimateTotalDuration:
    def test_single_segment(self):
        assert estimate_total_duration([{"duration": 10.5}]) == 10.5

    def test_multiple_segments(self):
        segs = [
            {"duration": 10},
            {"duration": 20.5},
            {"duration": 5.5},
        ]
        assert estimate_total_duration(segs) == 36.0

    def test_empty_list(self):
        assert estimate_total_duration([]) == 0.0

    def test_missing_duration_field(self):
        segs = [{"path": "a.mp4"}, {"duration": 10}]
        assert estimate_total_duration(segs) == 10.0

    def test_invalid_duration_skipped(self):
        segs = [
            {"duration": 10},
            {"duration": "abc"},
            {"duration": 20},
        ]
        assert estimate_total_duration(segs) == 30.0

    def test_string_duration(self):
        segs = [{"duration": "15.5"}]
        assert estimate_total_duration(segs) == 15.5

    def test_negative_duration(self):
        segs = [{"duration": -5}]
        assert estimate_total_duration(segs) == -5.0


# ── count_valid_segments ────────────────────────────────────────────────────


class TestCountValidSegments:
    def test_all_valid(self):
        segs = [
            {"video_path": "/a.mp4"},
            {"video_path": "/b.mp4"},
        ]
        assert count_valid_segments(segs) == 2

    def test_some_invalid(self):
        segs = [
            {"video_path": "/a.mp4"},
            {"video_path": ""},
            {"video_path": "/c.mp4"},
        ]
        assert count_valid_segments(segs) == 2

    def test_none_valid(self):
        segs = [
            {"video_path": ""},
            {"other_field": "x"},
        ]
        assert count_valid_segments(segs) == 0

    def test_empty_list(self):
        assert count_valid_segments([]) == 0
