-- This test suite tries to prevent the legacy notification popups from
-- regressing as the new notification API is improving.
local spawn     = require("awful.spawn")
local naughty   = require("naughty"    )
local gdebug    = require("gears.debug")
local gtable    = require("gears.table")
local gfs       = require("gears.filesystem")
local cairo     = require("lgi"        ).cairo
local beautiful = require("beautiful")
local Gio       = require("lgi"        ).Gio
local GLib      = require("lgi"        ).GLib
local gpcall    = require("gears.protected_call")
local dwidget   = require("naughty.widget._default")

-- Bypass the new rc.lua and force the legacy mode again.
naughty._reset_display_handlers()

-- This module test deprecated APIs
require("gears.debug").deprecate = function() end

local dbus_connection = assert(Gio.bus_get_sync(Gio.BusType.SESSION))

local function send_notify(app, id, icon, summary, body, actions, hints, timeout, callback)
    local parameters = GLib.Variant("(susssasa{sv}i)", {
        app, id, icon, summary, body, actions, hints, timeout
    })
    local reply_type = GLib.VariantType.new("(u)")

    local function invoke_callback(conn, result)
        local new_id = conn:call_finish(result).value[1]
        if callback then
            callback(new_id)
        end
    end
    dbus_connection:call("org.freedesktop.Notifications",
        "/org/freedesktop/Notifications", "org.freedesktop.Notifications",
        "Notify", parameters, reply_type, Gio.DBusCallFlags.NO_AUTO_START, -1,
        nil, invoke_callback)
end

local steps = {}

local has_cmd_notify = false

-- Use `notify-send` instead of the shimmed version to better test the dbus
-- to notification code.
local function check_cmd()
    local path = os.getenv("PATH")
    local pos = 1
    while path:find(":", pos) do
        local np = path:find(":", pos)
        local p = path:sub(pos, np-1).."/"

        pos = np+1

        local f = io.open(p.."notify-send")
        if f then
            f:close()
            has_cmd_notify = true
        end

        if has_cmd_notify then return end
    end
end

check_cmd()

if not has_cmd_notify then
    require("gears.debug").print_warning("Did not find notify-send, skipping some tests")
end

local active, destroyed, reasons, added_counter = {}, {}, {}, 0

local default_width, default_height = 0, 0

local function added_callback(n)
    table.insert(active, n)
    added_counter = added_counter + 1
end
naughty.connect_signal("added", added_callback)

local function destroyed_callback(n, reason)
    local found = false

    for k, n2 in ipairs(active) do
        if n2 == n then
            found = true
            table.remove(active, k)
        end
    end

    assert(found)

    if reason then
        reasons[reason] = reasons[reason] and reasons[reason] + 1 or 1
    end

    table.insert(destroyed, n)
end
naughty.connect_signal("destroyed", destroyed_callback)

-- Test vertical overlapping
local function test_overlap()
    local wa = mouse.screen.workarea

    for _, n1 in ipairs(active) do
        assert(n1.box)
        assert(n1.id)

        -- Check for overlapping the workarea
        assert(n1.box.y+default_height < wa.y+wa.height)
        assert(n1.box.y >= wa.y)

        -- Check for overlapping each other
        for _, n2 in ipairs(active) do
            assert(n2.box)
            if n1 ~= n2 then
                local geo1, geo2 = n1.box:geometry(), n2.box:geometry()
                assert(geo1.height == geo2.height)
                assert(geo1.height + 2*n1.box.border_width + naughty.config.spacing
                    == default_height)

                if n1.position == n2.position then
                    assert(
                        geo1.y >= geo2.y+default_height or
                        geo2.y >= geo1.y+default_height
                    )
                end
            end
        end
    end
end

-- Test default size, and title/message.
table.insert(steps, function()

    local n = naughty.notification {
        title   = "title",
        message = "message"
    }

    assert(n.box)
    local offset = 2*n.box.border_width
    default_width  = n.box.width+offset
    default_height = n.box.height + offset + naughty.config.spacing

    assert(default_width  > 0)
    assert(default_height > 0)

    -- Test title/message behavior.
    assert(n.textbox:get_markup() == "<b>title</b>\nmessage", n.textbox:get_markup())
    assert(n.textbox:get_text() == "title\nmessage", n.textbox:get_text())

    n.title = nil
    assert(n.textbox:get_markup() == "message", n.textbox:get_markup())
    assert(n.textbox:get_text() == "message", n.textbox:get_text())
    n.title = ""
    assert(n.textbox:get_markup() == "message", n.textbox:get_markup())
    assert(n.textbox:get_text() == "message", n.textbox:get_text())

    n.message = "<i>only</i> message"
    assert(n.textbox:get_markup() == "<i>only</i> message", n.textbox:get_markup())
    assert(n.textbox:get_text() == "only message", n.textbox:get_text())

    n.message = nil
    assert(n.textbox:get_markup() == "", n.textbox:get_markup())
    assert(n.textbox:get_text() == "", n.textbox:get_text())
    n.message = ""
    assert(n.textbox:get_markup() == "", n.textbox:get_markup())
    assert(n.textbox:get_text() == "", n.textbox:get_text())

    -- Only title (with markup).
    n.title = "<i>only</i> title"
    assert(n.textbox:get_markup() == "<b>&lt;i&gt;only&lt;/i&gt; title</b>", n.textbox:get_markup())
    assert(n.textbox:get_text() == "<i>only</i> title", n.textbox:get_text())

    -- Markup with only interpreting "<br>".
    n.title = ""
    n.message = "<3"
    assert(n.textbox:get_markup() == "&lt;3", n.textbox:get_markup())
    assert(n.textbox:get_text() == "<3", n.textbox:get_text())
    n.title = ">3"
    assert(n.textbox:get_markup() == "<b>&gt;3</b>\n&lt;3", n.textbox:get_markup())
    assert(n.textbox:get_text() == ">3\n<3", n.textbox:get_text())
    n.message = nil
    assert(n.textbox:get_markup() == "<b>&gt;3</b>", n.textbox:get_markup())
    assert(n.textbox:get_text() == ">3", n.textbox:get_text())

    -- Invalid utf8.
    local invalid_title = "title" .. string.char(0x80) .. "X"
    local invalid_message = "message" .. string.char(0x80) .. "X"
    n.title = invalid_title
    n.message = invalid_message
    assert(n.textbox:get_markup() == nil, n.textbox:get_markup())
    local expected_text = invalid_title .. "\n" .. invalid_message
    expected_text = expected_text:gsub(string.char(0x80), string.char(0xff))
    assert(n.textbox:get_text() == expected_text, n.textbox:get_text())

    n:destroy()

    assert(added_counter == 1)
    added_counter = 0

    return true
end)


if has_cmd_notify then
    table.insert(steps, function()
        spawn{ 'notify-send', 'title', 'message', '-t', '25000' }

        return true
    end)

    table.insert(steps, function()
        if #active ~= 1 then return end

        local n = active[1]

        assert(n.box)

        -- Make sure the expiration timer is started
        assert(n.timer)
        assert(n.timer.started)
        assert(n.is_expired == false)

        n:destroy()

        assert(#active == 0)

        return true
    end)

    -- Test pausing incoming notifications.
    table.insert(steps, function()
        assert(not naughty.suspended)

        naughty.suspended = true

        -- There is some magic behind this, check it works
        assert(naughty.suspended)

        spawn{ 'notify-send', 'title', 'message', '-t', '25000' }

        return true
    end)

    -- Test resuming incoming notifications.
    table.insert(steps, function(count)
        if count ~= 4 then return end

        assert(#active == 0)
        assert(#naughty.notifications.suspended == 1)
        assert(naughty.notifications.suspended[1]:get_suspended())

        naughty.resume()

        assert(not naughty.suspended)
        assert(#naughty.notifications.suspended == 0)
        assert(#active == 1)

        active[1]:destroy()
        assert(#active == 0)

        spawn{ 'notify-send', 'title', 'message', '-t', '1' }

        return true
    end)

    -- Test automatic expiration.
    table.insert(steps, function()
        if added_counter ~= 3 then return end

        return true
    end)

    table.insert(steps, function()
        if #active > 0 then return end

        -- It expired after one milliseconds, so it should be gone as soon as
        -- it is registered.
        assert(#active == 0)

        assert(not naughty.expiration_paused)
        naughty.expiration_paused = true

        -- There is some magic behind this, make sure it works
        assert(naughty.expiration_paused)

        spawn{ 'notify-send', 'title', 'message', '-t', '1' }

        return true
    end)

    -- Test disabling automatic expiration.
    table.insert(steps, function()
        if added_counter ~= 4 then return end

        -- It should not expire by itself, so that should always be true
        assert(#active == 1)

        return true
    end)

    -- Wait long enough to avoid races.
    table.insert(steps, function(count)
        if count ~= 4 then return end

        assert(#active == 1)
        assert(active[1].is_expired)

        naughty.expiration_paused = false
        assert(not naughty.expiration_paused)

        return true
    end)

    -- Make sure enabling expiration process the expired queue.
    table.insert(steps, function()
        -- Right now this doesn't require a step for itself, but this could change
        -- so better not "document" the instantaneous clearing of the queue.
        if #active > 0 then return end

        spawn{ 'notify-send', 'low',      'message', '-t', '25000', '-u', 'low' }
        spawn{ 'notify-send', 'normal',   'message', '-t', '25000', '-u', 'normal' }
        spawn{ 'notify-send', 'critical', 'message', '-t', '25000', '-u', 'critical' }

        return true
    end)

    -- Test the urgency level and default preset.
    table.insert(steps, function()
        if added_counter ~= 7 then return end

        while #active > 0 do
            active[1]:destroy()
        end

        return true
    end)

    -- Test what happens when the screen has the maximum number of notification it
    -- can display at one.
    table.insert(steps, function()
        local wa = mouse.screen.workarea
        local max_notif = math.floor(wa.height/default_height)

        -- Everything should fit, otherwise the math is wrong in
        -- `neughty.layout.legacy` and its a regression.
        for i=1, max_notif do
            spawn{ 'notify-send', 'notif '..i, 'message', '-t', '25000', '-u', 'low' }
        end

        return true
    end)

    -- Check the lack of overlapping and the presence of the expected content.
    table.insert(steps, function()
        local wa = mouse.screen.workarea
        local max_notif = math.floor(wa.height/default_height)
        if added_counter ~= 7 + max_notif then return end

        assert(#active == max_notif)

        test_overlap()

        -- Now add even more!
        for i=1, 5 do
            spawn{ 'notify-send', 'notif '..i, 'message', '-t', '25000', '-u', 'low' }
        end

        return true
    end)

    -- Test the code to hide the older notifications when there is too many for the
    -- screen.
    table.insert(steps, function()
        local wa = mouse.screen.workarea
        local max_notif = math.floor(wa.height/default_height)
        if added_counter ~= 7 + max_notif + 5 then return end

        -- The other should have been hidden
        assert(#active == max_notif)

        assert(reasons[naughty.notification_closed_reason.too_many_on_screen] == 5)

        test_overlap()

        while #active > 0 do
            active[1]:destroy()
        end

        return true
    end)

end

local positions = {
    "top_left"      , "top_middle"    , "top_right"     ,
    "bottom_left"   , "bottom_middle" , "bottom_right"  ,
    "middle"        ,
}

-- Test each corners.
table.insert(steps, function()
    for _, pos in ipairs(positions) do
        for i=1, 3 do
            -- Skip dbus for this one.
            naughty.notification {
                position = pos,
                title    = "At "..pos.." "..i,
                message  = "some message",
                timeout  = 25000,
            }
        end
    end

    return true
end)

table.insert(steps, function()
    if #active ~= #positions*3 then return end

    test_overlap()

    while #active > 0 do
        active[1]:destroy()
    end

    return true
end)

local big_icon     = cairo.ImageSurface(cairo.Format.ARGB32, 256, 256)
local cr           = cairo.Context(big_icon)
local small_icon   = cairo.ImageSurface(cairo.Format.ARGB32, 32 , 32 )
local cr2          = cairo.Context(small_icon)
local wierd_ratio1 = cairo.ImageSurface(cairo.Format.ARGB32, 256, 128)
local cr3          = cairo.Context(wierd_ratio1)
local wierd_ratio2 = cairo.ImageSurface(cairo.Format.ARGB32, 128, 256)
local cr4          = cairo.Context(wierd_ratio2)

-- Checkboard shirt pattern icon!
for i=1, 5 do
    for j=1, 5 do
        cr:set_source_rgb(
            i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1
        )
        cr:rectangle( (i-1)*48, (j-1)*48, 48, 48 )
        cr:fill()

        cr2:set_source_rgb(
            i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1
        )
        cr2:rectangle( (i-1)*6, (j-1)*6, 6, 6 )
        cr2:fill()

        cr3:set_source_rgb(
            i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1
        )
        cr3:rectangle( (i-1)*48, (j-1)*24, 48, 24 )
        cr3:fill()

        cr4:set_source_rgb(
            i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1
        )
        cr4:rectangle( (i-1)*24, (j-1)*48, 24, 48 )
        cr4:fill()
    end
end

-- Test the icon size constraints.
table.insert(steps, function()
    beautiful.notification_icon_size = 64

    -- Icons that are too large (they should be downscaled)
    local n1 = naughty.notification {
        icon    = big_icon,
        title   = "Has a nice icon!",
        message = "big",
        timeout = 25000,
    }

    assert(n1.id)
    assert(n1.iconbox)
    assert(n1.iconbox._private.image:get_width () == beautiful.notification_icon_size)
    assert(n1.iconbox._private.image:get_height() == beautiful.notification_icon_size)
    assert(n1.iconbox._private.image:get_width () == n1.size_info.icon_w)
    assert(n1.iconbox._private.image:get_height() == n1.size_info.icon_h)
    assert(n1.size_info.icon_scale_factor == 1/4)

    -- Icons that are too small (they should not be upscaled)
    local n2 = naughty.notification {
        icon    = small_icon,
        title   = "Has a nice icon!",
        message = "small",
        timeout = 25000,
    }

    assert(n2.id)
    assert(n2.iconbox)
    assert(n2.iconbox._private.image:get_width () == 32)
    assert(n2.iconbox._private.image:get_height() == 32)
    assert(n2.iconbox._private.image:get_width () == n2.size_info.icon_w)
    assert(n2.iconbox._private.image:get_height() == n2.size_info.icon_h)
    assert(not n2.size_info.icon_scale_factor)

    -- Downscaled non square icons (aspect ratio should be kept).
    local n3 = naughty.notification {
        icon    = wierd_ratio1,
        title   = "Has a nice icon!",
        message = "big",
        timeout = 25000,
    }

    local n4 = naughty.notification {
        icon    = wierd_ratio2,
        title   = "Has a nice icon!",
        message = "big",
        timeout = 25000,
    }

    assert(n3.iconbox)
    assert(n3.iconbox._private.image:get_width () == beautiful.notification_icon_size)
    assert(n3.iconbox._private.image:get_height() == beautiful.notification_icon_size/2)
    assert(n3.iconbox._private.image:get_width () == n3.size_info.icon_w)
    assert(n3.iconbox._private.image:get_height() == n3.size_info.icon_h)
    assert(n3.size_info.icon_scale_factor == 1/4)

    assert(n4.iconbox)
    assert(n4.iconbox._private.image:get_width () == beautiful.notification_icon_size/2)
    assert(n4.iconbox._private.image:get_height() == beautiful.notification_icon_size)
    assert(n4.iconbox._private.image:get_width () == n4.size_info.icon_w)
    assert(n4.iconbox._private.image:get_height() == n4.size_info.icon_h)
    assert(n4.size_info.icon_scale_factor == 1/4)

    -- The notification size should change proportionally to the icon size.
    assert(n1.box.width  == n2.box.width  + 32)
    assert(n1.box.height == n2.box.height + 32)
    assert(n1.box.height == n3.box.height + 32)
    assert(n1.box.width  == n4.box.width  + 32)
    assert(n1.box.height == n4.box.height)
    assert(n1.box.width  == n3.box.width )

    -- Make sure unconstrained icons work as expected.
    beautiful.notification_icon_size = nil

    local n5 = naughty.notification {
        icon    = big_icon,
        title   = "Has a nice icon!",
        message = "big",
        timeout = 25000,
    }

    assert(n5.iconbox)
    assert(n5.iconbox._private.image:get_width () == 256)
    assert(n5.iconbox._private.image:get_height() == 256)
    assert(n5.iconbox._private.image:get_width () == n5.size_info.icon_w)
    assert(n5.iconbox._private.image:get_height() == n5.size_info.icon_h)
    assert(not n5.size_info.icon_scale_factor)

    -- Make sure invalid icons don't prevent the message from being shown.
    local n6 = naughty.notification {
        icon    = "this/is/an/invlid/path",
        title   = "Has a nice icon!",
        message = "Very important life saving advice",
        timeout = 25000,
    }

    n1:destroy()
    n2:destroy()
    n3:destroy()
    n4:destroy()
    n5:destroy()
    n6:destroy()
    assert(#active == 0)

    return true
end)

-- Test notifications with size constraints.
table.insert(steps, function()
    local str = "foobar! "
    assert(#active == 0)

    -- 2^9 foobars is a lot of foobars.
    for _=1, 10 do
        str = str .. str
    end

    -- First, see what happen without any constraint and enormous messages.
    -- This also test notifications larger than the workarea.

    local n1 = naughty.notification {
        title   = str,
        message = str,
        timeout = 25000,
    }

    -- Same, but with an icon and larger borders.
    local n2 = naughty.notification {
        icon         = big_icon,
        title        = str,
        message      = str,
        timeout      = 25000,
        border_width = 40,
    }

    local wa = mouse.screen.workarea
    assert(n1.box.width +2*n1.box.border_width <= wa.width )
    assert(n1.box.height+2*n1.box.border_width <= wa.height)
    assert(n2.box.width +2*n2.box.border_width <= wa.width )
    assert(n2.box.height+2*n2.box.border_width <= wa.height)

    -- Check with client icons.
    assert(not n1.icon)

    n1._private.clients = {{icon= big_icon, type = "normal"}}
    assert(n1.icon == big_icon)
    assert(n1.box.width +2*n1.box.border_width <= wa.width )
    assert(n1.box.height+2*n1.box.border_width <= wa.height)
    assert(n2.box.width +2*n2.box.border_width <= wa.width )
    assert(n2.box.height+2*n2.box.border_width <= wa.height)

    n1:destroy()
    n2:destroy()

    -- Now set a maximum size and try again.
    beautiful.notification_max_width  = 256
    beautiful.notification_max_height = 96

    local n3 = naughty.notification {
        title   = str,
        message = str,
        timeout = 25000,
    }

    assert(n3.box.width  <= 256)
    assert(n3.box.height <= 96 )

    -- Now test when the icon is larger than the maximum.
    local n4 = naughty.notification {
        icon    = big_icon,
        title   = str,
        message = str,
        timeout = 25000,
    }

    assert(n4.box.width  <= 256)
    assert(n4.box.height <= 96 )
    assert(n4.iconbox._private.image:get_width () == n4.size_info.icon_w)
    assert(n4.iconbox._private.image:get_height() == n4.size_info.icon_h)
    assert(n4.size_info.icon_w <= 256)
    assert(n4.size_info.icon_h <= 96 )

    n3:destroy()
    n4:destroy()
    assert(#active == 0)

    return true
end)

-- Test more advanced features than what notify-send can provide.

-- Test the actions.
table.insert(steps, function()

    assert(#active == 0)

    send_notify("Awesome test", 0, "", "title", "message body",
        { "1", "one", "2", "two", "3", "three" }, {}, 25000)

    return true
end)

table.insert(steps, function()
    if #active == 0 then return end

    assert(#active == 1)
    local n = active[1]

    assert(n.box)
    assert(#n.actions == 3)
    assert(n.actions[1].name == "one"  )
    assert(n.actions[2].name == "two"  )
    assert(n.actions[3].name == "three")

    n:destroy()

    return true
end)

--TODO Test too many actions.

--TODO Test action with long names.

local nid, name_u, message_u, actions_u = nil

-- Test updating a notification.
table.insert(steps, function()
    send_notify("Awesome test", 0, "", "title", "message body",
        { "1", "one", "2", "two", "3", "three" }, {}, 25000, function(id)
            nid = id
        end)

    return true
end)

table.insert(steps, function()
    if #active == 0 or not nid then return end

    local n = active[1]

    n:connect_signal("property::title"  , function(_, val) name_u    = val end)
    n:connect_signal("property::message", function(_, val) message_u = val end)
    n:connect_signal("property::actions", function(_, val) actions_u = val end)

    send_notify("Awesome test", nid, "", "updated title", "updated message body",
        { "1", "four", "2", "five", "3", "six" }, {}, 25000)

    return true
end)

-- Test if all properties have been updated.
table.insert(steps, function()
    if not name_u    then return end
    if not message_u then return end
    if not actions_u then return end

    -- No new notification should have been created.
    assert(#active == 1)

    local n = active[1]

    assert(n.title   == "updated title"       )
    assert(n.message == "updated message body")

    assert(#n.actions == 3)
    assert(n.actions[1].name == "four" )
    assert(n.actions[2].name == "five" )
    assert(n.actions[3].name == "six"  )

    n:destroy()
    assert(#active == 0)

    assert(name_u == "updated title", name_u)
    assert(message_u == "updated message body", name_u)
    assert(#actions_u == 3)

    return true
end)

local int_n = nil

-- Test replace_id with internally generated notifications.
-- Even if it makes little sense, it should work and is used in the wild.
table.insert(steps, function()
    int_n = naughty.notification {
        title   = "foo",
        message = "bar",
        timeout = 25000,
    }

    local nid2 = int_n.id
    assert(int_n.id)
    local res, err = pcall(function()
        int_n.id = 1337
    end)

    assert(err:match("Notification identifier can only be set once"))
    assert(not res)
    assert(int_n.id == nid2)

    -- watch out, capi.dbus signals are not normal object signals
    send_notify("Awesome test", nid2, "", "title2", "text2",
        { "the one action" }, {}, 123)

    return true
end)

-- Test that the values were replaced
table.insert(steps, function()
    if int_n.title ~= "title2" then return end

    assert(int_n.title   == "title2")
    assert(int_n.message == "text2" )
    --assert(int_n.timeout == 123     ) --FIXME
    int_n:destroy()

    --TODO test what happens when updating a destroyed notification
    -- There is currently no API to resurrect notifications.

    return true
end)

-- Test adding actions, resident mode and action sharing.
table.insert(steps, function()
    local n1 = naughty.notification {
        title    = "foo",
        message  = "bar",
        timeout  = 25000,
        resident = true,
        actions  = { naughty.action { name = "a1" } }
    }

    local n2 = naughty.notification {
        title    = "foo",
        message  = "bar",
        resident = true,
        timeout  = 25000,
        actions  = { naughty.action { name = "a2" } }
    }

    local is_called = {}

    n1:connect_signal("invoked", function() is_called[1] = true end)
    n2:connect_signal("invoked", function() is_called[2] = true end)

    n1.actions[1]:invoke(n1)
    n2.actions[1]:invoke(n2)

    assert(is_called[1])
    assert(is_called[2])
    assert(not n1._private.is_destroyed)
    assert(not n2._private.is_destroyed)

    local shared_a = naughty.action { name = "a3" }

    n1:append_actions {shared_a}
    n2:append_actions {shared_a}

    n1:connect_signal("invoked", function() is_called[3] = true end)
    n2:connect_signal("invoked", function() is_called[4] = true end)

    assert(n1.actions[2] == shared_a)
    assert(n2.actions[2] == shared_a)

    shared_a:invoke(n1)

    assert(is_called[3])
    assert(not is_called[4])
    assert(not n1._private.is_destroyed)
    assert(not n2._private.is_destroyed)

    n1.resident = false
    n2.resident = false

    shared_a:invoke(n1)

    assert(n1._private.is_destroyed)
    assert(not n2._private.is_destroyed)

    shared_a:invoke(n2)
    assert(n2._private.is_destroyed)

    return true
end)

-- Test that exposing support for animations and persistence are exposed to DBus.
table.insert(steps, function()
    assert(not naughty.persistence_enabled)
    assert(not naughty.image_animations_enabled)

    assert(gtable.hasitem(naughty.dbus._capabilities, "icon-static"))
    assert(not gtable.hasitem(naughty.dbus._capabilities, "icon-multi"))
    assert(not gtable.hasitem(naughty.dbus._capabilities, "persistence"))

    naughty.persistence_enabled      = true
    naughty.image_animations_enabled = true

    assert(naughty.persistence_enabled)
    assert(naughty.image_animations_enabled)

    assert(gtable.hasitem(naughty.dbus._capabilities, "icon-multi"))
    assert(gtable.hasitem(naughty.dbus._capabilities, "persistence"))
    assert(not gtable.hasitem(naughty.dbus._capabilities, "icon-static"))

    naughty.persistence_enabled      = false
    naughty.image_animations_enabled = false

    assert(not naughty.persistence_enabled)
    assert(not naughty.image_animations_enabled)

    assert(    gtable.hasitem(naughty.dbus._capabilities, "icon-static"))
    assert(not gtable.hasitem(naughty.dbus._capabilities, "icon-multi" ))
    assert(not gtable.hasitem(naughty.dbus._capabilities, "persistence"))

    return true
end)

-- Check that request::icon stops when an icon is found.
table.insert(steps, function()

    local called = 0

    local function set_icon1()
        called = called + 1
    end

    local function set_icon2(n)
        called = called + 1
        n.icon = big_icon
    end

    local function set_icon3()
        called = called + 1
    end

    naughty.connect_signal("request::icon", set_icon1)
    naughty.connect_signal("request::icon", set_icon2)
    naughty.connect_signal("request::icon", set_icon3)

    assert(called == 0)

    local n1 = naughty.notification {
        title    = "foo",
        message  = "bar",
        app_icon = "baz"
    }

    assert(called == 2)

    n1:destroy()

    naughty.disconnect_signal("request::icon", set_icon1)
    naughty.disconnect_signal("request::icon", set_icon2)
    naughty.disconnect_signal("request::icon", set_icon3)

    -- Check if `disconnect_signal` works.
    n1 = naughty.notification {
        title    = "foo",
        message  = "bar",
        app_icon = "baz"
    }

    assert(called == 2)

    n1:destroy()

    return true
end)

local icon_requests = {}

-- Check if the action icon support is detected.
table.insert(steps, function()
    assert(#active == 0)

    naughty.connect_signal("request::action_icon", function(a, _, hints)
        icon_requests[hints.id] = a
        a.icon = hints.id == "list-add" and small_icon or big_icon
    end)

    naughty.connect_signal("request::icon", function(n, _)
        icon_requests[n] = true
    end)

    local hints = {
        ["action-icons"] = GLib.Variant("b", true),
    }

    send_notify("Awesome test", 0, "", "title", "message body",
        { "list-add", "add", "list-remove", "remove" }, hints, 25000)

    return true
end)

table.insert(steps, function()
    if #active ~= 1 then return end

    local n = active[1]

    assert(icon_requests[n])

    assert(n._private.freedesktop_hints)
    assert(n._private.freedesktop_hints["action-icons"] == true)

    assert(icon_requests["list-add"   ] == n.actions[1])
    assert(icon_requests["list-remove"] == n.actions[2])

    assert(n.actions[1].icon == small_icon)
    assert(n.actions[2].icon == big_icon  )

    assert(type(n.actions[1].position) == "number")
    assert(type(n.actions[2].position) == "number")
    assert(n.actions[1].position == 1)
    assert(n.actions[2].position == 2)

    n:destroy()

    return true
end)

-- Test the error popup.
table.insert(steps, function()
    local got = nil

    naughty.connect_signal("request::display_error", function(err)
        got = err
    end)

    awesome.emit_signal("debug::error", "foo")

    assert(got == "foo")

    return true
end)

-- Now check if the old deprecated (but still supported) APIs don't have errors.
table.insert(steps, function()
    -- Tests are (by default) not allowed to call deprecated APIs
    gdebug.deprecate = function() end

    local n = naughty.notification {
        title    = "foo",
        message  = "bar",
        timeout  = 25000,
        app_icon = "baz"
    }

    -- Make sure the suspension don't cause errors
    assert(not naughty.is_suspended())
    assert(not naughty.suspended)
    naughty.suspend()
    assert(naughty.is_suspended())
    assert(naughty.suspended)
    naughty.resume()
    assert(not naughty.is_suspended())
    assert(not naughty.suspended)
    naughty.toggle()
    assert(naughty.is_suspended())
    assert(naughty.suspended)
    naughty.toggle()
    assert(not naughty.is_suspended())
    assert(not naughty.suspended)
    naughty.suspended = not naughty.suspended
    assert(naughty.is_suspended())
    assert(naughty.suspended)
    naughty.suspended = not naughty.suspended
    assert(not naughty.is_suspended())
    assert(not naughty.suspended)

    -- Replace the text
    assert(icon_requests[n])
    assert(n.title   == "foo")
    assert(n.message == "bar")
    assert(n.text    == "bar")
    local width, height = n.width, n.height
    assert(width)
    assert(height)
    naughty.replace_text(n, "foobar", "baz")
    assert(n.title   == "foobar")
    assert(n.message == "baz")
    assert(n.text    == "baz")
    assert(n.width > width)
    assert(n.height == height)
    width, height = n.width, n.height
    naughty.replace_text(n, "foo", "bar\nbar")
    assert(n.title   == "foo")
    assert(n.message == "bar\nbar")
    assert(n.text    == "bar\nbar")
    assert(n.width < width)
    assert(n.height > height)
    width, height = n.width, n.height
    naughty.replace_text(n, "foo", "bar")
    assert(n.title   == "foo")
    assert(n.message == "bar")
    assert(n.text    == "bar")
    assert(n.width == width)
    assert(n.height < height)

    -- Test the ID system
    local id = n.id
    assert(naughty.getById(id) == n)
    assert(naughty.get_by_id(id) == n)
    assert(naughty.getById(42) ~= n)
    assert(naughty.get_by_id(42) ~= n)
    assert(naughty.get_by_id(9999) == nil)

    -- The timeout
    local real, called = n.reset_timeout, false
    n.reset_timeout = function(...)
        called = true
        return real(...)
    end
    naughty.reset_timeout(n, 1337)
    assert(called)
    assert(n.timer and n.timer.started)

    -- Destroy using the old API
    local old_count = #destroyed
    naughty.destroy(n)
    assert(old_count == #destroyed - 1)

    -- Destroy using the old API, while suspended
    local n2 = naughty.notification {
        title   = "foo",
        message = "bar",
        timeout = 25000,
    }
    naughty.suspended = true
    naughty.destroy(n2)
    assert(old_count == #destroyed - 2)
    naughty.suspended = false

    -- The old notify function and "text" instead of "message"
    n = naughty.notify { text = "foo" }
    assert(n.message == "foo")
    assert(n.text    == "foo")

    -- Calling `naughty.notify` with replace_id.
    n2 = naughty.notify {
        replaces_id = n.id,
        message     = "bar",
        title       = "foo",
    }

    assert(n         == n2   )
    assert(n.message == "bar")
    assert(n.text    == "bar")
    assert(n.title   == "foo")

    -- Finish by testing disconnect_signal
    naughty.disconnect_signal("destroyed", destroyed_callback)
    naughty.disconnect_signal("added", added_callback)

    return true
end)

-- Add a "new API" handler.
local current_template, had_error, handler_called = nil

table.insert(steps, function()
    assert(naughty.has_display_handler == false)

    naughty.connect_signal("request::display", function(n)
        handler_called = true

        naughty.layout.box {
            notification    = n,
            widget_template = current_template
        }
    end)

    return true
end)

-- Check that the various request::icon work.
table.insert(steps, function()
    local gsurface = require("gears.surface")

    local ls = gsurface.load_uncached_silently

    function gsurface.load_uncached_silently(input)
        return {
            input      = input,
            get_height = function() return 1 end,
            get_width  = function() return 1 end,
        }
    end

    local fr, gc = gfs.file_readable, naughty.notification.get_clients

    local mocked_client = {
        type = "normal",
        icon = "42",
    }

    function naughty.notification.get_clients()
        return {mocked_client}
    end

    function gfs.file_readable() return true end

    local n = naughty.notification {
        app_icon = "file:///one%20two"
    }

    assert(type(n.icon) == "table" and n.icon.input == "/one two")

    n:destroy()

    local n2 = naughty.notification {
        title = "foo"
    }

    assert(type(n2.icon) == "table" and n2.icon.input == "42")

    n2:destroy()

    -- Restore the real methods.
    gsurface.load_uncached_silently  = ls
    naughty.notification.get_clients = gc
    gfs.file_readable                = fr

    return true
end)

-- Make sure the legacy popup is used when the new APIs fails.
table.insert(steps, function()
    assert(naughty.has_display_handler == true)

    function gpcall._error_handler()
        had_error = true
    end

    local n = naughty.notification {
        title   = nil,
        message = nil,
        timeout = 25000,
    }

    assert(handler_called)
    assert(not had_error)
    assert(not n._private.widget_template_failed)
    assert(not n.box)

    -- Check adding messages later
    n.title   = "foo"
    n.message = "bar"
    n.icon    = big_icon

    n:destroy()
    handler_called = false

    -- Try with a broken template.
    current_template = {widget = function() assert(false) end}

    n = naughty.notification {
        title   = "foo",
        message = "bar",
        timeout = 25000,
    }

    assert(handler_called)
    assert(had_error)
    assert(not n.box)

    -- Check changing existing content.
    n.title   = "bar"
    n.message = "foo"
    n.icon    = big_icon
    n.image   = small_icon

    handler_called = false
    had_error      = false

    -- Break the default template
    assert(dwidget.widget)
    dwidget.widget = nil
    dwidget.layout = function() assert(false) end
    table.remove(dwidget, 1)

    n = naughty.notification {
        title   = "foo",
        message = "bar",
        timeout = 25000,
    }

    assert(handler_called)
    assert(n._private.widget_template_failed)
    assert(had_error)
    assert(n.box)

    return true
end)

-- Make sure it isn't possible to remove default variables (#3145).
table.insert(steps, function()
    naughty.config.defaults = {fake_variable = 24}
    naughty.config.text = 1337
    assert(naughty.config.defaults.fake_variable == 24)
    assert(naughty.config.defaults.timeout == 5)
    assert(naughty.config.text == 1337)

     return true
end)

require("_runner").run_steps(steps)
