"""audio_track_config 多轨道音频配置单测."""

import pytest
from domain.audio_track_config import (
    ALLOWED_AUDIO_EXTENSIONS,
    DEFAULT_VOLUMES,
    MAX_AUDIO_TRACKS,
    TRACK_TYPE_AMBIENT,
    TRACK_TYPE_BGM,
    TRACK_TYPE_MAIN,
    TRACK_TYPE_SFX,
    TRACK_TYPE_VOICEOVER,
    AudioTrack,
    MultiTrackMixConfig,
    clamp_volume,
    is_valid_audio_extension,
)

# ── 常量测试 ──────────────────────────────────────────────────────────────────


class TestConstants:
    """模块常量"""

    def test_track_type_constants(self):
        assert TRACK_TYPE_MAIN == "main"
        assert TRACK_TYPE_BGM == "bgm"
        assert TRACK_TYPE_VOICEOVER == "voiceover"
        assert TRACK_TYPE_SFX == "sfx"
        assert TRACK_TYPE_AMBIENT == "ambient"

    def test_max_tracks(self):
        assert MAX_AUDIO_TRACKS == 8

    def test_default_volumes(self):
        assert DEFAULT_VOLUMES[TRACK_TYPE_MAIN] == 1.0
        assert DEFAULT_VOLUMES[TRACK_TYPE_BGM] == 0.3
        assert DEFAULT_VOLUMES[TRACK_TYPE_VOICEOVER] == 1.0
        assert DEFAULT_VOLUMES[TRACK_TYPE_SFX] == 0.7
        assert DEFAULT_VOLUMES[TRACK_TYPE_AMBIENT] == 0.2

    def test_allowed_extensions(self):
        assert ".mp3" in ALLOWED_AUDIO_EXTENSIONS
        assert ".wav" in ALLOWED_AUDIO_EXTENSIONS
        assert ".aac" in ALLOWED_AUDIO_EXTENSIONS
        assert ".ogg" in ALLOWED_AUDIO_EXTENSIONS
        assert ".flac" in ALLOWED_AUDIO_EXTENSIONS
        assert ".m4a" in ALLOWED_AUDIO_EXTENSIONS
        assert ".wma" in ALLOWED_AUDIO_EXTENSIONS


# ── AudioTrack ───────────────────────────────────────────────────────────────


class TestAudioTrackDefaults:
    """AudioTrack 默认值"""

    def test_default_values(self):
        t = AudioTrack()
        assert t.track_id == ""
        assert t.track_type == TRACK_TYPE_SFX
        assert t.audio_path == ""
        assert t.volume == 1.0
        assert t.fade_in == 0.0
        assert t.fade_out == 0.0
        assert t.start_time == 0.0
        assert t.duration == 0.0
        assert t.enabled is True

    def test_custom_track(self):
        t = AudioTrack(
            track_id="bgm_001",
            track_type=TRACK_TYPE_BGM,
            audio_path="/music/bgm.mp3",
            volume=0.5,
            fade_in=1.5,
            fade_out=2.0,
            start_time=3.0,
            duration=30.0,
            enabled=False,
        )
        assert t.track_id == "bgm_001"
        assert t.track_type == TRACK_TYPE_BGM
        assert t.audio_path == "/music/bgm.mp3"
        assert t.volume == 0.5
        assert t.fade_in == 1.5
        assert t.start_time == 3.0
        assert t.duration == 30.0
        assert t.enabled is False


class TestAudioTrackFromDict:
    """AudioTrack.from_dict"""

    def test_empty_dict(self):
        t = AudioTrack.from_dict({})
        assert t.track_type == TRACK_TYPE_SFX
        assert t.audio_path == ""
        assert t.volume == DEFAULT_VOLUMES[TRACK_TYPE_SFX]
        assert t.enabled is True

    def test_full_dict(self):
        t = AudioTrack.from_dict(
            {
                "track_id": "t1",
                "track_type": "bgm",
                "audio_path": "/a.mp3",
                "volume": 0.8,
                "fade_in": 1.0,
                "fade_out": 2.0,
                "start_time": 5.0,
                "duration": 60.0,
                "enabled": True,
            }
        )
        assert t.track_id == "t1"
        assert t.track_type == "bgm"
        assert t.volume == 0.8
        assert t.fade_in == 1.0
        assert t.duration == 60.0

    def test_volume_clamped_to_zero(self):
        t = AudioTrack.from_dict({"volume": -0.5})
        assert t.volume == 0.0

    def test_volume_clamped_to_two(self):
        t = AudioTrack.from_dict({"volume": 3.0})
        assert t.volume == 2.0

    def test_invalid_volume_falls_back_to_default(self):
        t = AudioTrack.from_dict({"track_type": "bgm", "volume": "abc"})
        assert t.volume == DEFAULT_VOLUMES[TRACK_TYPE_BGM]

    def test_invalid_fade_in_falls_back(self):
        t = AudioTrack.from_dict({"fade_in": "bad"})
        assert t.fade_in == 0.0

    def test_negative_fade_in_clamped(self):
        t = AudioTrack.from_dict({"fade_in": -1.0})
        assert t.fade_in == 0.0

    def test_invalid_fade_out_falls_back(self):
        t = AudioTrack.from_dict({"fade_out": None})
        assert t.fade_out == 0.0

    def test_negative_start_time_clamped(self):
        t = AudioTrack.from_dict({"start_time": -5.0})
        assert t.start_time == 0.0

    def test_invalid_duration_falls_back(self):
        t = AudioTrack.from_dict({"duration": "long"})
        assert t.duration == 0.0

    def test_bgm_default_volume(self):
        t = AudioTrack.from_dict({"track_type": "bgm"})
        assert t.volume == 0.3

    def test_main_default_volume(self):
        t = AudioTrack.from_dict({"track_type": "main"})
        assert t.volume == 1.0

    def test_voiceover_default_volume(self):
        t = AudioTrack.from_dict({"track_type": "voiceover"})
        assert t.volume == 1.0

    def test_ambient_default_volume(self):
        t = AudioTrack.from_dict({"track_type": "ambient"})
        assert t.volume == 0.2

    def test_unknown_type_default_volume(self):
        t = AudioTrack.from_dict({"track_type": "unknown_type"})
        assert t.volume == 1.0

    def test_enabled_false(self):
        t = AudioTrack.from_dict({"enabled": False})
        assert t.enabled is False


class TestAudioTrackValidate:
    """AudioTrack.validate"""

    def test_empty_path_invalid(self):
        t = AudioTrack(audio_path="")
        valid, msg = t.validate()
        assert valid is False
        assert "audio_path" in msg

    def test_valid_track(self):
        t = AudioTrack(audio_path="/a.mp3", volume=0.5)
        valid, msg = t.validate()
        assert valid is True
        assert msg == ""

    def test_volume_below_zero_invalid(self):
        t = AudioTrack(audio_path="/a.mp3", volume=-0.1)
        valid, msg = t.validate()
        assert valid is False
        assert "volume" in msg

    def test_volume_above_two_invalid(self):
        t = AudioTrack(audio_path="/a.mp3", volume=2.1)
        valid, msg = t.validate()
        assert valid is False
        assert "volume" in msg

    def test_volume_zero_valid(self):
        t = AudioTrack(audio_path="/a.mp3", volume=0.0)
        valid, _ = t.validate()
        assert valid is True

    def test_volume_two_valid(self):
        t = AudioTrack(audio_path="/a.mp3", volume=2.0)
        valid, _ = t.validate()
        assert valid is True

    def test_negative_fade_in_invalid(self):
        t = AudioTrack(audio_path="/a.mp3", fade_in=-1.0)
        valid, msg = t.validate()
        assert valid is False
        assert "fade_in" in msg

    def test_negative_fade_out_invalid(self):
        t = AudioTrack(audio_path="/a.mp3", fade_out=-1.0)
        valid, msg = t.validate()
        assert valid is False
        assert "fade_out" in msg

    def test_negative_start_time_invalid(self):
        t = AudioTrack(audio_path="/a.mp3", start_time=-0.5)
        valid, msg = t.validate()
        assert valid is False
        assert "start_time" in msg

    def test_negative_duration_invalid(self):
        t = AudioTrack(audio_path="/a.mp3", duration=-1.0)
        valid, msg = t.validate()
        assert valid is False
        assert "duration" in msg


class TestAudioTrackIsEffective:
    """AudioTrack.is_effective 属性"""

    def test_enabled_with_path_effective(self):
        t = AudioTrack(audio_path="/a.mp3", enabled=True)
        assert t.is_effective is True

    def test_disabled_not_effective(self):
        t = AudioTrack(audio_path="/a.mp3", enabled=False)
        assert t.is_effective is False

    def test_no_path_not_effective(self):
        t = AudioTrack(audio_path="", enabled=True)
        assert t.is_effective is False

    def test_disabled_no_path_not_effective(self):
        t = AudioTrack(audio_path="", enabled=False)
        assert t.is_effective is False


# ── MultiTrackMixConfig ──────────────────────────────────────────────────────


class TestMultiTrackMixConfigDefaults:
    """MultiTrackMixConfig 默认值"""

    def test_default_values(self):
        c = MultiTrackMixConfig()
        assert c.tracks == []
        assert c.master_volume == 1.0
        assert c.normalize is True
        assert c.max_output_volume == 1.5

    def test_custom_config(self):
        t1 = AudioTrack(track_id="t1", audio_path="/a.mp3")
        c = MultiTrackMixConfig(
            tracks=[t1],
            master_volume=0.8,
            normalize=False,
            max_output_volume=2.0,
        )
        assert len(c.tracks) == 1
        assert c.master_volume == 0.8
        assert c.normalize is False
        assert c.max_output_volume == 2.0


class TestMultiTrackFromConfigDict:
    """MultiTrackMixConfig.from_config_dict"""

    def test_none_returns_default(self):
        c = MultiTrackMixConfig.from_config_dict(None)
        assert len(c.tracks) == 0
        assert c.master_volume == 1.0

    def test_empty_dict_returns_default(self):
        c = MultiTrackMixConfig.from_config_dict({})
        assert len(c.tracks) == 0

    def test_non_dict_returns_default(self):
        c = MultiTrackMixConfig.from_config_dict("not a dict")
        assert len(c.tracks) == 0

    def test_single_track(self):
        c = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "t1", "track_type": "bgm", "audio_path": "/bgm.mp3", "volume": 0.5},
                ],
            }
        )
        assert len(c.tracks) == 1
        assert c.tracks[0].track_id == "t1"
        assert c.tracks[0].volume == 0.5

    def test_multiple_tracks(self):
        c = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "t1", "track_type": "main", "audio_path": "/main.wav"},
                    {"track_id": "t2", "track_type": "bgm", "audio_path": "/bgm.mp3"},
                    {"track_id": "t3", "track_type": "sfx", "audio_path": "/sfx.wav"},
                ],
            }
        )
        assert len(c.tracks) == 3
        assert c.tracks[0].track_type == "main"
        assert c.tracks[1].track_type == "bgm"
        assert c.tracks[2].track_type == "sfx"

    def test_skip_disabled_tracks(self):
        c = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "t1", "audio_path": "/a.mp3", "enabled": True},
                    {"track_id": "t2", "audio_path": "/b.mp3", "enabled": False},
                    {"track_id": "t3", "audio_path": "/c.mp3"},
                ],
            }
        )
        assert len(c.tracks) == 2
        ids = [t.track_id for t in c.tracks]
        assert "t1" in ids
        assert "t2" not in ids
        assert "t3" in ids

    def test_skip_no_path_tracks(self):
        c = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "t1", "audio_path": "/a.mp3"},
                    {"track_id": "t2", "audio_path": ""},
                    {"track_id": "t3"},
                ],
            }
        )
        assert len(c.tracks) == 1
        assert c.tracks[0].track_id == "t1"

    def test_skip_non_dict_tracks(self):
        c = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "t1", "audio_path": "/a.mp3"},
                    "not a dict",
                    123,
                    None,
                ],
            }
        )
        assert len(c.tracks) == 1

    def test_master_volume_clamped(self):
        c = MultiTrackMixConfig.from_config_dict({"master_volume": 3.0})
        assert c.master_volume == 2.0

    def test_master_volume_negative_clamped(self):
        c = MultiTrackMixConfig.from_config_dict({"master_volume": -1.0})
        assert c.master_volume == 0.0

    def test_invalid_master_volume_falls_back(self):
        c = MultiTrackMixConfig.from_config_dict({"master_volume": "high"})
        assert c.master_volume == 1.0

    def test_normalize_false(self):
        c = MultiTrackMixConfig.from_config_dict({"normalize": False})
        assert c.normalize is False

    def test_max_output_volume_custom(self):
        c = MultiTrackMixConfig.from_config_dict({"max_output_volume": 2.0})
        assert c.max_output_volume == 2.0

    def test_invalid_max_output_volume_falls_back(self):
        c = MultiTrackMixConfig.from_config_dict({"max_output_volume": "big"})
        assert c.max_output_volume == 1.5

    def test_tracks_not_list_ignored(self):
        c = MultiTrackMixConfig.from_config_dict({"tracks": "not a list"})
        assert len(c.tracks) == 0


class TestMultiTrackProperties:
    """MultiTrackMixConfig 属性方法"""

    def _make_config(self):
        return MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"track_id": "m1", "track_type": "main", "audio_path": "/m.wav"},
                    {"track_id": "b1", "track_type": "bgm", "audio_path": "/b1.mp3"},
                    {"track_id": "b2", "track_type": "bgm", "audio_path": "/b2.mp3", "enabled": False},
                    {"track_id": "s1", "track_type": "sfx", "audio_path": "/s.wav"},
                    {"track_id": "x", "track_type": "ambient", "audio_path": ""},
                ],
            }
        )

    def test_has_effect_true(self):
        c = self._make_config()
        assert c.has_effect is True

    def test_has_effect_false(self):
        c = MultiTrackMixConfig()
        assert c.has_effect is False

    def test_effective_track_count(self):
        c = self._make_config()
        # m1 + b1 + s1 = 3个有效（b2禁用，x无路径）
        assert c.effective_track_count == 3

    def test_main_tracks(self):
        c = self._make_config()
        mains = c.main_tracks
        assert len(mains) == 1
        assert mains[0].track_id == "m1"

    def test_bgm_tracks(self):
        c = self._make_config()
        bgms = c.bgm_tracks
        assert len(bgms) == 1  # 只有b1有效
        assert bgms[0].track_id == "b1"

    def test_empty_tracks(self):
        c = MultiTrackMixConfig()
        assert c.effective_track_count == 0
        assert c.main_tracks == []
        assert c.bgm_tracks == []


# ── 工具函数 ─────────────────────────────────────────────────────────────────


class TestIsValidAudioExtension:
    """is_valid_audio_extension 函数"""

    def test_mp3(self):
        assert is_valid_audio_extension("song.mp3") is True

    def test_wav(self):
        assert is_valid_audio_extension("sound.wav") is True

    def test_aac(self):
        assert is_valid_audio_extension("audio.aac") is True

    def test_ogg(self):
        assert is_valid_audio_extension("music.ogg") is True

    def test_flac(self):
        assert is_valid_audio_extension("lossless.flac") is True

    def test_m4a(self):
        assert is_valid_audio_extension("apple.m4a") is True

    def test_wma(self):
        assert is_valid_audio_extension("windows.wma") is True

    def test_uppercase_extension(self):
        assert is_valid_audio_extension("SONG.MP3") is True

    def test_mixed_case_extension(self):
        assert is_valid_audio_extension("song.Mp3") is True

    def test_mp4_not_valid(self):
        assert is_valid_audio_extension("video.mp4") is False

    def test_txt_not_valid(self):
        assert is_valid_audio_extension("notes.txt") is False

    def test_no_extension(self):
        assert is_valid_audio_extension("README") is False

    def test_full_path(self):
        assert is_valid_audio_extension("/home/user/music/song.mp3") is True


class TestClampVolume:
    """clamp_volume 函数"""

    def test_within_range(self):
        assert clamp_volume(0.5) == 0.5

    def test_exact_min(self):
        assert clamp_volume(0.0) == 0.0

    def test_exact_max(self):
        assert clamp_volume(2.0) == 2.0

    def test_below_min(self):
        assert clamp_volume(-1.0) == 0.0

    def test_above_max(self):
        assert clamp_volume(3.0) == 2.0

    def test_custom_bounds(self):
        assert clamp_volume(5.0, min_vol=1.0, max_vol=10.0) == 5.0

    def test_custom_below_min(self):
        assert clamp_volume(0.5, min_vol=1.0) == 1.0

    def test_custom_above_max(self):
        assert clamp_volume(15.0, max_vol=10.0) == 10.0
