---------------------------------------------------------------------------
-- @author Alex Belykh
-- @copyright 2021 Alex Belykh
---------------------------------------------------------------------------

-- Require test_utils for the assert.widget_fit() helper.
require("wibox.test_utils")
-- Set emit_signal so as to not die on deprecation warnings.
_G.awesome.emit_signal = function() end
-- Silence debug warnings.
require("gears.debug").print_warning = function() end

local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local color = require("gears.color")
local graph = require("wibox.widget.graph")

local deprecated_properties = {
    height = true,
    width = true,
    stack_colors = true,
}

local property_defaults = {
    baseline_value = 0,
    clamp_bars = true,
    nan_indication = true,
    step_width = 1,
    step_spacing = 0,
}

local redrawless_properties = {
    capacity = true,
    height = true,
    width = true,
    stack_colors = true,
}

local data = {45.5, -44.5, -7.5, 1.5, 47.5, -38.5, 0.5, 38.0, 47.0, 23.5}
local data2 = {-26.5, 25.0, -19.0, 38.0, 12.0 -19.0, -35.0, 16.5}

local function push_data(widget, d, group_idx)
    -- Add in reverse, so that d could be compared with
    -- the backing value array directly.
    for i = #d,1,-1 do
        widget:add_value(d[i], group_idx)
    end
end

describe("wibox.widget.graph", function()
    local widget
    local redraw_needed, layout_changed

    before_each(function()
        widget = graph()

        widget:connect_signal("widget::redraw_needed", function()
            redraw_needed = redraw_needed + 1
        end)
        widget:connect_signal("widget::layout_changed", function()
            layout_changed = layout_changed + 1
        end)
        redraw_needed, layout_changed = 0, 0
    end)

    -- Check the trivial properties of all getters and setters.
    --
    -- There shouldn't be a field with a "get_/set_*" name, that isn't one.
    -- Iteration is over module fields, because fields of the instance can be
    -- polluted by inheritance and whatnot with something, that doesn't
    -- hold itself to the standard I'm setting here.
    for field, _ in pairs(graph) do

        if string.sub(field, 1, 4) == "get_" then
            -- A field with a getter-like name. Let's ensure that it is one.
            local prop_name = string.sub(field, 5)

            describe("field " .. field, function()
                it("has a corresponding set_" .. prop_name .. " counterpart", function()
                    assert.is_function(widget["set_" .. prop_name])
                end)

                it("is a property getter", function()
                    assert.is_function(widget[field])
                    stub(widget, field, 3456)
                    -- Access the property through metatable magic
                    local result_value = widget[prop_name]

                    assert.is.equal(3456, result_value)
                    assert.stub(widget[field]).was_called_with(widget)
                end)
            end)

        elseif string.sub(field, 1, 4) == "set_" then
            -- A field with a setter-like name. Let's ensure that it is one.
            local prop_name = string.sub(field, 5)

            describe("field " .. field, function()
                local property_signal_emitted, property_signal_emitted_with

                before_each(function()
                    widget:connect_signal("property::" .. prop_name, function(...)
                        property_signal_emitted = property_signal_emitted + 1
                        property_signal_emitted_with = {...}
                    end)
                    property_signal_emitted, property_signal_emitted_with = 0, nil
                end)

                it("has a corresponding get_" .. prop_name .. " counterpart", function()
                    assert.is_function(widget["get_" .. prop_name])
                end)

                it("is a property setter", function()
                    assert.is_function(widget[field])
                    assert.is.equal(0, redraw_needed)
                    assert.is.equal(0, layout_changed)
                    assert.is.equal(0, property_signal_emitted)

                    local s = spy.on(widget, field)

                    -- An access through metatable magic
                    widget[prop_name] = 3456

                    -- should have caused the call of the method
                    assert.spy(s).was_called_with(match.is_ref(widget), 3456)

                    -- and maybe a property::<prop_name> signal
                    assert.is.equal(deprecated_properties[prop_name] and 0 or 1, property_signal_emitted)
                    -- and maybe a redraw
                    assert.is.equal(redrawless_properties[prop_name] and 0 or 1, redraw_needed)
                    -- but never a layout change.
                    assert.is.equal(0, layout_changed)

                    if not deprecated_properties[prop_name] then
                        -- The property signal contains the new value.
                        assert.is.equal(widget, property_signal_emitted_with[1])
                        assert.is.equal(3456, property_signal_emitted_with[2])
                        -- What is set, can be gotten back through the prop_name field.
                        assert.is.equal(3456, widget[prop_name])
                        -- The setter returns the widget itself for call chaining.
                        assert.spy(s).returned_with(match.is_ref(widget))
                    end

                    s:clear()
                    assert.spy(s).was_not.called()

                    -- A repeated setting of the same value
                    widget[prop_name] = 3456
                    -- should have caused the call of the method again
                    assert.spy(s).was_called_with(match.is_ref(widget), 3456)
                    -- but none of the signals.
                    assert.is.equal(deprecated_properties[prop_name] and 0 or 1, property_signal_emitted)
                    assert.is.equal(redrawless_properties[prop_name] and 0 or 1, redraw_needed)
                    assert.is.equal(0, layout_changed)
                end)

                it("has a specific default value", function()
                    assert.is_equal(property_defaults[prop_name], widget[prop_name])
                end)

                it("is not magical on nil-s", function()
                    -- When set to nil
                    widget[prop_name] = nil
                    -- it should stay nil and not fall back to some "default".
                    assert.is.equal(nil, widget[prop_name])
                end)
            end) -- end describe(field)

        end -- end if
    end -- end field loop

    -- Now let's check some nontrivial behavior

    describe("values", function()
        it("are empty in a fresh instance", function()
            assert.is.same({}, widget._private.values)
        end)

        describe("method add_value()", function()
            it("adds values", function()
                push_data(widget, data)
                -- Adds into the first datagroup by default.
                assert.is.same({data}, widget._private.values)
            end)

            it("defaults to NaN when no/falsy value is supplied", function()
                local amount = 15
                for _ = 1, amount do
                    widget:add_value(false)
                    widget:add_value()
                    widget:add_value(false, 3)
                    widget:add_value(nil, 3)
                end

                assert.array(widget._private.values).has.no.holes()
                assert.is.equal(3, #widget._private.values)
                assert.is.equal(2*amount, #widget._private.values[1])
                assert.is.equal(0, #widget._private.values[2])
                assert.is.equal(2*amount, #widget._private.values[3])

                for i = 1, 2*amount do
                    local tmp = widget._private.values[1][i]
                    assert.is_not.equal(tmp, tmp)
                    tmp = widget._private.values[3][i]
                    assert.is_not.equal(tmp, tmp)
                end
            end)

            it("adds values into specific data group", function()
                push_data(widget, data, 15)
                assert.is.same(data, widget._private.values[15])

                -- Smaller datagroups are present too, but empty.
                assert.array(widget._private.values).has.no.holes()
                assert.is.equal(15, #widget._private.values)
                for i, data_group in ipairs(widget._private.values) do
                    assert.array(data_group).has.no.holes()
                    assert.is.equal(i ~= 15 and 0 or #data, #data_group)
                end

                -- Adding again to a different group
                push_data(widget, data2, 30)
                -- works
                assert.is.same(data2, widget._private.values[30])
                -- and doesn't affect the other group.
                assert.is.same(data, widget._private.values[15])

                -- Smaller-index datagroups are present but empty.
                assert.array(widget._private.values).has.no.holes()
                assert.is.equal(30, #widget._private.values)
                for i, data_group in ipairs(widget._private.values) do
                    assert.array(data_group).has.no.holes()
                    if i ~= 15 and i ~= 30 then
                        assert.is.same({}, data_group)
                    end
                end
            end)

            it("doesn't care about group order", function()
                for i = math.max(#data,#data2),1,-1 do
                    if i % 2 == 0 and data[i] then
                        widget:add_value(data[i], 1)
                    end
                    if data2[i] then
                     widget:add_value(data2[i], 2)
                    end
                    if i % 2 == 1 and data[i] then
                        widget:add_value(data[i], 1)
                    end
                end

                assert.is.same({data, data2}, widget._private.values)
            end)

            it("doesn't work with non-natural datagroups", function()
                for i = #data,1,-1 do
                    assert.has.errors(function()
                        widget:add_value(data[i], 14.5)
                    end)
                end
                for i = #data,1,-1 do
                    assert.has.errors(function()
                        widget:add_value(data[i], 0)
                    end)
                end
                for i = #data,1,-1 do
                    assert.has.errors(function()
                        widget:add_value(data[i], -4)
                    end)
                end
                for i = #data,1,-1 do
                    assert.has.errors(function()
                        widget:add_value(data[i], "index")
                    end)
                end
                for i = #data,1,-1 do
                    assert.has.errors(function()
                        widget:add_value(data[i], {1, 2, 3})
                    end)
                end
                for i = #data,1,-1 do
                    assert.has.errors(function()
                        widget:add_value(data[i], function() end)
                    end)
                end

                -- No values were added after all this,
                -- but some empty datagroups were. This is a bit suboptimal,
                -- but is not worth fixing. Adding an assert here
                -- so that one would be reminded to change it to
                -- a #values == 0 check, if one fixes it.
                assert.is.equal(14, #widget._private.values)
                assert.array(widget._private.values).has.no.holes()
                for _, data_group in ipairs(widget._private.values) do
                    assert.is.same({}, data_group)
                end
            end)

            it("honors the capacity property", function()
                local function check_cap(cap, expected_len)
                    expected_len = expected_len or math.max(0, math.ceil(cap))

                    widget.capacity = cap

                    push_data(widget, data, 1)
                    push_data(widget, data2, 2)

                    -- Only the last inserted elements are kept.
                    assert.is.same(
                        {
                           {unpack(data, 1, expected_len)},
                           {unpack(data2, 1, expected_len)}
                        },
                        widget._private.values
                    )
                end

                for i = 0, math.min(#data, #data2) do
                    check_cap(i)
                end
                -- It handles negatives and NaNs like zeros
                check_cap(-100, 0)
                check_cap(-100.5, 0)
                check_cap(0/0)
                -- And fractional numbers are rounded up
                check_cap(2.5, 3)

                -- But setting the capacity property by itself doesn't do anything,
                -- if add_value() wasn't called.
                widget.capacity = 1
                assert.is.equal(3, #widget._private.values[1])
                assert.is.equal(3, #widget._private.values[2])
            end)

            it("stores up to 8192 values when no usage stats are available", function()
                assert.is_nil(widget.capacity)
                assert.is_nil(widget._private.last_drawn_values_num)
                for i = 1, 9000 do
                    widget:add_value(i)
                    widget:add_value(i, 3)
                end
                assert.is.equal(8192, #widget._private.values[1])
                assert.is.equal(8192, #widget._private.values[3])
            end)

            it("relies on usage stats, when capacity is unset", function()
                assert.is_nil(widget.capacity)
                widget._private.last_drawn_values_num = 100

                for i = 1, 400 do
                    widget:add_value(i)
                    widget:add_value(i, 3)
                end

                -- The smallest multiple of 64 that is >= last_drawn_values_num + 64
                assert.is.equal(192, #widget._private.values[1])
                assert.is.equal(192, #widget._private.values[3])

                -- so 192 elements will be kept with this too.
                widget._private.last_drawn_values_num = 128

                widget:add_value(0)
                widget:add_value(0, 3)

                assert.is.equal(192, #widget._private.values[1])
                assert.is.equal(192, #widget._private.values[3])

                -- But this is one is already one too many.
                widget._private.last_drawn_values_num = 129

                for i = 1, 400 do
                    widget:add_value(i)
                    widget:add_value(i, 3)
                end

                assert.is.equal(256, #widget._private.values[1])
                assert.is.equal(256, #widget._private.values[3])

                -- Setting it back and calling add_value() once is enough
                -- to purge overflowing elements,
                widget._private.last_drawn_values_num = 128
                widget:add_value(0)
                assert.is.equal(192, #widget._private.values[1])
                -- but only in the group, for which add_value() happened to be
                -- called during the time last_drawn_values_num was small enough,
                -- even though it's probably not a very fair behavior and could
                -- lead to weird visual artefacts.
                assert.is.equal(256, #widget._private.values[3])

                -- Calling add_value for the other group puts it in line too.
                widget:add_value(0, 3)
                assert.is.equal(192, #widget._private.values[3])
            end)


        end) -- end describe(add_value)

        describe("method clear()", function()
            it("clears values", function()
                local function check_clear(i)
                    assert.is.same({}, widget._private.values)
                    push_data(widget, data)
                    assert.is.same({data}, widget._private.values)
                    widget:clear()
                    assert.is.same({}, widget._private.values)
                    push_data(widget, data2, 3*i)
                    assert.is.same(data2, widget._private.values[3*i])
                    widget:clear()
                    assert.is.same({}, widget._private.values)
                end

                for i = 1, 3 do
                    check_clear(i)
                end
            end)
        end) -- end describe(clear)

    end) -- end describe(values)

    describe("method fit()", function()
        it("requests all available space", function()
            assert.widget_fit(widget, {120, 80}, {120, 80})
            assert.widget_fit(widget, {1300, 700}, {1300, 700})
        end)
    end)

    describe("drawing-related", function()
        local dimensions
        local context
        local cr
        before_each(function()
            dimensions = {120, 80}
            context = { fake_context = "fake context" }
            cr = setmetatable({"fake cairo"}, {__index = function() return function() end end})
            -- assert.widget_fit(widget, dimensions, dimensions)
        end)

        describe("property step_shape()", function()
            it("is not called when there are no values", function()
                widget.step_shape = spy.new()

                widget:draw(context, cr, unpack(dimensions))
                widget:draw(context, cr, unpack(dimensions))

                -- No values to draw, so no shapes drawn
                assert.spy(widget.step_shape).was_not_called()
            end)

            it("is called to draw values", function()
                widget.step_shape = spy.new()

                -- It is called once for each value.
                push_data(widget, data)
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called(#data)

                -- All data is drawn despite the gap between data groups
                push_data(widget, data2, 3)
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called(2*#data + #data2)
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called(3*#data + 2*#data2)
            end)

            it("receives proper arguments from draw()", function()
                widget.step_shape = spy.new()
                push_data(widget, data)
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called_with(
                    cr, -- cairo
                    match.is_number(), -- width
                    match.is_number() -- height
                )
            end)

            it("doesn't receive NaNs from draw()", function()
                widget.step_shape = spy.new(function(_, width, height)
                    -- These are never NaNs.
                    assert.is_equal(width, width)
                    assert.is_equal(height, height)
                end)

                -- When there are no NaNs in data, there are no NaNs.
                push_data(widget, data)
                push_data(widget, data2, 3)
                widget:draw(context, cr, unpack(dimensions))
                -- Write out arguments in full once so that one doesn't
                -- forget to update this test if arguments get changed.
                assert.spy(widget.step_shape).was_called_with(
                    cr, match.is_number(), match.is_number()
                )
                widget.step_shape:clear()

                -- No matter where the NaNs are,
                widget:add_value(0/0, 1)
                widget:add_value(0/0, 2)
                widget:add_value(0/0, 3)
                widget:add_value(0/0, 4)
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called()
                widget.step_shape:clear()

                -- even if there's nothing but NaNs, we get them not,
                widget:clear()
                for _ = 1, 100 do
                    widget:add_value(0/0, 1)
                    widget:add_value(0/0, 2)
                    widget:add_value(0/0, 3)
                end
                widget:draw(context, cr, unpack(dimensions))
                -- in fact we don't even get called.
                assert.spy(widget.step_shape).was_not_called()
                widget.step_shape:clear()
            end)

            it("'s errors are not silenced by draw()", function()
                widget.step_shape = spy.new(function() assert(false) end)
                push_data(widget, data)

                assert.has.errors(function()
                   widget:draw(context, cr, unpack(dimensions))
                end)
                assert.spy(widget.step_shape).was_called(1)
            end)

            it("is disabled by falsy group_colors", function()
                -- TODO: if should_draw_data_group() gets decided on,
                -- this should be rewritten in terms of it and
                -- should_draw_data_group() should be tested by colors in turn.
                widget.step_shape = spy.new()
                widget.group_colors = { "#feedf00d", "#deadbeef", "#0badcafe" }
                push_data(widget, data, 1)
                push_data(widget, data, 2)
                push_data(widget, data2, 3)
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called(2*#data + #data2)
                widget.step_shape:clear()
                widget.group_colors = { nil, false, "#feedf00d" }
                widget:draw(context, cr, unpack(dimensions))
                assert.spy(widget.step_shape).was_called(#data2)
            end)
        end) -- end describe(step_shape)

        describe("method compute_drawn_values_num()", function()
            it("'s default implementation computes things correctly", function()
                local function cdvn(param)
                    return widget:compute_drawn_values_num(param)
                end

                -- Negative and zero values don't make it return negatives.
                -- Default settings, border_width=step_spacing=0, step_width=1.
                assert.is.equal(0, cdvn(-5))
                assert.is.equal(0, cdvn(0))
                assert.is.equal(10, cdvn(10))
                assert.is.equal(11, cdvn(10.5))
                -- border_width doesn't affect anything, because
                -- the function assumes that it was subtracted already.
                widget.border_width = 2
                assert.is.equal(0, cdvn(-50.5))
                assert.is.equal(0, cdvn(0))
                assert.is.equal(9, cdvn(9))
                assert.is.equal(10, cdvn(10))
                assert.is.equal(11, cdvn(10.5))
                -- All of this is just how you'd expect it to be for
                -- math.ceil(width/(step_width + step_spacing), which it is.
                widget.step_width = 2
                assert.is.equal(0, cdvn(-100))
                assert.is.equal(0, cdvn(0))
                assert.is.equal(4, cdvn(8))
                assert.is.equal(5, cdvn(9))
                assert.is.equal(5, cdvn(10))
                assert.is.equal(6, cdvn(11))
                assert.is.equal(6, cdvn(12))
                widget.step_spacing = 2
                assert.is.equal(0, cdvn(-100))
                assert.is.equal(0, cdvn(0))
                assert.is.equal(2, cdvn(8))
                assert.is.equal(3, cdvn(9))
                assert.is.equal(3, cdvn(10))
                assert.is.equal(3, cdvn(11))
                assert.is.equal(3, cdvn(12))
                assert.is.equal(4, cdvn(13))
            end)

            it("is called by draw() once", function()
                local s = spy.on(widget, "compute_drawn_values_num")

                widget:draw(context, cr, unpack(dimensions))

                assert.spy(s).was_called(1)
                assert.spy(s).was_called_with(match.is_ref(widget), dimensions[1])
            end)

            it("'s result is stored by draw() in last_drawn_values_num", function()
                local our_value = 10
                local function cdvn()
                    return our_value
                end
                widget.compute_drawn_values_num = cdvn

                -- The usage stats variable is uninitialized before first call.
                assert.is_nil(widget._private.last_drawn_values_num)

                widget:draw(context, cr, unpack(dimensions))
                -- Now it should be initialized.
                assert.is.equal(our_value, widget._private.last_drawn_values_num)

                -- Repeated calls with increasing values should set it instantly.
                our_value = 20
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(our_value, widget._private.last_drawn_values_num)
                our_value = 35
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(our_value, widget._private.last_drawn_values_num)
                our_value = 50
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(our_value, widget._private.last_drawn_values_num)
                -- Same values keep it on the same level too.
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(our_value, widget._private.last_drawn_values_num)

                -- Calls with smaller values should only decrement it by one.
                our_value = 30
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(49, widget._private.last_drawn_values_num)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(48, widget._private.last_drawn_values_num)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(47, widget._private.last_drawn_values_num)

                -- No matter how small the values are.
                our_value = 0
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(46, widget._private.last_drawn_values_num)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(45, widget._private.last_drawn_values_num)

                -- Negatives and NaNs are outright ignored.
                our_value = -100
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(45, widget._private.last_drawn_values_num)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(45, widget._private.last_drawn_values_num)
                our_value = 0/0
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(45, widget._private.last_drawn_values_num)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(45, widget._private.last_drawn_values_num)

                -- Positive infinity will fry the widget forever though.
                our_value = math.huge
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(math.huge, widget._private.last_drawn_values_num)
                our_value = 15
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(math.huge, widget._private.last_drawn_values_num)
            end)

        end) -- end describe(computed_drawn_values_num)

        describe("colors", function()
            local magic_color
            local magic_color_used
            local current_cr_source
            before_each(function()
                cr.set_source = function(_, src)
                    if src == magic_color then
                        magic_color_used = magic_color_used + 1
                    end
                    current_cr_source = src
                end
                magic_color, magic_color_used = color("#deadc0de"), 0
                current_cr_source = nil
            end)

            it("aren't used, when unset", function()
                -- Let's check that our magic color isn't some default color
                -- in a fresh widget
                assert.is.equal(0, magic_color_used)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(0, magic_color_used)
                -- even after pushing data.
                push_data(widget, data, 4)
                widget:draw(context, cr, unpack(dimensions))
                assert.is.equal(0, magic_color_used)
            end)

            describe("property color", function()
                -- These tests overlap with pick_data_group_color() tests,
                -- but are added for completeness.

                it("is used by draw() when set", function()
                    assert.is.equal(0, magic_color_used)
                    widget.color = magic_color
                    -- Not used, if there are no values to draw.
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                    -- Used, if there are, once for each data group.
                    push_data(widget, data, 4)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(4, magic_color_used)
                end)

                it("is overridden by group_colors property", function()
                    assert.is.equal(0, magic_color_used)
                    widget.color = magic_color
                    widget.group_colors = { "#feedf00d" }
                    push_data(widget, data, 1)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                end)
            end) -- end describe(color)

            describe("property group_colors", function()
                -- These tests overlap with pick_data_group_color() tests,
                -- but are added for completeness.

                it("is used by draw() when set", function()
                    assert.is.equal(0, magic_color_used)
                    widget.group_colors = { "#feedfood", false, magic_color, magic_color }
                    -- Not used, if there are no values to draw.
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                    -- Not used, if there are no values in its group.
                    push_data(widget, data, 1)
                    push_data(widget, data2, 2)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                     -- Used, if there are, once for each data group.
                    push_data(widget, data, 3)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(1, magic_color_used)
                    push_data(widget, data2, 4)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(3, magic_color_used)
                end)
            end) -- end describe(group_colors)

            describe("property background_color", function()
                it("is used by draw() when set", function()
                    assert.is.equal(0, magic_color_used)
                    widget.background_color = magic_color
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(1, magic_color_used)
                end)
            end) -- end describe(background_color)

            describe("property border_color", function()
                it("is used by draw() when set", function()
                    assert.is.equal(0, magic_color_used)
                    widget.border_color = magic_color

                    -- Not used, when border_width <= 0, so not by default either.
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                    widget.border_width = 0
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                    widget.border_width = -1
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)

                    -- Anything positive enables the border.
                    widget.border_width = 0.000001
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(1, magic_color_used)
                end)
            end) -- end describe(border_color)

            describe("property nan_color", function()
                it("is good to go by default", function()
                    assert.is.truthy(widget.nan_indication)
                    -- TODO: maybe expose default_nan_color and check that it's used.
                end)

                it("is used by draw() when set", function()
                    assert.is.equal(0, magic_color_used)
                    widget.nan_color = magic_color
                    push_data(widget, data, 4)

                    -- Not used, when there are no NaNs.
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)

                    -- Is used once when a NaN is found, no matter how many
                    -- NaNs and groups there are.
                    widget:add_value(0/0, 4)
                    widget:add_value(0/0, 4)
                    widget:add_value(0/0, 5)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(1, magic_color_used)

                    -- But not when nan_indication is disabled.
                    widget.nan_indication = false
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(1, magic_color_used)
                end)
            end) -- end describe(nan_color)

            describe("method pick_data_group_color()", function()
                it("is called by draw() for each data group", function()
                    widget.pick_data_group_color = spy.new()
                    -- Not used, when there are no data groups.
                    widget:draw(context, cr, unpack(dimensions))
                    assert.spy(widget.pick_data_group_color).was_not.called()
                    -- Used once for each data group (even empty ones, as it
                    -- happens, though that is not really necessary).
                    -- But I'm testing for it here too, so that possible
                    -- future behavior change can be detected, and it can
                    -- be decided, if it's ok to break.
                    push_data(widget, data, 1)
                    push_data(widget, data2, 3)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.spy(widget.pick_data_group_color).was_called(3)
                    assert.spy(widget.pick_data_group_color).was_called_with(match.is_ref(widget), 1)
                    assert.spy(widget.pick_data_group_color).was_called_with(match.is_ref(widget), 2)
                    assert.spy(widget.pick_data_group_color).was_called_with(match.is_ref(widget), 3)
                end)

                it("is used by draw() to pick colors", function()
                    local s = spy.new(function(_, data_group)
                        return data_group > 2 and magic_color or "#feedf00d"
                    end)
                    widget.pick_data_group_color = s

                    -- Not used, if there are no values to draw.
                    assert.is.equal(0, magic_color_used)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                    assert.spy(s).was_not_called()
                    -- Colors it returns are used for respective data groups,
                    -- once for each data group.
                    push_data(widget, data, 1)
                    push_data(widget, data2, 2)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(0, magic_color_used)
                    assert.spy(s).was_called(2)
                    push_data(widget, data, 3)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(1, magic_color_used)
                    assert.spy(s).was_called(5)
                    push_data(widget, data2, 4)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(3, magic_color_used)
                    assert.spy(s).was_called(9)
                    s:clear()
                end)

                it("'s returned color is set early, so as to be available in the corresponding step_shape()", function()
                    local current_data_group
                    widget.pick_data_group_color = function(_, data_group)
                        current_data_group = data_group
                        return (data_group % 2 == 1) and magic_color or "#feedf00d"
                    end
                    widget.step_shape = function()
                        if current_data_group % 2 == 1 then
                            assert.is.equal(magic_color, current_cr_source)
                        else
                            assert.is_not.equal(magic_color, current_cr_source)
                        end
                    end
                    assert.is.equal(0, magic_color_used)
                    assert.is_nil(current_cr_source)
                    push_data(widget, data, 1)
                    push_data(widget, data2, 2)
                    push_data(widget, data2, 3)
                    push_data(widget, data, 4)
                    widget:draw(context, cr, unpack(dimensions))
                    assert.is.equal(2, magic_color_used)
                end)

                it("returns some valid color even in a fresh widget", function()
                    local groups = {1, 2, 50, 0, -1}
                    local colors = {}
                    for i, group in ipairs(groups) do
                        colors[i] = widget:pick_data_group_color(group)
                        assert.is.truthy(colors[i])
                    end
                    -- Regardless of data presence.
                    push_data(widget, data, 2)
                    local colors2 = {}
                    for i, group in ipairs(groups) do
                        colors2[i] = widget:pick_data_group_color(group)
                        assert.is.truthy(colors2[i])
                    end
                    assert.is.same(colors, colors2)
                    -- And it's actually the same default color in all cases.
                    for i = 2, #colors do
                        assert.is.equal(colors[i-1], colors[i])
                    end
                    -- And gears.color() doesn't choke on it.
                    assert.is.truthy(color(colors[1]))
                    -- Because it's red, tbh.
                    assert.is.equal("#ff0000", colors[1])
                end)

                it("uses color and group_colors properties", function()
                    local groups = {1, 2, 5, 3, 4, 0, 6, -1}
                    for _, group in ipairs(groups) do
                        assert.is_not.equal(magic_color, widget:pick_data_group_color(group))
                    end
                    widget.color = magic_color
                    for _, group in ipairs(groups) do
                        assert.is.equal(magic_color, widget:pick_data_group_color(group))
                    end
                    -- But group_colors override color.
                    local group_colors = { "#feedf00d", "#deadbeef", false, nil, "#0badcafe" }
                    widget.group_colors = {unpack(group_colors, 1, 5)}
                    for _, group in ipairs(groups) do
                        if group_colors[group] then
                            assert.is.equal(group_colors[group], widget:pick_data_group_color(group))
                        else
                            -- But not when the color is falsy in the table group_colors.
                            assert.is.equal(magic_color, widget:pick_data_group_color(group))
                        end
                    end
                end)
             end) -- end describe(pick_data_group_color)

        end) -- end describe(colors)

    end) -- end describe(drawing-related)

end) -- end describe(graph)

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
