"""Tests for #1256: random asset selection and random start_time in preview generation.""" from unittest.mock import MagicMock import pytest from packages.domain.edit_plan_clip import EditPlanClip from packages.domain.plan_generator_utils import ( _calc_random_start_time, distribute_assets, ) class TestAssignAssetWithStartTime: """Test assign_asset() with optional start_time parameter.""" def test_assign_asset_without_start_time(self): """Backward compatible: assign_asset without start_time keeps default 0.0""" clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0) clip.assign_asset("asset1") assert clip.asset_id == "asset1" assert clip.start_time == 0.0 def test_assign_asset_with_valid_start_time(self): """assign_asset with valid start_time sets it correctly""" clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0) clip.assign_asset("asset1", start_time=3.5) assert clip.asset_id == "asset1" assert clip.start_time == 3.5 def test_assign_asset_with_zero_start_time(self): """assign_asset with start_time=0.0 sets it to 0.0""" clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0, start_time=5.0) clip.assign_asset("asset1", start_time=0.0) assert clip.start_time == 0.0 def test_assign_asset_with_none_start_time(self): """assign_asset with start_time=None keeps existing start_time""" clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0, start_time=2.0) clip.assign_asset("asset1", start_time=None) assert clip.start_time == 2.0 # unchanged def test_assign_asset_with_negative_start_time(self): """assign_asset with negative start_time is ignored""" clip = EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0, start_time=2.0) clip.assign_asset("asset1", start_time=-1.0) assert clip.start_time == 2.0 # unchanged, negative ignored class TestCalcRandomStartTime: """Test _calc_random_start_time helper function.""" def test_returns_none_when_no_durations(self): """Returns None when asset_durations is None""" result = _calc_random_start_time("asset1", 5.0, None) assert result is None def test_returns_none_when_asset_not_in_durations(self): """Returns None when asset_id not in durations dict""" result = _calc_random_start_time("asset1", 5.0, {"other": 30.0}) assert result is None def test_returns_zero_when_duration_too_short(self): """Returns 0.0 when asset duration <= clip duration""" result = _calc_random_start_time("asset1", 10.0, {"asset1": 5.0}) assert result == 0.0 def test_returns_valid_random_start_time(self): """Returns start_time within valid range""" import random random.seed(42) result = _calc_random_start_time("asset1", 5.0, {"asset1": 30.0}) # max_start = 30.0 - 5.0 = 25.0 assert 0.0 <= result <= 25.0 def test_returns_zero_when_duration_zero(self): """Returns None when asset duration is 0""" result = _calc_random_start_time("asset1", 5.0, {"asset1": 0.0}) assert result is None class TestDistributeAssetsRandom: """Test distribute_assets() with random_selection parameter.""" def test_one_take_without_random(self): """ONE_TAKE without random: assets assigned in order""" clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=i, duration=5.0) for i in range(3)] assets = ["a1", "a2", "a3", "a4"] distribute_assets(clips, assets, "one_take", random_selection=False) assert clips[0].asset_id == "a1" assert clips[1].asset_id == "a2" assert clips[2].asset_id == "a3" def test_one_take_with_random(self): """ONE_TAKE with random: assets assigned in random order""" clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=i, duration=5.0) for i in range(3)] assets = ["a1", "a2", "a3", "a4"] # Run multiple times to verify randomness results = set() for _ in range(10): distribute_assets(clips, assets, "one_take", random_selection=True) results.add(tuple(c.asset_id for c in clips)) # Should have multiple different orderings assert len(results) > 1, "Random selection should produce different orderings" def test_with_asset_durations_sets_start_time(self): """distribute_assets with asset_durations sets random start_time""" clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0)] durations = {"a1": 30.0} distribute_assets(clips, ["a1"], "one_take", asset_durations=durations) assert clips[0].asset_id == "a1" # start_time should be set (0 <= start_time <= 25.0) assert 0.0 <= clips[0].start_time <= 25.0 def test_pip_mode_with_random(self): """PIP mode with random selection works correctly""" clips = [ EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="overlay", order=1, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="overlay", order=2, duration=5.0), ] assets = ["a1", "a2", "a3"] distribute_assets(clips, assets, "pip", random_selection=True) # All clips should have assets assigned assert clips[0].asset_id assert clips[1].asset_id assert clips[2].asset_id def test_voice_pip_mode_with_random(self): """VOICE_PIP mode with random selection works correctly""" clips = [ EditPlanClip.create(plan_id="p1", clip_type="background", order=0, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="corner_voice", order=1, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="b_roll", order=2, duration=5.0), ] assets = ["a1", "a2", "a3"] durations = {"a1": 30.0, "a2": 25.0, "a3": 20.0} distribute_assets(clips, assets, "voice_pip", random_selection=True, asset_durations=durations) # All clips should have assets and start_times for clip in clips: assert clip.asset_id assert 0.0 <= clip.start_time <= 25.0 # max_start = duration - clip_duration def test_does_not_modify_original_list(self): """random_selection should not modify the original asset_ids list""" clips = [EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0)] assets = ["a1", "a2", "a3"] original = list(assets) distribute_assets(clips, assets, "one_take", random_selection=True) assert assets == original, "Original asset_ids list should not be modified" class TestDistributeAssetsBackwardCompatible: """Ensure backward compatibility - existing calls without new params still work.""" def test_distribute_assets_default_params(self): """distribute_assets works with just required params""" clips = [ EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="main", order=1, duration=5.0), ] distribute_assets(clips, ["a1", "a2"], "one_take") assert clips[0].asset_id == "a1" assert clips[1].asset_id == "a2" assert clips[0].start_time == 0.0 # default def test_all_modes_work_without_new_params(self): """All editing modes work without random_selection/asset_durations""" # one_take / voice_over: use main clips for mode in ["one_take", "voice_over"]: clips = [ EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0), ] distribute_assets(clips, ["a1"], mode) assert clips[0].asset_id == "a1" # pip: main + overlay clips = [ EditPlanClip.create(plan_id="p1", clip_type="main", order=0, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="overlay", order=1, duration=5.0), ] distribute_assets(clips, ["a1"], "pip") assert clips[0].asset_id == "a1" # first asset goes to main # voice_pip: background + corner_voice + b_roll clips = [ EditPlanClip.create(plan_id="p1", clip_type="background", order=0, duration=5.0), EditPlanClip.create(plan_id="p1", clip_type="corner_voice", order=1, duration=5.0), ] distribute_assets(clips, ["a1"], "voice_pip") assert clips[0].asset_id == "a1" # first asset goes to background