# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import logging
import sys
import types
from unittest.mock import Mock, call

from helpers import reload_plugin
import pytest

import apprise
from apprise.plugins.glib import GLibUrgency, NotifyGLib

# Disable logging output during testing
logging.disable(logging.CRITICAL)


@pytest.fixture
def enabled_glib_environment(monkeypatch):
    """
    Fully mocked GI/GLib/Gio/GdkPixbuf environment for local and CI.
    """
    # Step 1: Fake gi and repository
    gi = types.ModuleType("gi")
    gi.require_version = Mock()

    fake_variant = Mock(name="Variant")
    fake_error = type("GLibError", (Exception,), {})
    fake_pixbuf = Mock()
    fake_image = Mock()

    fake_pixbuf.new_from_file.return_value = fake_image
    fake_image.get_width.return_value = 100
    fake_image.get_height.return_value = 100
    fake_image.get_rowstride.return_value = 1
    fake_image.get_has_alpha.return_value = False
    fake_image.get_bits_per_sample.return_value = 8
    fake_image.get_n_channels.return_value = 1
    fake_image.get_pixels.return_value = b""

    gi.repository = types.SimpleNamespace(
        Gio=Mock(),
        GLib=types.SimpleNamespace(Variant=fake_variant, Error=fake_error),
        GdkPixbuf=types.SimpleNamespace(Pixbuf=fake_pixbuf),
    )

    # Step 2: Inject into sys.modules
    sys.modules["gi"] = gi
    sys.modules["gi.repository"] = gi.repository

    # Step 3: Reload plugin with all mocks in place
    reload_plugin("glib")


def test_plugin_glib_gdkpixbuf_attribute_error(monkeypatch):
    """Simulate AttributeError from importing GdkPixbuf"""

    # Create gi module
    gi = types.ModuleType("gi")

    # Create gi.repository mock, but DO NOT include GdkPixbuf
    gi.repository = types.SimpleNamespace(
        Gio=Mock(),
        GLib=types.SimpleNamespace(
            Variant=Mock(), Error=type("GLibError", (Exception,), {})
        ),
        # GdkPixbuf missing entirely triggers AttributeError
    )

    def fake_require_version(name, version):
        if name == "GdkPixbuf":
            # Simulate success in require_version
            return
        return

    gi.require_version = Mock(side_effect=fake_require_version)

    # Inject into sys.modules
    sys.modules["gi"] = gi
    sys.modules["gi.repository"] = gi.repository

    # Trigger the plugin reload with our patched environment
    reload_plugin("glib")

    from apprise.plugins import glib as plugin_glib

    assert plugin_glib.NOTIFY_GLIB_IMAGE_SUPPORT is False


def test_plugin_glib_basic_notify(enabled_glib_environment):
    """Basic notification path"""
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    assert isinstance(obj, NotifyGLib)
    assert obj.notify("body", title="title") is True


def test_plugin_glib_url_includes_coordinates(enabled_glib_environment):
    """Test that x/y coordinates appear in the rendered URL."""
    obj = apprise.Apprise.instantiate(
        "glib://_/?x=7&y=9", suppress_exceptions=False
    )
    url = obj.url(privacy=False)

    assert "x=7" in url
    assert "y=9" in url


def test_plugin_glib_icon_fails_gracefully(mocker, enabled_glib_environment):
    """Simulate image load failure"""
    import gi

    gi.repository.GdkPixbuf.Pixbuf.new_from_file.side_effect = AttributeError(
        "fail"
    )
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    spy = mocker.spy(obj, "logger")
    assert obj.notify("msg", title="t") is True
    assert any(
        "Could not load notification icon" in str(x)
        for x in spy.warning.call_args_list
    )


def test_plugin_glib_send_raises_glib_error(mocker, enabled_glib_environment):
    """Simulate GLib.Error in DBusProxy creation"""
    import gi

    gi.repository.Gio.DBusProxy.new_for_bus_sync.side_effect = (
        gi.repository.GLib.Error("fail")
    )
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    assert obj.notify("fail test") is False


def test_plugin_glib_send_raises_generic(mocker, enabled_glib_environment):
    """Simulate generic error in gio_iface.Notify()"""
    # Re: https://github.com/caronc/apprise/issues/1383
    # This test validates that the NotifyGLib plugin correctly handles a
    # generic exception raised by the `Notify()` method call on a mocked
    # DBus interface. However, it is only meaningful in environments that:
    #
    #  1. Do NOT have PyGObject (`gi`) installed, OR
    #  2. Have `gi`, but without introspection or live bindings activated.
    #
    # When PyGObject is installed and active, the `gi.repository` namespace
    # becomes populated by introspected C-based objects that do not behave
    # like regular Python functions. This causes mock patching via
    # `mocker.patch("gi.repository.Gio.DBusProxy.new_for_bus_sync")` to
    # silently fail or be ignored, as Python's mocking machinery cannot
    # reliably override these introspected symbols.
    #
    # This test exists to ensure coverage of legacy or minimal environments
    # where Apprise's GLib support can still be used through soft mocks,
    # such as CI/CD pipelines or headless test setups where PyGObject is
    # absent or stubbed (as done via `enabled_glib_environment`).
    #
    # Note: In production environments with active PyGObject, exception
    # handling is already tested via `GLib.Error` branches or during actual
    # usage of `NotifyGLib.send()`. This test supplements that by simulating
    # the rare fallback case of a non-GLib-related exception during Notify().
    import gi

    if hasattr(gi, "repository"):
        pytest.skip(
            "pygobject introspection active, test won't behave as expected"
        )

    fake_iface = Mock()
    fake_iface.Notify.side_effect = RuntimeError("boom")

    mocker.patch(
        "gi.repository.Gio.DBusProxy.new_for_bus_sync",
        return_value=fake_iface,
    )

    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    logger = mocker.spy(obj, "logger")
    assert obj.notify("boom", title="fail") is False
    logger.warning.assert_called_with("Failed to send GLib/Gio notification.")


def test_plugin_glib_disabled(mocker, enabled_glib_environment):
    """Test disabled plugin returns False on notify()"""
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    obj.enabled = False
    assert obj.notify("x") is False


def test_plugin_glib_invalid_coords():
    """Invalid x/y coordinates cause TypeError"""
    with pytest.raises(TypeError):
        NotifyGLib(x_axis="bad", y_axis="1")
    with pytest.raises(TypeError):
        NotifyGLib(x_axis="1", y_axis="bad")


def test_plugin_glib_urgency_parsing():
    """Urgency variants map correctly"""
    assert NotifyGLib(urgency="high").urgency == GLibUrgency.HIGH
    assert NotifyGLib(urgency="invalid").urgency == GLibUrgency.NORMAL
    assert NotifyGLib(urgency="2").urgency == GLibUrgency.HIGH
    assert NotifyGLib(urgency=0).urgency == GLibUrgency.LOW


def test_plugin_glib_parse_url_fields():
    url = "glib://_/?x=5&y=5&image=no&priority=high"
    result = NotifyGLib.parse_url(url)
    assert result["x_axis"] == "5"
    assert result["y_axis"] == "5"
    assert result["include_image"] is False
    assert result["urgency"] == "high"


def test_plugin_glib_xy_axis_applied_to_variant(enabled_glib_environment):
    """Ensure x/y values are added to GLib.Variant payload."""
    obj = apprise.Apprise.instantiate(
        "glib://_/?x=5&y=10", suppress_exceptions=False
    )

    # Patch GLib.Variant to track calls
    import gi

    spy_variant = Mock(wraps=gi.repository.GLib.Variant)
    gi.repository.GLib.Variant = spy_variant

    assert obj.notify("Test with coords", title="xy") is True

    # Check x and y were added to meta_payload
    assert call("i", 5) in spy_variant.mock_calls
    assert call("i", 10) in spy_variant.mock_calls


def test_plugin_glib_no_image_support(monkeypatch, enabled_glib_environment):
    """Simulate GdkPixbuf unavailable"""
    monkeypatch.setattr(
        "apprise.plugins.glib.NOTIFY_GLIB_IMAGE_SUPPORT", False
    )
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    assert obj.notify("no image") is True


def test_plugin_glib_url_redaction(enabled_glib_environment):
    """url() privacy mode redacts safely"""
    obj = apprise.Apprise.instantiate(
        "glib://_/?image=no&urgency=high", suppress_exceptions=False
    )
    url = obj.url(privacy=True)
    assert "image=" in url
    assert "urgency=" in url
    assert url.startswith("glib://_/")


def test_plugin_glib_require_version_importerror(monkeypatch):
    """Simulate gi.require_version() raising ImportError"""
    gi = types.ModuleType("gi")
    gi.require_version = Mock(side_effect=ImportError("no gio"))
    sys.modules["gi"] = gi
    reload_plugin("glib")
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    assert not isinstance(obj, NotifyGLib)


def test_plugin_glib_require_version_valueerror(monkeypatch):
    """Simulate gi.require_version() raising ValueError without reload
    crash."""

    import gi

    import apprise.plugins.glib as plugin_glib

    # Patch require_version after import
    monkeypatch.setattr(
        gi, "require_version", Mock(side_effect=ValueError("fail"))
    )

    # Re-evaluate plugin support logic manually
    try:
        gi.require_version("Gio", "2.0")

    except Exception:
        plugin_glib.NOTIFY_GLIB_SUPPORT_ENABLED = False
        plugin_glib.NotifyGLib.enabled = False

    # Confirm plugin is now marked disabled
    assert not plugin_glib.NotifyGLib.enabled

    # Apprise will skip this plugin
    obj = apprise.Apprise.instantiate("glib://", suppress_exceptions=False)
    assert not isinstance(obj, plugin_glib.NotifyGLib)


def test_plugin_glib_gdkpixbuf_require_version_valueerror(monkeypatch):
    """Simulate gi.require_version('GdkPixbuf', ...) raising ValueError"""

    # Step 1: Mock GI
    gi = types.ModuleType("gi")
    gi.repository = types.ModuleType("gi.repository")

    def fake_require_version(name: str, version: str) -> None:
        if name == "GdkPixbuf":
            raise ValueError("GdkPixbuf unavailable")

    gi.require_version = Mock(side_effect=fake_require_version)

    # Step 2: Patch into sys.modules
    sys.modules["gi"] = gi
    sys.modules["gi.repository"] = gi.repository

    # Step 3: Reload plugin to trigger branch
    reload_plugin("glib")

    # Step 4: Confirm GdkPixbuf image support was not enabled
    from apprise.plugins import glib as plugin_glib

    assert plugin_glib.NOTIFY_GLIB_IMAGE_SUPPORT is False


def test_plugin_glib_notify_generic_exception(
    mocker, enabled_glib_environment
):
    """
    Test that a generic exception occurring during the notification send
    is caught and handled (returning False).
    """
    import gi

    # 1. Create a mock object to act as the DBus Proxy
    mock_proxy = mocker.Mock()

    # 2. Force the Notify method to raise a generic Exception
    #    This triggers the 'except Exception as e:'
    mock_proxy.Notify.side_effect = Exception("Simulated Generic Failure")

    # 3. Patch the Gio constructor to return our crashing mock
    #    The code calls: Gio.DBusProxy.new_for_bus_sync(...)
    gi.repository.Gio.DBusProxy.new_for_bus_sync.return_value = mock_proxy

    # 4. Instantiate the plugin
    from apprise.plugins.glib import NotifyGLib

    obj = NotifyGLib(targets=["glib://"])

    # 5. Spy on the logger to ensure the warning is logged
    logger_spy = mocker.spy(obj, "logger")

    # 6. Execute notify()
    #    It should catch the exception and return False
    assert obj.notify(title="Title", body="Body") is False

    # 7. Verify the specific log message from glib.py
    logger_spy.warning.assert_called_with(
        "Failed to send GLib/Gio notification."
    )
