"""audio_track_config 模块单测 — 纯逻辑，无外部依赖."""

from __future__ import annotations

import pytest

from packages.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_default_volumes_keys(self):
        assert set(DEFAULT_VOLUMES.keys()) == {
            TRACK_TYPE_MAIN,
            TRACK_TYPE_BGM,
            TRACK_TYPE_VOICEOVER,
            TRACK_TYPE_SFX,
            TRACK_TYPE_AMBIENT,
        }

    def test_default_volumes_values(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_max_audio_tracks(self):
        assert MAX_AUDIO_TRACKS == 8

    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
        assert ".mp4" not in ALLOWED_AUDIO_EXTENSIONS
        assert ".txt" not in ALLOWED_AUDIO_EXTENSIONS


# ── AudioTrack 默认值 ────────────────────────────────────────────────────────


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


# ── AudioTrack.from_dict ─────────────────────────────────────────────────────


class TestAudioTrackFromDict:
    def test_full_fields(self):
        track = AudioTrack.from_dict(
            {
                "track_id": "t1",
                "track_type": "bgm",
                "audio_path": "/tmp/a.mp3",
                "volume": 0.5,
                "fade_in": 1.5,
                "fade_out": 2.0,
                "start_time": 3.0,
                "duration": 10.0,
                "enabled": True,
            }
        )
        assert track.track_id == "t1"
        assert track.track_type == "bgm"
        assert track.audio_path == "/tmp/a.mp3"
        assert track.volume == 0.5
        assert track.fade_in == 1.5
        assert track.fade_out == 2.0
        assert track.start_time == 3.0
        assert track.duration == 10.0
        assert track.enabled is True

    def test_empty_dict_defaults(self):
        track = AudioTrack.from_dict({})
        assert track.track_type == TRACK_TYPE_SFX
        assert track.volume == DEFAULT_VOLUMES[TRACK_TYPE_SFX]
        assert track.fade_in == 0.0
        assert track.enabled is True

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

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

    def test_invalid_volume_fallback_to_default(self):
        track = AudioTrack.from_dict({"track_type": "bgm", "volume": "invalid"})
        assert track.volume == DEFAULT_VOLUMES[TRACK_TYPE_BGM]

    def test_negative_fade_clamped_to_zero(self):
        track = AudioTrack.from_dict({"fade_in": -1.0, "fade_out": -2.0})
        assert track.fade_in == 0.0
        assert track.fade_out == 0.0

    def test_invalid_fade_fallback(self):
        track = AudioTrack.from_dict({"fade_in": "bad", "fade_out": "bad"})
        assert track.fade_in == 0.0
        assert track.fade_out == 0.0

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

    def test_invalid_start_time_fallback(self):
        track = AudioTrack.from_dict({"start_time": "bad"})
        assert track.start_time == 0.0

    def test_negative_duration_clamped(self):
        track = AudioTrack.from_dict({"duration": -3.0})
        assert track.duration == 0.0

    def test_invalid_duration_fallback(self):
        track = AudioTrack.from_dict({"duration": "bad"})
        assert track.duration == 0.0

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

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

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

    def test_string_numeric_values(self):
        track = AudioTrack.from_dict(
            {
                "volume": "0.8",
                "fade_in": "1.0",
                "start_time": "2.5",
            }
        )
        assert track.volume == 0.8
        assert track.fade_in == 1.0
        assert track.start_time == 2.5


# ── AudioTrack.validate ──────────────────────────────────────────────────────


class TestAudioTrackValidate:
    def test_valid_track(self):
        track = AudioTrack(audio_path="/tmp/a.mp3")
        ok, err = track.validate()
        assert ok is True
        assert err == ""

    def test_empty_audio_path_invalid(self):
        track = AudioTrack(audio_path="")
        ok, err = track.validate()
        assert ok is False
        assert "audio_path" in err

    def test_volume_below_zero_invalid(self):
        # from_dict 会 clamp，但直接构造可以测试
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "volume", -0.1)
        ok, err = track.validate()
        assert ok is False
        assert "volume" in err

    def test_volume_above_two_invalid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "volume", 2.1)
        ok, err = track.validate()
        assert ok is False
        assert "volume" in err

    def test_volume_boundary_zero_valid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "volume", 0.0)
        ok, _ = track.validate()
        assert ok is True

    def test_volume_boundary_two_valid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "volume", 2.0)
        ok, _ = track.validate()
        assert ok is True

    def test_negative_fade_in_invalid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "fade_in", -1.0)
        ok, err = track.validate()
        assert ok is False
        assert "fade_in" in err

    def test_negative_fade_out_invalid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "fade_out", -1.0)
        ok, err = track.validate()
        assert ok is False
        assert "fade_out" in err

    def test_negative_start_time_invalid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "start_time", -0.5)
        ok, err = track.validate()
        assert ok is False
        assert "start_time" in err

    def test_negative_duration_invalid(self):
        track = AudioTrack(audio_path="a.mp3")
        object.__setattr__(track, "duration", -1.0)
        ok, err = track.validate()
        assert ok is False
        assert "duration" in err


# ── AudioTrack.is_effective ──────────────────────────────────────────────────


class TestAudioTrackIsEffective:
    def test_enabled_with_path(self):
        track = AudioTrack(audio_path="/tmp/a.mp3", enabled=True)
        assert track.is_effective is True

    def test_disabled_with_path(self):
        track = AudioTrack(audio_path="/tmp/a.mp3", enabled=False)
        assert track.is_effective is False

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

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


# ── MultiTrackMixConfig.from_config_dict ─────────────────────────────────────


class TestMultiTrackMixConfigFromDict:
    def test_none_config_empty(self):
        cfg = MultiTrackMixConfig.from_config_dict(None)
        assert cfg.tracks == []
        assert cfg.master_volume == 1.0
        assert cfg.normalize is True
        assert cfg.max_output_volume == 1.5

    def test_empty_dict_defaults(self):
        cfg = MultiTrackMixConfig.from_config_dict({})
        assert cfg.tracks == []
        assert cfg.master_volume == 1.0
        assert cfg.normalize is True

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

    def test_multiple_tracks(self):
        cfg = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"audio_path": "main.wav", "track_type": "main"},
                    {"audio_path": "bgm.mp3", "track_type": "bgm"},
                    {"audio_path": "sfx.wav", "track_type": "sfx"},
                ],
            }
        )
        assert len(cfg.tracks) == 3
        types = [t.track_type for t in cfg.tracks]
        assert "main" in types
        assert "bgm" in types
        assert "sfx" in types

    def test_skip_disabled_track(self):
        cfg = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"audio_path": "a.mp3", "enabled": True},
                    {"audio_path": "b.mp3", "enabled": False},
                ],
            }
        )
        assert len(cfg.tracks) == 1
        assert cfg.tracks[0].audio_path == "a.mp3"

    def test_skip_missing_audio_path(self):
        cfg = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"audio_path": "a.mp3"},
                    {"track_type": "bgm"},  # 无audio_path
                ],
            }
        )
        assert len(cfg.tracks) == 1

    def test_invalid_track_skipped(self):
        cfg = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [
                    {"audio_path": "a.mp3"},
                    "not_a_dict",
                    {"audio_path": 123, "volume": "bad"},  # 类型不对
                ],
            }
        )
        # 第二个不是dict跳过，第三个audio_path会被转成字符串"123"
        # 但 track_type 非dict的话在 isinstance(t, dict) 判断就被跳过
        assert len(cfg.tracks) >= 1

    def test_master_volume_clamped(self):
        cfg = MultiTrackMixConfig.from_config_dict(
            {
                "tracks": [],
                "master_volume": 3.0,
            }
        )
        assert cfg.master_volume == 2.0

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

    def test_invalid_master_volume_fallback(self):
        cfg = MultiTrackMixConfig.from_config_dict(
            {
                "master_volume": "invalid",
            }
        )
        assert cfg.master_volume == 1.0

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

    def test_custom_max_output_volume(self):
        cfg = MultiTrackMixConfig.from_config_dict({"max_output_volume": 2.0})
        assert cfg.max_output_volume == 2.0

    def test_invalid_max_output_volume_fallback(self):
        cfg = MultiTrackMixConfig.from_config_dict({"max_output_volume": "bad"})
        assert cfg.max_output_volume == 1.5

    def test_not_dict_config(self):
        cfg = MultiTrackMixConfig.from_config_dict("not a dict")
        assert cfg.tracks == []
        assert cfg.master_volume == 1.0


# ── MultiTrackMixConfig 属性 ─────────────────────────────────────────────────


class TestMultiTrackMixConfigProperties:
    def test_has_effect_empty(self):
        cfg = MultiTrackMixConfig()
        assert cfg.has_effect is False

    def test_has_effect_with_tracks(self):
        cfg = MultiTrackMixConfig(
            tracks=[
                AudioTrack(audio_path="a.mp3", enabled=True),
            ]
        )
        assert cfg.has_effect is True

    def test_has_effect_all_disabled(self):
        cfg = MultiTrackMixConfig(
            tracks=[
                AudioTrack(audio_path="a.mp3", enabled=False),
            ]
        )
        assert cfg.has_effect is False

    def test_effective_track_count(self):
        cfg = MultiTrackMixConfig(
            tracks=[
                AudioTrack(audio_path="a.mp3", enabled=True),
                AudioTrack(audio_path="b.mp3", enabled=False),
                AudioTrack(audio_path="c.mp3", enabled=True),
                AudioTrack(audio_path="", enabled=True),
            ]
        )
        assert cfg.effective_track_count == 2

    def test_main_tracks(self):
        cfg = MultiTrackMixConfig(
            tracks=[
                AudioTrack(audio_path="m1.mp3", track_type=TRACK_TYPE_MAIN),
                AudioTrack(audio_path="b1.mp3", track_type=TRACK_TYPE_BGM),
                AudioTrack(audio_path="m2.mp3", track_type=TRACK_TYPE_MAIN, enabled=False),
            ]
        )
        mains = cfg.main_tracks
        assert len(mains) == 1
        assert mains[0].audio_path == "m1.mp3"

    def test_bgm_tracks(self):
        cfg = MultiTrackMixConfig(
            tracks=[
                AudioTrack(audio_path="b1.mp3", track_type=TRACK_TYPE_BGM),
                AudioTrack(audio_path="b2.mp3", track_type=TRACK_TYPE_BGM),
                AudioTrack(audio_path="v1.mp3", track_type=TRACK_TYPE_VOICEOVER),
            ]
        )
        assert len(cfg.bgm_tracks) == 2


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


class TestUtils:
    @pytest.mark.parametrize(
        "name,expected",
        [
            ("song.mp3", True),
            ("audio.WAV", True),
            ("track.m4a", True),
            ("video.mp4", False),
            ("text.txt", False),
            ("", False),
            ("/path/to/music.flac", True),
            ("sound.OGG", True),
        ],
    )
    def test_is_valid_audio_extension(self, name, expected):
        assert is_valid_audio_extension(name) is expected

    def test_clamp_volume_within_range(self):
        assert clamp_volume(1.0) == 1.0
        assert clamp_volume(0.0) == 0.0
        assert clamp_volume(2.0) == 2.0

    def test_clamp_volume_below_min(self):
        assert clamp_volume(-0.5) == 0.0

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

    def test_clamp_volume_custom_range(self):
        assert clamp_volume(0.5, 0.2, 0.8) == 0.5
        assert clamp_volume(0.1, 0.2, 0.8) == 0.2
        assert clamp_volume(1.0, 0.2, 0.8) == 0.8
