from __future__ import annotations import json import os from pathlib import Path import sys import tempfile import unittest from click.testing import CliRunner SRC_ROOT = Path(__file__).resolve().parents[2] / "src" if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) from ffx import cli # noqa: E402 class RenameCliTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() self.workspace = Path(self.tempdir.name) self.home_dir = self.workspace / "home" self.home_dir.mkdir() def tearDown(self): self.tempdir.cleanup() def write_source(self, filename: str, payload: bytes = b"episode") -> Path: source_path = self.workspace / filename source_path.write_bytes(payload) return source_path def write_config(self, data: dict) -> None: config_dir = self.home_dir / ".local" / "etc" config_dir.mkdir(parents=True, exist_ok=True) (config_dir / "ffx.json").write_text(json.dumps(data), encoding="utf-8") def invoke_rename(self, *args: str): runner = CliRunner() result = runner.invoke( cli.ffx, ["rename", *args], env={**os.environ, "HOME": str(self.home_dir)}, ) self.assertEqual(0, result.exit_code, result.output) return result def test_rename_moves_matching_file_in_place(self): source_path = self.write_source("demo_S02E03.mkv", b"season-episode") result = self.invoke_rename("--prefix", "dball", str(source_path)) target_path = self.workspace / "dball_s02e03.mkv" self.assertIn("demo_S02E03.mkv -> dball_s02e03.mkv", result.output) self.assertFalse(source_path.exists()) self.assertTrue(target_path.exists()) self.assertEqual(b"season-episode", target_path.read_bytes()) def test_rename_uses_default_season_and_suffix_for_episode_only_match(self): source_path = self.write_source("demo_E07.mp4", b"episode-only") result = self.invoke_rename( "--prefix", "dball", "--suffix", "bonus", str(source_path), ) target_path = self.workspace / "dball_s01e07_bonus.mp4" self.assertIn("demo_E07.mp4 -> dball_s01e07_bonus.mp4", result.output) self.assertFalse(source_path.exists()) self.assertTrue(target_path.exists()) self.assertEqual(b"episode-only", target_path.read_bytes()) def test_rename_cli_season_overrides_source_season(self): source_path = self.write_source("demo_s02e07.webm") result = self.invoke_rename( "--prefix", "dball", "--season", "5", str(source_path), ) target_path = self.workspace / "dball_s05e07.webm" self.assertIn("demo_s02e07.webm -> dball_s05e07.webm", result.output) self.assertFalse(source_path.exists()) self.assertTrue(target_path.exists()) def test_rename_dry_run_prints_mapping_without_moving(self): source_path = self.write_source("demo_E07.mkv") result = self.invoke_rename( "--dry-run", "--prefix", "dball", str(source_path), ) target_path = self.workspace / "dball_s01e07.mkv" self.assertIn("demo_E07.mkv -> dball_s01e07.mkv", result.output) self.assertTrue(source_path.exists()) self.assertFalse(target_path.exists()) def test_rename_uses_configured_indicator_digit_lengths(self): self.write_config( { "defaultIndicatorSeasonDigits": 3, "defaultIndicatorEpisodeDigits": 4, } ) source_path = self.write_source("demo_E07.mkv") result = self.invoke_rename("--prefix", "dball", str(source_path)) target_path = self.workspace / "dball_s001e0007.mkv" self.assertIn("demo_E07.mkv -> dball_s001e0007.mkv", result.output) self.assertFalse(source_path.exists()) self.assertTrue(target_path.exists()) def test_rename_skips_non_matching_filenames(self): source_path = self.write_source("demo_finale.mkv") result = self.invoke_rename("--prefix", "dball", str(source_path)) self.assertIn("No matching files found.", result.output) self.assertTrue(source_path.exists()) if __name__ == "__main__": unittest.main()