"""multi_track_mixer_pure 单元测试."""

import math

from apps.worker.video_processing.multi_track_mixer_pure import (
    build_amix_filter,
    build_mix_filter_complex,
    build_track_filter_chain,
    calculate_amix_volume_compensation,
    calculate_effective_range,
    calculate_total_tracks,
    count_track_types,
    db_to_linear,
    estimate_mix_duration,
    filter_enabled_tracks,
    is_track_visible,
    linear_to_db,
    normalize_volume,
    sort_tracks_by_priority,
    validate_audio_track,
    validate_mix_config,
)

# ── calculate_effective_range ───────────────────────────────────────────────


class TestCalculateEffectiveRange:
    def test_simple_inside_target(self):
        start, need, trim = calculate_effective_range(
            track_start=2.0,
            track_duration=5.0,
            audio_duration=10.0,
            target_duration=20.0,
        )
        assert start == 2.0
        assert need == 5.0
        assert trim == 0.0

    def test_zero_duration_uses_full_audio(self):
        start, need, trim = calculate_effective_range(
            track_start=1.0,
            track_duration=0,
            audio_duration=8.0,
            target_duration=20.0,
        )
        assert start == 1.0
        assert need == 8.0
        assert trim == 0.0

    def test_negative_start_trims_beginning(self):
        start, need, trim = calculate_effective_range(
            track_start=-2.0,
            track_duration=0,
            audio_duration=10.0,
            target_duration=20.0,
        )
        assert start == 0.0
        assert need == 8.0  # 10 - 2
        assert trim == 2.0

    def test_starts_after_target_duration(self):
        start, need, trim = calculate_effective_range(
            track_start=25.0,
            track_duration=5.0,
            audio_duration=10.0,
            target_duration=20.0,
        )
        assert start == 0.0
        assert need == 0.0
        assert trim == 0.0

    def test_ends_before_zero(self):
        start, need, trim = calculate_effective_range(
            track_start=-10.0,
            track_duration=5.0,
            audio_duration=10.0,
            target_duration=20.0,
        )
        assert need == 0.0

    def test_truncated_at_end(self):
        start, need, trim = calculate_effective_range(
            track_start=15.0,
            track_duration=10.0,
            audio_duration=10.0,
            target_duration=20.0,
        )
        assert start == 15.0
        assert need == 5.0  # 截断到目标时长
        assert trim == 0.0

    def test_zero_audio_duration(self):
        start, need, trim = calculate_effective_range(
            track_start=0,
            track_duration=10,
            audio_duration=0,
            target_duration=20.0,
        )
        assert need == 0.0

    def test_negative_audio_duration(self):
        start, need, trim = calculate_effective_range(
            track_start=0,
            track_duration=10,
            audio_duration=-1,
            target_duration=20.0,
        )
        assert need == 0.0

    def test_track_longer_than_audio(self):
        start, need, trim = calculate_effective_range(
            track_start=0,
            track_duration=20,
            audio_duration=10,
            target_duration=30,
        )
        assert need == 10.0  # 受限于音频长度

    def test_trim_start_exceeds_audio(self):
        start, need, trim = calculate_effective_range(
            track_start=-15.0,
            track_duration=0,
            audio_duration=10.0,
            target_duration=20.0,
        )
        # 被截掉15秒，但音频只有10秒 → 全没了
        assert need == 0.0


# ── is_track_visible ────────────────────────────────────────────────────────


class TestIsTrackVisible:
    def test_visible_track(self):
        assert is_track_visible(2, 5, 10, 20) is True

    def test_invisible_after_target(self):
        assert is_track_visible(25, 5, 10, 20) is False

    def test_invisible_zero_audio(self):
        assert is_track_visible(0, 10, 0, 20) is False

    def test_invisible_all_trimmed(self):
        assert is_track_visible(-20, 10, 10, 20) is False


# ── build_track_filter_chain ────────────────────────────────────────────────


class TestBuildTrackFilterChain:
    def test_basic_chain_structure(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        parts = result.split(",")
        # 至少有: atrim, asetpts, atrim, asetpts
        assert any("atrim=" in p for p in parts)
        assert parts.count("asetpts=N/SR/TB") == 2

    def test_volume_filter_applied(self):
        result = build_track_filter_chain(
            volume=0.5,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "volume=0.500" in result

    def test_volume_one_omitted(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "volume=" not in result

    def test_volume_clamped(self):
        result = build_track_filter_chain(
            volume=3.0,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "volume=2.000" in result  # 钳制到2.0

    def test_volume_negative_clamped(self):
        result = build_track_filter_chain(
            volume=-1.0,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "volume=0.000" in result

    def test_fade_in_applied(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=1.0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "afade=t=in:st=0:d=1.000" in result

    def test_fade_in_longer_than_duration_skipped(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=10.0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "afade=t=in" not in result

    def test_fade_out_applied(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=1.0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "afade=t=out:st=4.000:d=1.000" in result

    def test_fade_out_longer_than_duration_skipped(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=10.0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "afade=t=out" not in result

    def test_delay_applied(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=0,
            effective_start=2.5,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "adelay=2500|2500" in result

    def test_zero_delay_skipped(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "adelay" not in result

    def test_final_truncation_exists(self):
        result = build_track_filter_chain(
            volume=1.0,
            fade_in=0,
            fade_out=0,
            effective_start=0,
            need_duration=5.0,
            trim_start=0,
            target_duration=10.0,
        )
        assert "atrim=0:10.000" in result  # 最终截断

    def test_full_chain_with_all_features(self):
        result = build_track_filter_chain(
            volume=0.8,
            fade_in=0.5,
            fade_out=1.0,
            effective_start=2.0,
            need_duration=6.0,
            trim_start=1.0,
            target_duration=10.0,
        )
        # 有atrim开头截断
        assert "atrim=1.000:7.000" in result
        # 有音量
        assert "volume=0.800" in result
        # 有淡入淡出
        assert "afade=t=in" in result
        assert "afade=t=out" in result
        # 有延迟
        assert "adelay=2000|2000" in result
        # 有最终截断
        assert "atrim=0:10.000" in result


# ── build_amix_filter ───────────────────────────────────────────────────────


class TestBuildAmixFilter:
    def test_zero_inputs(self):
        assert build_amix_filter(0) == ""

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

    def test_single_input(self):
        result = build_amix_filter(1)
        assert "amix=inputs=1:" in result
        assert "duration=longest" in result
        assert "dropout_transition=0" in result

    def test_multiple_inputs(self):
        result = build_amix_filter(5)
        assert "amix=inputs=5:" in result

    def test_shortest_mode(self):
        result = build_amix_filter(3, duration_mode="shortest")
        assert "duration=shortest" in result

    def test_first_mode(self):
        result = build_amix_filter(3, duration_mode="first")
        assert "duration=first" in result

    def test_invalid_mode_falls_back(self):
        result = build_amix_filter(3, duration_mode="invalid")
        assert "duration=longest" in result


# ── calculate_amix_volume_compensation ──────────────────────────────────────


class TestCalculateAmixVolumeCompensation:
    def test_zero_inputs(self):
        assert calculate_amix_volume_compensation(0) == 1.0

    def test_single_input(self):
        assert calculate_amix_volume_compensation(1) == 1.0

    def test_two_inputs(self):
        assert calculate_amix_volume_compensation(2) == 2.0

    def test_five_inputs(self):
        assert calculate_amix_volume_compensation(5) == 5.0

    def test_negative_inputs(self):
        assert calculate_amix_volume_compensation(-1) == 1.0


# ── build_mix_filter_complex ────────────────────────────────────────────────


class TestBuildMixFilterComplex:
    def test_no_tracks_no_main_empty(self):
        assert build_mix_filter_complex(0, has_main=False) == ""

    def test_main_only(self):
        result = build_mix_filter_complex(0, has_main=True)
        assert "[0:a]" in result
        assert "amix=inputs=1:" in result
        assert "[mixed]" in result
        # 单路无音量补偿
        assert "volume=" not in result

    def test_main_plus_tracks(self):
        result = build_mix_filter_complex(2, has_main=True)
        assert "[0:a][1:a][2:a]" in result
        assert "amix=inputs=3:" in result
        # 3路有音量补偿
        assert "volume=3.0" in result

    def test_tracks_only_no_main(self):
        result = build_mix_filter_complex(3, has_main=False)
        assert "[0:a][1:a][2:a]" in result
        assert "amix=inputs=3:" in result
        assert "volume=3.0" in result

    def test_duration_mode_passed(self):
        result = build_mix_filter_complex(2, has_main=True, duration_mode="shortest")
        assert "duration=shortest" in result

    def test_output_label(self):
        result = build_mix_filter_complex(2, has_main=True)
        assert result.endswith("[mixed]")


# ── normalize_volume ────────────────────────────────────────────────────────


class TestNormalizeVolume:
    def test_none_returns_one(self):
        assert normalize_volume(None) == 1.0

    def test_normal_value(self):
        assert normalize_volume(0.5) == 0.5

    def test_max_value(self):
        assert normalize_volume(2.0) == 2.0

    def test_above_max_clamped(self):
        assert normalize_volume(3.0) == 2.0

    def test_below_min_clamped(self):
        assert normalize_volume(-1.0) == 0.0

    def test_zero(self):
        assert normalize_volume(0) == 0.0

    def test_string_number(self):
        assert normalize_volume("0.5") == 0.5

    def test_invalid_string(self):
        assert normalize_volume("abc") == 1.0


# ── db_to_linear / linear_to_db ─────────────────────────────────────────────


class TestDbConversions:
    def test_zero_db_is_one(self):
        assert abs(db_to_linear(0) - 1.0) < 0.001

    def test_negative_db_less_than_one(self):
        assert db_to_linear(-20) < 1.0

    def test_positive_db_greater_than_one(self):
        assert db_to_linear(20) > 1.0

    def test_roundtrip_conversion(self):
        original = 0.5
        db = linear_to_db(original)
        back = db_to_linear(db)
        assert abs(back - original) < 0.001

    def test_20db_is_10x(self):
        # 20dB = 10倍
        assert abs(db_to_linear(20) - 10.0) < 0.001

    def test_linear_zero_is_neg_inf(self):
        assert math.isinf(linear_to_db(0))
        assert linear_to_db(0) < 0

    def test_linear_negative_is_neg_inf(self):
        assert math.isinf(linear_to_db(-1))


# ── sort_tracks_by_priority ─────────────────────────────────────────────────


class TestSortTracksByPriority:
    def test_sorted_ascending(self):
        tracks = [
            {"name": "c", "priority": 3},
            {"name": "a", "priority": 1},
            {"name": "b", "priority": 2},
        ]
        result = sort_tracks_by_priority(tracks)
        assert [t["name"] for t in result] == ["a", "b", "c"]

    def test_default_priority_100(self):
        tracks = [
            {"name": "low", "priority": 50},
            {"name": "default"},  # 默认100
            {"name": "high", "priority": 150},
        ]
        result = sort_tracks_by_priority(tracks)
        assert result[0]["name"] == "low"
        assert result[1]["name"] == "default"
        assert result[2]["name"] == "high"

    def test_same_priority_stable(self):
        tracks = [
            {"name": "first", "priority": 5},
            {"name": "second", "priority": 5},
            {"name": "third", "priority": 5},
        ]
        result = sort_tracks_by_priority(tracks)
        assert [t["name"] for t in result] == ["first", "second", "third"]

    def test_empty_list(self):
        assert sort_tracks_by_priority([]) == []

    def test_original_not_modified(self):
        tracks = [{"priority": 3}, {"priority": 1}]
        original = list(tracks)
        sort_tracks_by_priority(tracks)
        assert tracks == original


# ── filter_enabled_tracks ───────────────────────────────────────────────────


class TestFilterEnabledTracks:
    def test_all_enabled(self):
        tracks = [{"name": "a", "enabled": True}, {"name": "b"}]
        result = filter_enabled_tracks(tracks)
        assert len(result) == 2

    def test_some_disabled(self):
        tracks = [
            {"name": "a", "enabled": True},
            {"name": "b", "enabled": False},
            {"name": "c", "enabled": "false"},
            {"name": "d", "enabled": 0},
        ]
        result = filter_enabled_tracks(tracks)
        assert len(result) == 1
        assert result[0]["name"] == "a"

    def test_all_disabled(self):
        tracks = [
            {"name": "a", "enabled": False},
            {"name": "b", "enabled": "false"},
        ]
        assert filter_enabled_tracks(tracks) == []

    def test_empty_list(self):
        assert filter_enabled_tracks([]) == []

    def test_string_true_enabled(self):
        tracks = [{"name": "a", "enabled": "true"}]
        result = filter_enabled_tracks(tracks)
        assert len(result) == 1


# ── count_track_types ───────────────────────────────────────────────────────


class TestCountTrackTypes:
    def test_multiple_types(self):
        tracks = [
            {"track_type": "bgm"},
            {"track_type": "voice"},
            {"track_type": "bgm"},
            {"track_type": "sfx"},
            {"track_type": "bgm"},
        ]
        result = count_track_types(tracks)
        assert result == {"bgm": 3, "voice": 1, "sfx": 1}

    def test_default_type(self):
        tracks = [{"name": "a"}, {"track_type": "bgm"}]
        result = count_track_types(tracks)
        assert result["unknown"] == 1
        assert result["bgm"] == 1

    def test_empty_list(self):
        assert count_track_types([]) == {}


# ── validate_audio_track ────────────────────────────────────────────────────


class TestValidateAudioTrack:
    def test_valid_with_asset_id(self):
        valid, errors = validate_audio_track({"asset_id": "asset_123"})
        assert valid is True
        assert errors == []

    def test_valid_with_audio_path(self):
        valid, errors = validate_audio_track({"audio_path": "/tmp/a.mp3"})
        assert valid is True
        assert errors == []

    def test_missing_source(self):
        valid, errors = validate_audio_track({})
        assert valid is False
        assert any("audio_path 或 asset_id" in e for e in errors)

    def test_negative_volume(self):
        valid, errors = validate_audio_track({"asset_id": "a", "volume": -1})
        assert valid is False
        assert any("volume" in e for e in errors)

    def test_volume_too_high(self):
        valid, errors = validate_audio_track({"asset_id": "a", "volume": 3.0})
        assert valid is False
        assert any("volume" in e for e in errors)

    def test_invalid_volume_string(self):
        valid, errors = validate_audio_track({"asset_id": "a", "volume": "abc"})
        assert valid is False
        assert any("volume" in e for e in errors)

    def test_negative_fade_in(self):
        valid, errors = validate_audio_track({"asset_id": "a", "fade_in": -1})
        assert valid is False
        assert any("fade_in" in e for e in errors)

    def test_negative_fade_out(self):
        valid, errors = validate_audio_track({"asset_id": "a", "fade_out": -1})
        assert valid is False
        assert any("fade_out" in e for e in errors)

    def test_invalid_fade_in_string(self):
        valid, errors = validate_audio_track({"asset_id": "a", "fade_in": "abc"})
        assert valid is False
        assert any("fade_in" in e for e in errors)

    def test_invalid_start_time(self):
        valid, errors = validate_audio_track({"asset_id": "a", "start_time": "abc"})
        assert valid is False
        assert any("start_time" in e for e in errors)

    def test_multiple_errors(self):
        valid, errors = validate_audio_track(
            {
                "volume": "abc",
                "fade_in": "def",
                "start_time": "ghi",
            }
        )
        assert valid is False
        assert len(errors) >= 4  # source + volume + fade_in + start_time


# ── validate_mix_config ─────────────────────────────────────────────────────


class TestValidateMixConfig:
    def test_valid_config(self):
        config = {
            "tracks": [{"audio_path": "/a.mp3", "volume": 1.0}],
            "target_duration": 10.0,
        }
        valid, errors = validate_mix_config(config)
        assert valid is True
        assert errors == []

    def test_no_tracks(self):
        valid, errors = validate_mix_config({})
        assert valid is False
        assert any("至少需要一条轨道" in e for e in errors)

    def test_empty_tracks(self):
        valid, errors = validate_mix_config({"tracks": []})
        assert valid is False
        assert any("至少需要一条轨道" in e for e in errors)

    def test_invalid_track_errors_prefixed(self):
        config = {"tracks": [{"volume": "abc"}]}
        valid, errors = validate_mix_config(config)
        assert valid is False
        assert any(e.startswith("第1轨：") for e in errors)

    def test_multiple_invalid_tracks(self):
        config = {
            "tracks": [
                {"volume": "bad"},
                {"audio_path": "/a.mp3", "fade_in": "bad"},
            ]
        }
        valid, errors = validate_mix_config(config)
        assert valid is False
        track1_errors = [e for e in errors if e.startswith("第1轨：")]
        track2_errors = [e for e in errors if e.startswith("第2轨：")]
        assert len(track1_errors) >= 1
        assert len(track2_errors) >= 1

    def test_negative_target_duration(self):
        config = {
            "tracks": [{"audio_path": "/a.mp3"}],
            "target_duration": -5,
        }
        valid, errors = validate_mix_config(config)
        assert valid is False
        assert any("target_duration" in e for e in errors)

    def test_invalid_target_duration(self):
        config = {
            "tracks": [{"audio_path": "/a.mp3"}],
            "target_duration": "abc",
        }
        valid, errors = validate_mix_config(config)
        assert valid is False
        assert any("target_duration" in e for e in errors)


# ── calculate_total_tracks ──────────────────────────────────────────────────


class TestCalculateTotalTracks:
    def test_with_main_default(self):
        config = {"tracks": [{}, {}, {}]}
        assert calculate_total_tracks(config) == 4  # 3 + 1主

    def test_with_main_explicit(self):
        config = {"tracks": [{}, {}], "has_main_audio": True}
        assert calculate_total_tracks(config) == 3

    def test_without_main(self):
        config = {"tracks": [{}, {}], "has_main_audio": False}
        assert calculate_total_tracks(config) == 2

    def test_no_tracks_with_main(self):
        config = {"tracks": [], "has_main_audio": True}
        assert calculate_total_tracks(config) == 1

    def test_empty_config(self):
        assert calculate_total_tracks({}) == 1


# ── estimate_mix_duration ───────────────────────────────────────────────────


class TestEstimateMixDuration:
    def test_single_track(self):
        tracks = [{"start_time": 0, "duration": 10}]
        assert estimate_mix_duration(tracks) == 10.0

    def test_multiple_tracks_takes_max(self):
        tracks = [
            {"start_time": 0, "duration": 10},
            {"start_time": 5, "duration": 20},  # end=25
            {"start_time": 2, "duration": 8},  # end=10
        ]
        assert estimate_mix_duration(tracks) == 25.0

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

    def test_zero_duration_tracks_ignored(self):
        tracks = [
            {"start_time": 0, "duration": 0},
            {"start_time": 5, "duration": 0},
        ]
        assert estimate_mix_duration(tracks) == 0.0

    def test_invalid_values_skipped(self):
        tracks = [
            {"start_time": "abc", "duration": 10},
            {"start_time": 0, "duration": "xyz"},
            {"start_time": 2, "duration": 5},
        ]
        assert estimate_mix_duration(tracks) == 7.0

    def test_negative_start_time(self):
        tracks = [{"start_time": -5, "duration": 10}]  # end=5
        assert estimate_mix_duration(tracks) == 5.0

    def test_string_numbers(self):
        tracks = [{"start_time": "2.5", "duration": "3.5"}]
        assert estimate_mix_duration(tracks) == 6.0
