""" Tests for engine.config module. """ import sys import tempfile from pathlib import Path from unittest.mock import patch import pytest from engine import config class TestArgValue: """Tests for _arg_value helper.""" def test_returns_value_when_flag_present(self): """Returns the value following the flag.""" with patch.object(sys, "argv", ["prog", "--font-file", "test.otf"]): result = config._arg_value("--font-file") assert result == "test.otf" def test_returns_none_when_flag_missing(self): """Returns None when flag is not present.""" with patch.object(sys, "argv", ["prog"]): result = config._arg_value("--font-file") assert result is None def test_returns_none_when_no_value(self): """Returns None when flag is last.""" with patch.object(sys, "argv", ["prog", "--font-file"]): result = config._arg_value("--font-file") assert result is None class TestArgInt: """Tests for _arg_int helper.""" def test_parses_valid_int(self): """Parses valid integer.""" with patch.object(sys, "argv", ["prog", "--font-index", "5"]): result = config._arg_int("--font-index", 0) assert result == 5 def test_returns_default_on_invalid(self): """Returns default on invalid input.""" with patch.object(sys, "argv", ["prog", "--font-index", "abc"]): result = config._arg_int("--font-index", 0) assert result == 0 def test_returns_default_when_missing(self): """Returns default when flag missing.""" with patch.object(sys, "argv", ["prog"]): result = config._arg_int("--font-index", 10) assert result == 10 class TestResolveFontPath: """Tests for _resolve_font_path helper.""" def test_returns_absolute_paths(self): """Absolute paths are returned as-is.""" result = config._resolve_font_path("/absolute/path.otf") assert result == "/absolute/path.otf" def test_resolves_relative_paths(self): """Relative paths are resolved to repo root.""" result = config._resolve_font_path("fonts/test.otf") assert str(config._REPO_ROOT) in result def test_expands_user_home(self): """Tilde paths are expanded.""" with patch("pathlib.Path.expanduser", return_value=Path("/home/user/fonts")): result = config._resolve_font_path("~/fonts/test.otf") assert isinstance(result, str) class TestListFontFiles: """Tests for _list_font_files helper.""" def test_returns_empty_for_missing_dir(self): """Returns empty list for missing directory.""" result = config._list_font_files("/nonexistent/directory") assert result == [] def test_filters_by_extension(self): """Only returns valid font extensions.""" with tempfile.TemporaryDirectory() as tmpdir: Path(tmpdir, "valid.otf").touch() Path(tmpdir, "valid.ttf").touch() Path(tmpdir, "invalid.txt").touch() Path(tmpdir, "image.png").touch() result = config._list_font_files(tmpdir) assert len(result) == 2 assert all(f.endswith((".otf", ".ttf")) for f in result) def test_sorts_alphabetically(self): """Results are sorted alphabetically.""" with tempfile.TemporaryDirectory() as tmpdir: Path(tmpdir, "zfont.otf").touch() Path(tmpdir, "afont.otf").touch() result = config._list_font_files(tmpdir) filenames = [Path(f).name for f in result] assert filenames == ["afont.otf", "zfont.otf"] class TestDefaults: """Tests for default configuration values.""" def test_headline_limit(self): """HEADLINE_LIMIT has sensible default.""" assert config.HEADLINE_LIMIT > 0 def test_feed_timeout(self): """FEED_TIMEOUT has sensible default.""" assert config.FEED_TIMEOUT > 0 def test_font_extensions(self): """Font extensions are defined.""" assert ".otf" in config._FONT_EXTENSIONS assert ".ttf" in config._FONT_EXTENSIONS assert ".ttc" in config._FONT_EXTENSIONS class TestGlyphs: """Tests for glyph constants.""" def test_glitch_glyphs_defined(self): """GLITCH glyphs are defined.""" assert len(config.GLITCH) > 0 def test_kata_glyphs_defined(self): """KATA glyphs are defined.""" assert len(config.KATA) > 0 class TestSetFontSelection: """Tests for set_font_selection function.""" def test_updates_font_path(self): """Updates FONT_PATH globally.""" original = config.FONT_PATH config.set_font_selection(font_path="/new/path.otf") assert config.FONT_PATH == "/new/path.otf" config.FONT_PATH = original def test_updates_font_index(self): """Updates FONT_INDEX globally.""" original = config.FONT_INDEX config.set_font_selection(font_index=5) assert config.FONT_INDEX == 5 config.FONT_INDEX = original def test_handles_none_values(self): """Handles None values gracefully.""" original_path = config.FONT_PATH original_index = config.FONT_INDEX config.set_font_selection(font_path=None, font_index=None) assert original_path == config.FONT_PATH assert original_index == config.FONT_INDEX class TestActiveTheme: """Tests for ACTIVE_THEME global and set_active_theme function.""" def test_active_theme_initially_none(self): """ACTIVE_THEME should be None at module start.""" # Reset to None to test initial state original = config.ACTIVE_THEME config.ACTIVE_THEME = None try: assert config.ACTIVE_THEME is None finally: config.ACTIVE_THEME = original def test_set_active_theme_green(self): """Setting green theme works correctly.""" config.set_active_theme("green") assert config.ACTIVE_THEME is not None assert config.ACTIVE_THEME.name == "green" assert len(config.ACTIVE_THEME.main_gradient) == 12 assert len(config.ACTIVE_THEME.message_gradient) == 12 def test_set_active_theme_default(self): """Default theme is green when not specified.""" config.set_active_theme() assert config.ACTIVE_THEME is not None assert config.ACTIVE_THEME.name == "green" def test_set_active_theme_invalid(self): """Invalid theme_id raises KeyError.""" with pytest.raises(KeyError): config.set_active_theme("nonexistent") def test_set_active_theme_all_themes(self): """Verify orange and purple themes work.""" # Test orange config.set_active_theme("orange") assert config.ACTIVE_THEME.name == "orange" # Test purple config.set_active_theme("purple") assert config.ACTIVE_THEME.name == "purple" def test_set_active_theme_idempotent(self): """Calling set_active_theme multiple times works.""" config.set_active_theme("green") first_theme = config.ACTIVE_THEME config.set_active_theme("green") second_theme = config.ACTIVE_THEME assert first_theme.name == second_theme.name assert first_theme.name == "green" class TestConfigDataclass: """Tests for Config dataclass.""" def test_config_has_required_fields(self): """Config has all required fields.""" c = config.Config() assert hasattr(c, "headline_limit") assert hasattr(c, "feed_timeout") assert hasattr(c, "mic_threshold_db") assert hasattr(c, "mode") assert hasattr(c, "firehose") assert hasattr(c, "ntfy_topic") assert hasattr(c, "ntfy_reconnect_delay") assert hasattr(c, "message_display_secs") assert hasattr(c, "font_dir") assert hasattr(c, "font_path") assert hasattr(c, "font_index") assert hasattr(c, "font_picker") assert hasattr(c, "font_sz") assert hasattr(c, "render_h") assert hasattr(c, "ssaa") assert hasattr(c, "scroll_dur") assert hasattr(c, "frame_dt") assert hasattr(c, "firehose_h") assert hasattr(c, "grad_speed") assert hasattr(c, "glitch_glyphs") assert hasattr(c, "kata_glyphs") assert hasattr(c, "script_fonts") def test_config_defaults(self): """Config has sensible defaults.""" c = config.Config() assert c.headline_limit == 1000 assert c.feed_timeout == 10 assert c.mic_threshold_db == 50 assert c.mode == "news" assert c.firehose is False assert c.ntfy_reconnect_delay == 5 assert c.message_display_secs == 30 def test_config_is_immutable(self): """Config is frozen (immutable).""" c = config.Config() with pytest.raises(AttributeError): c.headline_limit = 500 # type: ignore def test_config_custom_values(self): """Config accepts custom values.""" c = config.Config( headline_limit=500, mode="poetry", firehose=True, ntfy_topic="https://ntfy.sh/test", ) assert c.headline_limit == 500 assert c.mode == "poetry" assert c.firehose is True assert c.ntfy_topic == "https://ntfy.sh/test" class TestConfigFromArgs: """Tests for Config.from_args method.""" def test_from_args_defaults(self): """from_args creates config with defaults from empty argv.""" c = config.Config.from_args(["prog"]) assert c.mode == "news" assert c.firehose is False assert c.font_picker is True def test_from_args_poetry_mode(self): """from_args detects --poetry flag.""" c = config.Config.from_args(["prog", "--poetry"]) assert c.mode == "poetry" def test_from_args_poetry_short_flag(self): """from_args detects -p short flag.""" c = config.Config.from_args(["prog", "-p"]) assert c.mode == "poetry" def test_from_args_firehose(self): """from_args detects --firehose flag.""" c = config.Config.from_args(["prog", "--firehose"]) assert c.firehose is True def test_from_args_no_font_picker(self): """from_args detects --no-font-picker flag.""" c = config.Config.from_args(["prog", "--no-font-picker"]) assert c.font_picker is False def test_from_args_font_index(self): """from_args parses --font-index.""" c = config.Config.from_args(["prog", "--font-index", "3"]) assert c.font_index == 3 class TestGetSetConfig: """Tests for get_config and set_config functions.""" def test_get_config_returns_config(self): """get_config returns a Config instance.""" c = config.get_config() assert isinstance(c, config.Config) def test_set_config_allows_injection(self): """set_config allows injecting a custom config.""" custom = config.Config(mode="poetry", headline_limit=100) config.set_config(custom) assert config.get_config().mode == "poetry" assert config.get_config().headline_limit == 100 def test_set_config_then_get_config(self): """set_config followed by get_config returns the set config.""" original = config.get_config() test_config = config.Config(headline_limit=42) config.set_config(test_config) result = config.get_config() assert result.headline_limit == 42 config.set_config(original) class TestPlatformFontPaths: """Tests for platform font path detection.""" def test_get_platform_font_paths_returns_dict(self): """_get_platform_font_paths returns a dictionary.""" fonts = config._get_platform_font_paths() assert isinstance(fonts, dict) def test_platform_font_paths_common_languages(self): """Common language font mappings exist.""" fonts = config._get_platform_font_paths() common = {"ja", "zh-cn", "ko", "ru", "ar", "hi"} found = set(fonts.keys()) & common assert len(found) > 0