neser 1.2.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
Documentation
"""Tests for the CLI entrypoint in main.py."""
import os
import sys
import unittest
from io import StringIO
from unittest.mock import MagicMock, patch

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

class TestMainCliArgParsing(unittest.TestCase):
    """Verify argument parsing without invoking real DB or network."""

    def _run(self, args):
        """Run main.main() with the given argv list and capture stdout."""
        with patch("sys.argv", ["main.py"] + args):
            with patch("main.MetadataDb") as MockDb, \
                 patch("main.TheGamesDbClient") as MockClient, \
                 patch("main.Syncer") as MockSyncer:
                mock_db_instance = MagicMock()
                mock_client_instance = MagicMock()
                mock_syncer_instance = MagicMock()
                MockDb.return_value.__enter__ = MagicMock(return_value=mock_db_instance)
                MockDb.return_value.__exit__ = MagicMock(return_value=False)
                MockClient.return_value = mock_client_instance
                MockSyncer.return_value = mock_syncer_instance
                mock_db_instance.get_game_counts.return_value = {}
                mock_client_instance.get_api_limit.return_value = {
                    "remaining_monthly_allowance": 900, "extra_allowance": 0
                }
                mock_db_instance.list_games.return_value = []
                mock_db_instance.get_game_images.return_value = []
                mock_db_instance.get_image_base_urls.return_value = {}
                captured = StringIO()
                import main as m
                m.main(output=captured)
                return mock_db_instance, mock_client_instance, mock_syncer_instance, captured.getvalue()

    def test_sync_nes_calls_syncer_sync(self):
        _, _, syncer, _ = self._run(["sync", "--platform", "nes", "--api-key", "testkey"])
        syncer.sync.assert_called_once()
        call_kwargs = syncer.sync.call_args[1]
        self.assertEqual(call_kwargs["platform_id"], 7)

    def test_sync_force_full_passes_flag(self):
        _, _, syncer, _ = self._run(["sync", "--platform", "nes", "--force-full", "--api-key", "testkey"])
        call_kwargs = syncer.sync.call_args[1]
        self.assertTrue(call_kwargs["force_full"])

    def test_sync_without_api_key_raises(self):
        with patch("sys.argv", ["main.py", "sync", "--platform", "nes"]), \
             patch.dict("os.environ", {}, clear=True):
            import main as m
            with self.assertRaises(SystemExit):
                m.main(output=StringIO())

    def test_sync_reads_api_key_from_env(self):
        with patch("sys.argv", ["main.py", "sync", "--platform", "nes"]), \
             patch.dict("os.environ", {"THEGAMESDB_API_KEY": "envkey"}), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient") as MockClient, \
             patch("main.Syncer") as MockSyncer:
            mock_db_instance = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db_instance)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            MockClient.return_value = MagicMock()
            MockSyncer.return_value = MagicMock()
            import main as m
            m.main(output=StringIO())
            MockClient.assert_called_once()
            init_kwargs = MockClient.call_args[1]
            self.assertEqual(init_kwargs["api_key"], "envkey")

    def test_sync_cli_api_key_overrides_env(self):
        with patch("sys.argv", ["main.py", "sync", "--platform", "nes", "--api-key", "clikey"]), \
             patch.dict("os.environ", {"THEGAMESDB_API_KEY": "envkey"}), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient") as MockClient, \
             patch("main.Syncer") as MockSyncer:
            mock_db_instance = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db_instance)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            MockClient.return_value = MagicMock()
            MockSyncer.return_value = MagicMock()
            import main as m
            m.main(output=StringIO())
            init_kwargs = MockClient.call_args[1]
            self.assertEqual(init_kwargs["api_key"], "clikey")

    def test_list_platform_calls_list_games(self):
        db, _, _, _ = self._run(["list", "--platform", "nes", "--api-key", "testkey"])
        db.list_games.assert_called_once()
        call_kwargs = db.list_games.call_args[1]
        self.assertEqual(call_kwargs["platform_id"], 7)

    def test_list_game_id_filters_by_id(self):
        db, _, _, _ = self._run(["list", "--game-id", "135", "--api-key", "testkey"])
        db.list_games.assert_called_once()
        call_kwargs = db.list_games.call_args[1]
        self.assertEqual(call_kwargs["game_id"], 135)

    def test_status_command_prints_game_counts(self):
        with patch("sys.argv", ["main.py", "status", "--api-key", "testkey"]), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient") as MockClient, \
             patch("main.Syncer"):
            mock_db_instance = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db_instance)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            mock_db_instance.get_game_counts.return_value = {7: 42}
            mock_db_instance.list_platforms.return_value = [
                {"id": 7, "name": "Nintendo Entertainment System (NES)", "alias": "nes"}
            ]
            MockClient.return_value.get_api_limit.return_value = {
                "remaining_monthly_allowance": 900, "extra_allowance": 5
            }
            captured = StringIO()
            import main as m
            m.main(output=captured)
            output = captured.getvalue()
            self.assertIn("42", output)

    def test_images_command_prints_urls(self):
        with patch("sys.argv", ["main.py", "images", "135", "--api-key", "testkey"]), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient"), \
             patch("main.Syncer"):
            mock_db_instance = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db_instance)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            mock_db_instance.get_game_images.return_value = [
                {"id": 718, "type": "boxart", "side": "back",
                 "filename": "boxart/back/135-2.jpg", "resolution": "1000x1435"}
            ]
            mock_db_instance.build_image_url.return_value = (
                "https://cdn.thegamesdb.net/images/original/boxart/back/135-2.jpg"
            )
            captured = StringIO()
            import main as m
            m.main(output=captured)
            output = captured.getvalue()
            self.assertIn("boxart/back/135-2.jpg", output)

    def test_sync_all_platforms_syncs_nes_gb_gbc_gba(self):
        _, _, syncer, _ = self._run(["sync", "--platform", "all", "--api-key", "testkey"])
        platform_ids = [c[1]["platform_id"] for c in syncer.sync.call_args_list]
        self.assertIn(7, platform_ids)   # NES
        self.assertIn(4, platform_ids)   # GB
        self.assertIn(41, platform_ids)  # GBC
        self.assertIn(5, platform_ids)   # GBA


class TestInfoCommand(unittest.TestCase):
    """Tests for the 'info' subcommand."""

    def _run_info(self, args, search_results=None):
        search_results = search_results or []
        with patch("sys.argv", ["main.py", "info"] + args), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient"), \
             patch("main.Syncer"):
            mock_db = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            mock_db.search_games.return_value = search_results
            mock_db.get_game_genres.return_value = []
            mock_db.get_game_developers.return_value = []
            mock_db.get_game_publishers.return_value = []
            mock_db.get_game_images.return_value = []
            mock_db.get_reference.return_value = None
            captured = StringIO()
            import main as m
            m.main(output=captured)
            return mock_db, captured.getvalue()

    def test_info_calls_search_games_with_name(self):
        db, _ = self._run_info(["castlevania"])
        db.search_games.assert_called_once()
        call_args = db.search_games.call_args
        args_list = list(call_args[0]) + list(call_args[1].values())
        self.assertIn("castlevania", args_list)

    def test_info_with_platform_passes_platform_id(self):
        db, _ = self._run_info(["castlevania", "--platform", "nes"])
        call_kwargs = db.search_games.call_args[1]
        self.assertEqual(call_kwargs.get("platform_id"), 7)

    def test_info_no_results_prints_not_found(self):
        _, output = self._run_info(["zeldaXXX"])
        self.assertIn("No games found", output)
        self.assertIn("zeldaXXX", output)

    def test_info_prints_game_title_and_id(self):
        game = {
            "id": 135, "game_title": "Castlevania", "platform_id": 7,
            "release_date": "1987-05-01", "rating": "E", "players": 1,
            "overview": "Fight Dracula.", "coop": "No", "youtube": "",
            "alternates": None, "last_updated": "2025-01-01",
        }
        _, output = self._run_info(["castlevania"], search_results=[game])
        self.assertIn("135", output)
        self.assertIn("Castlevania", output)

    def test_info_resolves_genre_names(self):
        game = {
            "id": 135, "game_title": "Castlevania", "platform_id": 7,
            "release_date": "", "rating": "", "players": None,
            "overview": "", "coop": "", "youtube": "",
            "alternates": None, "last_updated": "",
        }
        with patch("sys.argv", ["main.py", "info", "castlevania"]), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient"), \
             patch("main.Syncer"):
            mock_db = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            mock_db.search_games.return_value = [game]
            mock_db.get_game_genres.return_value = [15]
            mock_db.get_game_developers.return_value = []
            mock_db.get_game_publishers.return_value = []
            mock_db.get_game_images.return_value = []
            mock_db.get_reference.side_effect = lambda table, eid: (
                {"id": 15, "name": "Action"} if table == "genres" and eid == 15 else None
            )
            captured = StringIO()
            import main as m
            m.main(output=captured)
            self.assertIn("Action", captured.getvalue())

    def test_info_shows_image_count(self):
        game = {
            "id": 135, "game_title": "Castlevania", "platform_id": 7,
            "release_date": "", "rating": "", "players": None,
            "overview": "", "coop": "", "youtube": "",
            "alternates": None, "last_updated": "",
        }
        with patch("sys.argv", ["main.py", "info", "castlevania"]), \
             patch("main.MetadataDb") as MockDb, \
             patch("main.TheGamesDbClient"), \
             patch("main.Syncer"):
            mock_db = MagicMock()
            MockDb.return_value.__enter__ = MagicMock(return_value=mock_db)
            MockDb.return_value.__exit__ = MagicMock(return_value=False)
            mock_db.search_games.return_value = [game]
            mock_db.get_game_genres.return_value = []
            mock_db.get_game_developers.return_value = []
            mock_db.get_game_publishers.return_value = []
            mock_db.get_game_image_counts_by_type.return_value = {
                "boxart": 2, "screenshot": 3, "fanart": 1,
            }
            mock_db.get_reference.return_value = None
            captured = StringIO()
            import main as m
            m.main(output=captured)
            output = captured.getvalue()
            self.assertIn("boxart", output)
            self.assertIn("2", output)
            self.assertIn("screenshot", output)
            self.assertIn("3", output)
            self.assertIn("fanart", output)
            self.assertIn("1", output)


if __name__ == "__main__":
    unittest.main()