import sys
from types import ModuleType
from unittest.mock import MagicMock, call

import pytest


@pytest.fixture(autouse=True)
def import_player_with_real_xbmc_player(monkeypatch):
    import xbmc

    class FakeKodiPlayer:
        def __init__(self, *args, **kwargs):
            pass

    monkeypatch.setattr(xbmc, "Player", FakeKodiPlayer)
    sys.modules.pop("lib.player", None)
    yield
    sys.modules.pop("lib.player", None)


def _player_instance():
    from lib.player import JacktookPLayer

    player = object.__new__(JacktookPLayer)
    player.data = {
        "mode": "tv",
        "title": "Show",
        "ids": {"tmdb_id": "123"},
        "tv_data": {"season": 1, "episode": 1},
    }
    player.playnext = MagicMock()
    player.play = MagicMock()
    player.run = MagicMock()
    player.PLAYLIST = MagicMock()
    return player


def test_handle_next_dialog_action_ignores_playlist_advance_and_uses_handler(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.PLAYLIST.size.return_value = 2
    player.PLAYLIST.getposition.return_value = 0
    clear_property = MagicMock()
    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 0)
    monkeypatch.setattr("lib.player.clear_property", clear_property)
    monkeypatch.setattr(player, "_get_next_episode_data", lambda: None)

    JacktookPLayer._handle_next_dialog_action(player)

    player.playnext.assert_not_called()
    player.play.assert_not_called()
    clear_property.assert_called_once_with("jacktook_next_dialog_action")


def test_handle_next_dialog_action_still_watching_cancel_clears_and_stops(monkeypatch):
    import xbmcgui

    from lib.player import JacktookPLayer

    player = _player_instance()
    player.PLAYLIST.size.return_value = 2
    player.PLAYLIST.getposition.return_value = 0
    clear_property = MagicMock()
    set_property = MagicMock()
    dialog = MagicMock()
    dialog.yesno.return_value = False

    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 2)
    monkeypatch.setattr("lib.player.get_property", lambda key: "2")
    monkeypatch.setattr("lib.player.clear_property", clear_property)
    monkeypatch.setattr("lib.player.set_property", set_property)
    monkeypatch.setattr(xbmcgui, "Dialog", lambda: dialog)

    JacktookPLayer._handle_next_dialog_action(player)

    player.playnext.assert_not_called()
    set_property.assert_not_called()
    assert clear_property.call_args_list == [
        call("jacktook_consecutive_autoplays"),
        call("jacktook_next_dialog_action"),
    ]


def test_check_next_dialog_time_mode_triggers_at_time_left_threshold(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.total_time = 600
    player.current_time = 550
    player.watched_percentage = 91.7
    player.playback_started_properly = True
    player.next_dialog = True
    player.playing_next_time = 50
    execute_builtin = MagicMock()

    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: False)
    monkeypatch.setattr("lib.player.action_url_run", lambda **kwargs: "RunPlugin(test)")
    monkeypatch.setattr("lib.player.xbmc.executebuiltin", execute_builtin)

    JacktookPLayer.check_next_dialog(player)

    execute_builtin.assert_called_once_with("RunPlugin(test)")
    assert player.next_dialog is False


def test_check_next_dialog_passes_authoritative_next_tv_data(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.data["tv_data"] = {"season": 1, "episode": 3}
    player.total_time = 600
    player.current_time = 550
    player.watched_percentage = 91.7
    player.playback_started_properly = True
    player.next_dialog = True
    player.playing_next_time = 50
    next_tv_data = {"season": 1, "episode": 4, "name": "Immediate Next"}
    captured = {}

    monkeypatch.setattr(player, "_get_next_episode_data", MagicMock(return_value=next_tv_data))
    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: False)

    def fake_action_url_run(**kwargs):
        captured.update(kwargs)
        return "RunPlugin(test)"

    execute_builtin = MagicMock()
    monkeypatch.setattr("lib.player.action_url_run", fake_action_url_run)
    monkeypatch.setattr("lib.player.xbmc.executebuiltin", execute_builtin)

    JacktookPLayer.check_next_dialog(player)

    execute_builtin.assert_called_once_with("RunPlugin(test)")
    assert captured["name"] == "run_next_dialog"
    assert captured["item_info"]["next_tv_data"] == next_tv_data
    assert player.data["next_tv_data"] == next_tv_data
    player._get_next_episode_data.assert_called_once_with()


def test_check_next_dialog_percentage_mode_triggers_at_percentage_threshold(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.total_time = 600
    player.current_time = 570
    player.watched_percentage = 95.0
    player.playback_started_properly = True
    player.next_dialog = True
    player.playing_next_time = 10
    execute_builtin = MagicMock()

    def get_setting(key, default=None):
        return {"playnext_use_percentage": True, "playnext_percentage": 95}.get(key, default)

    monkeypatch.setattr("lib.player.get_setting", get_setting)
    monkeypatch.setattr("lib.player.action_url_run", lambda **kwargs: "RunPlugin(test)")
    monkeypatch.setattr("lib.player.xbmc.executebuiltin", execute_builtin)

    JacktookPLayer.check_next_dialog(player)

    execute_builtin.assert_called_once_with("RunPlugin(test)")
    assert player.next_dialog is False


def test_check_next_dialog_does_not_trigger_before_halfway_guard(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.total_time = 600
    player.current_time = 299
    player.watched_percentage = 99.0
    player.playback_started_properly = True
    player.next_dialog = True
    player.playing_next_time = 400
    execute_builtin = MagicMock()

    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: True)
    monkeypatch.setattr("lib.player.xbmc.executebuiltin", execute_builtin)

    JacktookPLayer.check_next_dialog(player)

    execute_builtin.assert_not_called()
    assert player.next_dialog is True


def test_play_window_background_tasks_ignores_missing_progress_bar(monkeypatch):
    from lib.gui.play_window import PlayWindow

    class TestPlayWindow(PlayWindow):
        def smart_play_action(self):
            self.smart_play_called = True

    window = object.__new__(TestPlayWindow)
    window.closed = False
    window.playing_file = "file.mkv"
    window.smart_play_called = False
    window.getControl = MagicMock(side_effect=RuntimeError("missing"))
    window.getTotalTime = MagicMock(side_effect=[3, 1, 1])
    window.getTime = MagicMock(return_value=0)
    window.getPlayingFile = MagicMock(return_value="file.mkv")
    window.setProperty = MagicMock()
    window.close = MagicMock()
    monkeypatch.setattr("lib.gui.play_window.xbmc.sleep", MagicMock())

    window.background_tasks()

    window.close.assert_called_once_with()
    assert window.smart_play_called is True


def test_build_next_episode_properties_prefers_next_tv_data():
    from lib.gui.play_next_window import build_next_episode_properties

    properties = build_next_episode_properties(
        {
            "title": "Current Title",
            "query": "Show Name",
            "poster": "poster.jpg",
            "fanart": "fanart.jpg",
            "clearlogo": "logo.png",
            "plot": "Episode plot",
            "tv_data": {"season": 1, "episode": 1},
            "next_tv_data": {"season": "2", "episode": "3", "name": "The Next One"},
        }
    )

    assert properties["next.title"] == "Show Name"
    assert properties["next.poster"] == "poster.jpg"
    assert properties["next.fanart"] == "fanart.jpg"
    assert properties["next.clearlogo"] == "logo.png"
    assert properties["next.season"] == "2"
    assert properties["next.episode"] == "3"
    assert properties["next.episode_label"] == "S02E03"
    assert properties["next.episode_name"] == "The Next One"
    assert properties["next.plot"] == "Episode plot"


def test_build_next_episode_properties_infers_next_episode_from_tv_data():
    from lib.gui.play_next_window import build_next_episode_properties

    properties = build_next_episode_properties(
        {
            "title": "Show Name",
            "tv_data": {"season": "1", "episode": "1"},
        }
    )

    assert properties["next.title"] == "Show Name"
    assert properties["next.season"] == "1"
    assert properties["next.episode"] == "2"
    assert properties["next.episode_label"] == "S01E02"
    assert properties["next.episode_name"] == "Episode 2"


def test_build_next_episode_properties_handles_invalid_episode_data():
    from lib.gui.play_next_window import build_next_episode_properties

    properties = build_next_episode_properties(
        {
            "title": "Show Name",
            "tv_data": {"season": "one", "episode": "two"},
        }
    )

    assert properties["next.title"] == "Show Name"
    assert properties["next.season"] == ""
    assert properties["next.episode"] == ""
    assert properties["next.episode_label"] == ""
    assert properties["next.episode_name"] == ""


def test_handle_next_dialog_action_reuses_authoritative_next_tv_data(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.data["tv_data"] = {"season": 1, "episode": 3}
    player.data["next_tv_data"] = {"season": 1, "episode": 4, "name": "Immediate Next"}
    player.PLAYLIST.size.return_value = 2
    player.PLAYLIST.getposition.return_value = 0
    recalculator = MagicMock(return_value={"season": 1, "episode": 8, "name": "Wrong Drift"})
    cache_handler = MagicMock(return_value=True)

    monkeypatch.setattr(player, "_get_next_episode_data", recalculator)
    monkeypatch.setattr(player, "_queue_from_autoscrape_cache", cache_handler)
    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 0)
    monkeypatch.setattr("lib.player.get_property", lambda key: "")

    JacktookPLayer._handle_next_dialog_action(player)

    player.playnext.assert_not_called()
    recalculator.assert_not_called()
    cache_handler.assert_called_once_with(
        {"season": 1, "episode": 4, "name": "Immediate Next"},
        {"tmdb_id": "123"},
    )


def test_handle_next_dialog_action_falls_back_when_authoritative_missing(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    fallback_next = {"season": 1, "episode": 2, "name": "Fallback Next"}
    recalculator = MagicMock(return_value=fallback_next)
    cache_handler = MagicMock(return_value=True)

    monkeypatch.setattr(player, "_get_next_episode_data", recalculator)
    monkeypatch.setattr(player, "_queue_from_autoscrape_cache", cache_handler)
    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 0)
    monkeypatch.setattr("lib.player.get_property", lambda key: "")

    JacktookPLayer._handle_next_dialog_action(player)

    recalculator.assert_called_once_with()
    cache_handler.assert_called_once_with(fallback_next, {"tmdb_id": "123"})


def test_handle_next_dialog_action_falls_back_when_authoritative_invalid(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    player.data["next_tv_data"] = {"season": "bad", "episode": 4, "name": "Invalid"}
    fallback_next = {"season": 1, "episode": 2, "name": "Fallback Next"}
    recalculator = MagicMock(return_value=fallback_next)
    cache_handler = MagicMock(return_value=True)

    monkeypatch.setattr(player, "_get_next_episode_data", recalculator)
    monkeypatch.setattr(player, "_queue_from_autoscrape_cache", cache_handler)
    monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 0)
    monkeypatch.setattr("lib.player.get_property", lambda key: "")

    JacktookPLayer._handle_next_dialog_action(player)

    recalculator.assert_called_once_with()
    cache_handler.assert_called_once_with(fallback_next, {"tmdb_id": "123"})


def test_drain_nextep_queue_source_select_cancel_does_not_force_container_update(monkeypatch):
    from lib.player import JacktookPLayer

    player = _player_instance()
    JacktookPLayer._nextep_queue.clear()
    JacktookPLayer._nextep_queue.append(
        {
            "data": {
                "mode": "tv",
                "ids": {"tmdb_id": "123"},
                "tv_data": {"season": 1, "episode": 4},
                "query": "Show",
                "media_type": "tv",
            },
            "results": [{"name": "Source 1"}],
        }
    )
    show_source_select = MagicMock(return_value=False)
    fake_search = ModuleType("lib.search")
    fake_search.show_source_select = show_source_select
    execute_builtin = MagicMock()
    close_busy_dialog = MagicMock()
    close_all_dialog = MagicMock()
    clear_property = MagicMock()

    monkeypatch.setitem(sys.modules, "lib.search", fake_search)
    monkeypatch.setattr("lib.utils.kodi.settings.auto_play_enabled", lambda: False)
    monkeypatch.setattr("lib.player.xbmc.executebuiltin", execute_builtin)
    monkeypatch.setattr("lib.player.close_busy_dialog", close_busy_dialog)
    monkeypatch.setattr("lib.player.close_all_dialog", close_all_dialog)
    monkeypatch.setattr("lib.player.clear_property", clear_property)

    try:
        assert JacktookPLayer._drain_nextep_queue(player) is True
    finally:
        JacktookPLayer._nextep_queue.clear()

    show_source_select.assert_called_once()
    assert not any(
        call_args.args
        and isinstance(call_args.args[0], str)
        and "Container.Update" in call_args.args[0]
        for call_args in execute_builtin.call_args_list
    )
    close_busy_dialog.assert_called_once_with()
    close_all_dialog.assert_called_once_with()
    clear_property.assert_called_once_with("jacktook_next_dialog_action")
    player.PLAYLIST.clear.assert_called_once_with()


def _make_play_next(monkeypatch, auto_timeout=10):
    """Create a PlayNext instance without calling __init__ for testing."""
    from lib.gui.play_next_window import PlayNext

    window = object.__new__(PlayNext)
    window.auto_timeout = auto_timeout
    window.closed = False
    window.playing_file = "file.mkv"
    window.player = MagicMock()
    window.action = None
    window.handle_action = MagicMock()
    window.setProperty = MagicMock()
    window.close = MagicMock()
    window.doModal = MagicMock()
    window.calculate_percent = MagicMock(return_value=50.0)
    window.getControl = MagicMock(side_effect=RuntimeError("missing"))
    return window


def test_play_next_background_tasks_timeout_reached(monkeypatch):
    """Task 3.1: Countdown reaches zero triggers auto-play."""
    monkeypatch.setattr("lib.gui.play_next_window.xbmc.sleep", lambda ms: None)
    window = _make_play_next(monkeypatch, auto_timeout=3)
    window.getTotalTime = MagicMock(return_value=600)
    window.getTime = MagicMock(return_value=500)
    window.getPlayingFile = MagicMock(return_value="file.mkv")

    window.background_tasks()

    window.handle_action.assert_called_once_with(7, 3001)
    window.close.assert_called_once_with()


def test_play_next_background_tasks_episode_ended(monkeypatch):
    """Task 3.1: Episode ends (<= 2s remaining) triggers immediate auto-play."""
    monkeypatch.setattr("lib.gui.play_next_window.xbmc.sleep", lambda ms: None)
    window = _make_play_next(monkeypatch, auto_timeout=10)
    window.getTotalTime = MagicMock(return_value=600)
    window.getTime = MagicMock(return_value=598)
    window.getPlayingFile = MagicMock(return_value="file.mkv")

    window.background_tasks()

    # handle_action called, then early return (close not called directly
    # because handle_action is mocked; in production handle_action calls close)
    window.handle_action.assert_called_once_with(7, 3001)


def test_play_next_background_tasks_window_closed(monkeypatch):
    """Task 3.1: Window closed during countdown exits without auto-play."""
    monkeypatch.setattr("lib.gui.play_next_window.xbmc.sleep", lambda ms: None)
    window = _make_play_next(monkeypatch, auto_timeout=10)
    window.getTotalTime = MagicMock(return_value=600)
    window.getTime = MagicMock(return_value=500)
    window.getPlayingFile = MagicMock(return_value="file.mkv")
    window.closed = True  # Window already closed before loop starts

    window.background_tasks()

    window.handle_action.assert_not_called()
    window.close.assert_called_once_with()


def test_play_next_background_tasks_legacy_mode(monkeypatch):
    """Task 3.1: Legacy mode (timeout <= 0) uses episode-remaining timer."""
    monkeypatch.setattr("lib.gui.play_next_window.xbmc.sleep", lambda ms: None)
    # Mock smart_play_action to avoid real implementation
    monkeypatch.setattr(
        "lib.gui.play_next_window.PlayNext.smart_play_action",
        lambda self: None,
    )
    window = _make_play_next(monkeypatch, auto_timeout=0)
    # Set near end so super().background_tasks() loop exits immediately
    window.getTotalTime = MagicMock(return_value=600)
    window.getTime = MagicMock(return_value=598)
    window.getPlayingFile = MagicMock(return_value="file.mkv")

    window.background_tasks()

    window.setProperty.assert_any_call("timer_label", "Playing in {} seconds")
    window.close.assert_called_once_with()
