"""Tests for lutris.services.gog_cloud_hooks

Tests the game lifecycle integration hooks for GOG cloud sync.
"""

import os
import sys
import unittest
from unittest.mock import MagicMock, patch

import lutris.services.gog_cloud_hooks as _mod
from lutris.services.gog_cloud import (
    CloudSaveLocation,
    SyncAction,
    SyncResult,
)
from lutris.services.gog_cloud_hooks import (
    _get_game_runner_info,
    _get_gog_service,
    _resolve_save_locations,
    sync_after_quit,
    sync_before_launch,
)


class TestGetGogService(unittest.TestCase):
    """Test the _get_gog_service helper."""

    def test_returns_authenticated_service(self):
        mock_service = MagicMock()
        mock_service.is_authenticated.return_value = True

        mock_gog_mod = MagicMock(GOGService=MagicMock(return_value=mock_service))
        with patch.dict(sys.modules, {"lutris.services.gog": mock_gog_mod}):
            result = _get_gog_service()
        self.assertEqual(result, mock_service)

    def test_returns_none_when_not_authenticated(self):
        mock_service = MagicMock()
        mock_service.is_authenticated.return_value = False

        mock_gog_mod = MagicMock(GOGService=MagicMock(return_value=mock_service))
        with patch.dict(sys.modules, {"lutris.services.gog": mock_gog_mod}):
            result = _get_gog_service()
        self.assertIsNone(result)

    def test_returns_none_on_import_error(self):
        with patch.dict(sys.modules, {"lutris.services.gog": None}):
            result = _get_gog_service()
        self.assertIsNone(result)


class TestGetGameRunnerInfo(unittest.TestCase):
    """Test _get_game_runner_info helper."""

    def test_native_linux_game(self):
        game = MagicMock()
        game.runner_name = "linux"
        game.directory = "/opt/games/mygame"

        info = _get_game_runner_info(game)
        self.assertTrue(info["is_native"])
        self.assertEqual(info["platform"], "linux")
        self.assertIsNone(info["wine_prefix"])
        self.assertEqual(info["install_path"], "/opt/games/mygame")

    def test_wine_game(self):
        game = MagicMock()
        game.runner_name = "wine"
        game.directory = "/opt/games/mygame"
        game.runner.prefix_path = "/home/user/.wine"

        with patch.dict(os.environ, {"USER": "testuser"}):
            info = _get_game_runner_info(game)
        self.assertFalse(info["is_native"])
        self.assertEqual(info["platform"], "windows")
        self.assertEqual(info["wine_prefix"], "/home/user/.wine")
        self.assertEqual(info["wine_user"], "testuser")

    def test_no_runner(self):
        game = MagicMock()
        game.runner_name = ""
        game.runner = None
        game.directory = ""

        info = _get_game_runner_info(game)
        self.assertFalse(info["is_native"])
        self.assertIsNone(info["wine_prefix"])


class TestSyncBeforeLaunch(unittest.TestCase):
    """Test sync_before_launch function."""

    def test_skips_non_gog_game(self):
        game = MagicMock()
        game.service = "steam"
        game.appid = "12345"

        results = sync_before_launch(game)
        self.assertEqual(results, [])

    def test_skips_no_appid(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = None

        results = sync_before_launch(game)
        self.assertEqual(results, [])

    def test_skips_when_no_service(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"

        with patch.object(_mod, "_get_gog_service", return_value=None):
            results = sync_before_launch(game)
        self.assertEqual(results, [])

    def test_skips_when_no_save_locations(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"

        mock_service = MagicMock()
        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=[]),
        ):
            results = sync_before_launch(game)
        self.assertEqual(results, [])

    def test_syncs_download_direction(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"

        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.DOWNLOAD, downloaded=["a.sav"])

        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_before_launch(game)

        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.DOWNLOAD)
        mock_sync.sync_saves.assert_called_once_with(
            "12345",
            "/tmp/saves",
            "saves",
            "windows",
            preferred_action="download",
            progress_callback=None,
        )


class TestSyncAfterQuit(unittest.TestCase):
    """Test sync_after_quit function."""

    def test_skips_non_gog_game(self):
        game = MagicMock()
        game.service = "egs"
        game.appid = "12345"

        results = sync_after_quit(game)
        self.assertEqual(results, [])

    def test_syncs_upload_direction(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"

        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.UPLOAD, uploaded=["save.dat"])

        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_after_quit(game)

        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.UPLOAD)
        mock_sync.sync_saves.assert_called_once_with(
            "12345",
            "/tmp/saves",
            "saves",
            "windows",
            preferred_action="upload",
            progress_callback=None,
        )

    def test_handles_multiple_save_locations(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"

        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.NONE)

        save_locations = [
            {"name": "__default", "save_path": "/tmp/default", "platform": "windows"},
            {"name": "saves", "save_path": "/tmp/saves", "platform": "windows"},
        ]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_after_quit(game)

        self.assertEqual(len(results), 2)
        self.assertEqual(mock_sync.sync_saves.call_count, 2)

    def test_handles_sync_error(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"

        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(error="Network error")

        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_after_quit(game)

        self.assertEqual(len(results), 1)
        self.assertIsNotNone(results[0].error)


class TestResolveSaveLocations(unittest.TestCase):
    """Test _resolve_save_locations helper."""

    def test_returns_empty_on_token_error(self):
        game = MagicMock()
        game.appid = "12345"
        game.runner_name = "wine"
        game.directory = "/opt/games/mygame"

        service = MagicMock()
        service.load_token.side_effect = Exception("no token")

        result = _resolve_save_locations(game, service)
        self.assertEqual(result, [])

    def test_returns_empty_on_credentials_error(self):
        game = MagicMock()
        game.appid = "12345"
        game.runner_name = "wine"
        game.directory = "/opt/games/mygame"

        service = MagicMock()
        service.load_token.return_value = {"access_token": "tok", "refresh_token": "ref"}

        with patch.object(_mod, "get_game_client_credentials", side_effect=ValueError("no builds")):
            result = _resolve_save_locations(game, service)
        self.assertEqual(result, [])

    def test_resolves_save_locations(self):
        game = MagicMock()
        game.appid = "12345"
        game.runner_name = "linux"
        game.runner = None
        game.directory = "/opt/games/mygame"

        service = MagicMock()
        service.load_token.return_value = {"access_token": "tok", "refresh_token": "ref"}

        mock_locations = [
            CloudSaveLocation(name="saves", location="<?INSTALL?>/saves"),
        ]

        with (
            patch.object(_mod, "get_game_client_credentials", return_value=("cid", "csecret")),
            patch.object(_mod, "get_game_scoped_token", return_value={"access_token": "gtoken", "user_id": "u1"}),
            patch.object(_mod, "get_cloud_save_locations", return_value=mock_locations),
        ):
            result = _resolve_save_locations(game, service)

        self.assertEqual(len(result), 1)
        self.assertEqual(result[0]["name"], "saves")
        self.assertEqual(result[0]["save_path"], "/opt/games/mygame/saves")

    def test_returns_empty_on_scoped_token_error(self):
        game = MagicMock()
        game.appid = "12345"
        game.runner_name = "linux"
        game.directory = "/opt/games/mygame"

        service = MagicMock()
        service.load_token.return_value = {"access_token": "tok", "refresh_token": "ref"}

        with (
            patch.object(_mod, "get_game_client_credentials", return_value=("cid", "csecret")),
            patch.object(_mod, "get_game_scoped_token", side_effect=ValueError("bad")),
        ):
            result = _resolve_save_locations(game, service)
        self.assertEqual(result, [])

    def test_returns_empty_on_no_access_token(self):
        game = MagicMock()
        game.appid = "12345"
        game.runner_name = "linux"
        game.directory = "/opt/games/mygame"

        service = MagicMock()
        service.load_token.return_value = {"access_token": "tok", "refresh_token": "ref"}

        with (
            patch.object(_mod, "get_game_client_credentials", return_value=("cid", "csecret")),
            patch.object(_mod, "get_game_scoped_token", return_value={"access_token": "", "user_id": "u1"}),
        ):
            result = _resolve_save_locations(game, service)
        self.assertEqual(result, [])

    def test_returns_empty_on_no_locations(self):
        game = MagicMock()
        game.appid = "12345"
        game.runner_name = "linux"
        game.runner = None
        game.directory = "/opt/games/mygame"

        service = MagicMock()
        service.load_token.return_value = {"access_token": "tok", "refresh_token": "ref"}

        with (
            patch.object(_mod, "get_game_client_credentials", return_value=("cid", "csecret")),
            patch.object(_mod, "get_game_scoped_token", return_value={"access_token": "gtoken", "user_id": "u1"}),
            patch.object(_mod, "get_cloud_save_locations", return_value=[]),
        ):
            result = _resolve_save_locations(game, service)
        self.assertEqual(result, [])


class TestSyncBeforeLaunchBranches(unittest.TestCase):
    """Test sync_before_launch error/conflict branches."""

    def _make_game(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"
        return game

    def test_sync_error_in_result(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(error="Connection refused")
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_before_launch(game)
        self.assertEqual(len(results), 1)
        self.assertIsNotNone(results[0].error)

    def test_conflict_dialog_shown_user_downloads(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        # First call returns conflict, second (after dialog) returns download
        mock_sync.sync_saves.side_effect = [
            SyncResult(action=SyncAction.CONFLICT),
            SyncResult(action=SyncAction.DOWNLOAD, downloaded=["a.sav"]),
        ]
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
            patch.object(_mod, "_show_conflict_dialog", return_value="download"),
        ):
            results = sync_before_launch(game)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.DOWNLOAD)
        self.assertEqual(mock_sync.sync_saves.call_count, 2)

    def test_conflict_dialog_user_skips(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.CONFLICT)
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
            patch.object(_mod, "_show_conflict_dialog", return_value=None),
        ):
            results = sync_before_launch(game)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.CONFLICT)
        # Only one call since user skipped
        self.assertEqual(mock_sync.sync_saves.call_count, 1)

    def test_none_result(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.NONE)
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_before_launch(game)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.NONE)


class TestSyncAfterQuitBranches(unittest.TestCase):
    """Test sync_after_quit additional branches."""

    def _make_game(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"
        game.skip_cloud_sync = False
        return game

    def test_skips_when_no_service(self):
        game = self._make_game()
        with patch.object(_mod, "_get_gog_service", return_value=None):
            results = sync_after_quit(game)
        self.assertEqual(results, [])

    def test_skips_when_no_save_locations(self):
        game = self._make_game()
        mock_service = MagicMock()
        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=[]),
        ):
            results = sync_after_quit(game)
        self.assertEqual(results, [])

    def test_conflict_dialog_shown_user_uploads(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.side_effect = [
            SyncResult(action=SyncAction.CONFLICT),
            SyncResult(action=SyncAction.UPLOAD, uploaded=["a.sav"]),
        ]
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
            patch.object(_mod, "_show_conflict_dialog", return_value="upload"),
        ):
            results = sync_after_quit(game)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.UPLOAD)
        self.assertEqual(mock_sync.sync_saves.call_count, 2)

    def test_conflict_dialog_user_skips(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.CONFLICT)
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
            patch.object(_mod, "_show_conflict_dialog", return_value=None),
        ):
            results = sync_after_quit(game)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.CONFLICT)
        self.assertEqual(mock_sync.sync_saves.call_count, 1)

    def test_none_result(self):
        game = self._make_game()
        mock_service = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.NONE)
        save_locations = [{"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}]

        with (
            patch.object(_mod, "_get_gog_service", return_value=mock_service),
            patch.object(_mod, "_resolve_save_locations", return_value=save_locations),
            patch.object(_mod, "GOGCloudSync", return_value=mock_sync),
        ):
            results = sync_after_quit(game)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].action, SyncAction.NONE)


class TestShowConflictDialog(unittest.TestCase):
    """Test _show_conflict_dialog helper.

    The function now marshals the GTK dialog to the main thread via
    ``GLib.idle_add`` and waits on a ``threading.Event``.  In tests we
    mock ``GLib.idle_add`` to invoke the callback immediately so the
    event gets set without a real GTK main loop.
    """

    _show_conflict_dialog = staticmethod(_mod._show_conflict_dialog)

    @staticmethod
    def _immediate_idle_add(callback, *args):
        """Simulate GLib.idle_add by calling the callback right away."""
        callback(*args)
        return 0  # dummy source id

    def test_returns_none_on_import_error(self):
        """When GTK isn't available, should return None gracefully."""
        mock_glib = MagicMock()
        mock_glib.idle_add = self._immediate_idle_add

        with patch.dict(
            sys.modules,
            {
                "lutris.gui.dialogs.cloud_sync": None,
                "gi.repository": MagicMock(GLib=mock_glib),
            },
        ):
            result = self._show_conflict_dialog("Game", "saves")
        self.assertIsNone(result)

    def test_returns_dialog_action(self):
        """When dialog can be constructed, returns its action."""
        mock_dialog = MagicMock()
        mock_dialog.action = "download"
        mock_dialog_cls = MagicMock(return_value=mock_dialog)
        mock_module = MagicMock(CloudSyncConflictDialog=mock_dialog_cls)

        mock_glib = MagicMock()
        mock_glib.idle_add = self._immediate_idle_add

        with patch.dict(
            sys.modules,
            {
                "lutris.gui.dialogs.cloud_sync": mock_module,
                "gi.repository": MagicMock(GLib=mock_glib),
            },
        ):
            result = self._show_conflict_dialog("Game", "saves")
        self.assertEqual(result, "download")
        mock_dialog_cls.assert_called_once_with("Game", "saves")


class TestSyncLocation(unittest.TestCase):
    """Test _sync_location helper directly."""

    _sync_location = staticmethod(_mod._sync_location)

    def _make_game(self):
        game = MagicMock()
        game.service = "gog"
        game.appid = "12345"
        game.name = "Test Game"
        return game

    def test_no_conflict_returns_directly(self):
        game = self._make_game()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.DOWNLOAD, downloaded=["x"])
        loc = {"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}

        result = self._sync_location(mock_sync, game, loc, "download", "test")
        self.assertEqual(result.action, SyncAction.DOWNLOAD)
        mock_sync.sync_saves.assert_called_once()

    def test_conflict_retries_with_user_choice(self):
        game = self._make_game()
        mock_sync = MagicMock()
        mock_sync.sync_saves.side_effect = [
            SyncResult(action=SyncAction.CONFLICT),
            SyncResult(action=SyncAction.UPLOAD, uploaded=["y"]),
        ]
        loc = {"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}

        with patch.object(_mod, "_show_conflict_dialog", return_value="upload"):
            result = self._sync_location(mock_sync, game, loc, "download", "test")
        self.assertEqual(result.action, SyncAction.UPLOAD)
        self.assertEqual(mock_sync.sync_saves.call_count, 2)

    def test_conflict_skip_returns_conflict(self):
        game = self._make_game()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(action=SyncAction.CONFLICT)
        loc = {"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}

        with patch.object(_mod, "_show_conflict_dialog", return_value=None):
            result = self._sync_location(mock_sync, game, loc, "download", "test")
        self.assertEqual(result.action, SyncAction.CONFLICT)
        mock_sync.sync_saves.assert_called_once()

    def test_error_result_logged(self):
        game = self._make_game()
        mock_sync = MagicMock()
        mock_sync.sync_saves.return_value = SyncResult(error="failed")
        loc = {"name": "saves", "save_path": "/tmp/saves", "platform": "windows"}

        result = self._sync_location(mock_sync, game, loc, "upload", "test")
        self.assertIsNotNone(result.error)


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