-- This testsuite intends to tests the delayed screen creation mode and it's
-- automatic fallback when no screens are created.

local runner = require("_runner")
local ascreen = require("awful.screen")
local gtable = require("gears.table")
local grect = require("gears.geometry").rectangle

local xft_dpi = 42

local fake_viewports, max_id, allow_zero = screen._viewports(), 1, false
local called = 0

local saved_dpi1, saved_dpi2, saved_dpi3

-- Make sure what is tested matches some criteria.
-- Prevents silly errors when adding more test case.
local function validate_viewports(viewports)
    for _, a in ipairs(viewports) do
        assert(type(a.id) == "number")
        assert(a.id > max_id)
        assert(type(a.outputs ) == "table")
        assert(type(a.geometry) == "table")

        for _, prop in ipairs {"x", "y", "width", "height"} do
            assert(type(a.geometry[prop]) == "number")
            assert(a.geometry[prop] >= 0)
        end

        max_id = a.id
    end
end

local function fake_replace(viewports)
    fake_viewports = viewports
    screen.emit_signal("property::_viewports", fake_viewports)
end

-- Rollback the clock and replay the initialization.
local function fake_replay(viewports)
    if viewports then
        validate_viewports(viewports)
    end

    viewports = viewports or fake_viewports

    ascreen._viewports, fake_viewports = {}, viewports

    while screen.count() > 0 do
        screen[1]:fake_remove()
    end

    assert(screen.count() == 0)

    screen.emit_signal("scanning")
    screen.emit_signal("property::_viewports", fake_viewports)
    screen.emit_signal("scanned")
    assert(screen.count() == #viewports)

    assert(type(ascreen._get_xft_dpi()) ~= "boolean")

    -- Check of the viewports (+new metadata) is created.
    for k, a in ipairs(viewports) do
        local s = screen[k]

        -- Check if the extra metadata are computed.
        assert(s._private.viewport.maximum_dpi)
        assert(s._private.viewport.minimum_dpi)
        assert(s._private.viewport.preferred_dpi)
        assert(s.dpi)
        assert(s.maximum_dpi   == s._private.viewport.maximum_dpi  )
        assert(s.minimum_dpi   == s._private.viewport.minimum_dpi  )
        assert(s.preferred_dpi == s._private.viewport.preferred_dpi)

        -- Make sure it enters either the main `if` or the fallback one.
        assert(s._private.viewport.minimum_dpi   ~= math.huge)
        assert(s._private.viewport.preferred_dpi ~= math.huge)

        -- Check the range.
        assert(s._private.viewport.maximum_dpi   >= s._private.viewport.minimum_dpi)
        assert(s._private.viewport.preferred_dpi >= s._private.viewport.minimum_dpi)
        assert(s._private.viewport.preferred_dpi <= s._private.viewport.maximum_dpi)

        -- Check if the screen was created using the right viewport
        -- (order *is* relevant).
        assert(#s._private.viewport.outputs == #a.outputs)
        assert(s._private.viewport and s._private.viewport.id == a.id)
    end

    -- The original shall not be modified, the CAPI for this is read-only.
    for _, a in ipairs(fake_viewports) do
        assert(not a.minimum_dpi  )
        assert(not a.maximum_dpi  )
        assert(not a.preferred_dpi)
        assert(not a.dpi          )
    end
end

local function remove_output(viewport, name)
    for k, output in ipairs(viewport.outputs) do
        if output.name == name then
            table.remove(viewport.outputs, k)
            screen.emit_signal("property::_viewports", fake_viewports)
            return
        end
    end
    assert(false)
end

local function add_output(viewport, output)
    table.insert(viewport.outputs, output)
    screen.emit_signal("property::_viewports", fake_viewports)
end

local function remove_viewport(id)
    for k, a in ipairs(fake_viewports) do
        if a.id == id then
            table.remove(fake_viewports, k)
            screen.emit_signal("property::_viewports", fake_viewports)
            return
        end
    end
    assert(false)
end

local function add_viewport(viewport)
    assert(viewport.id)
    assert(viewport.geometry)
    assert(viewport.outputs)
    table.insert(fake_viewports, viewport)
    screen.emit_signal("property::_viewports", fake_viewports)
end

local steps = {

-- Monkeypatch for the tests.
function()
    -- This should be true for all tests for.
    assert(not screen.automatic_factory)

    -- Avoids touching the real XRDB, the tests don't control it.
    function ascreen._get_xft_dpi()
        return xft_dpi
    end

    -- This testsuite needs to manipulate this.
    function ascreen._get_viewports()
        local clone = gtable.clone(fake_viewports, true)
        assert(#clone > 0 or allow_zero)
        return clone
    end

    return true
end,

-- Remove all the screens, which is **not** supported, but necessary to play
-- with the initial screen creation code.
function()
    -- Simple scenario, a single screen.
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 2,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "leet", mm_width = 353, mm_height = 11 }},
        },
    }

    -- It should honor the XFT DPI when set.
    assert(xft_dpi == screen[1].dpi)

    -- Now that we know it works, unset it for the other steps.
    xft_dpi = nil

    -- Check if the "normal" fallback works.
    fake_replay()
    assert(screen[1].dpi ~= 42)

    return true
end,

-- Test with multiple outputs.
function()
    -- Simple scenario, a single screen.
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 3,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {
                { name = "leet", mm_width = 353  , mm_height = 11   },
                { name = "leet", mm_width = 353/2, mm_height = 11/2 },
                { name = "leet", mm_width = 353/3, mm_height = 11/3 },
            },
        },
    }

    assert(screen[1].maximum_dpi   == screen[1].minimum_dpi*3)
    assert(screen[1].preferred_dpi == screen[1].minimum_dpi  )

    return true
end,

-- Test with zero outputs.
function()
    -- Simple scenario, a single screen.
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 4,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {},
        },
    }

    assert(screen[1].maximum_dpi   == screen[1].minimum_dpi)
    assert(screen[1].preferred_dpi == screen[1].minimum_dpi)

    return true
end,

-- Test with 2 screens.
function()
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 5,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "0x1ee7", mm_width = 353, mm_height = 11 }},
        },
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 6,
            geometry = { x = 1337, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "3leet", mm_width = 353, mm_height = 11 }},
        },
    }

    saved_dpi1 = screen[1].preferred_dpi

    return true
end,

-- Test with 2 screens with twice the density.
function()
    -- Simple scenario, a single screen.
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 7,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "0x1ee7", mm_width = 353/2, mm_height = 11/2 }},
        },
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 8,
            geometry = { x = 1337, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "3leet", mm_width = 353/3, mm_height = 11/3 }},
        },
    }

    -- Test if scaling works as expected.
    assert(saved_dpi1*2 == screen[1].preferred_dpi)
    assert(saved_dpi1*3 == screen[2].preferred_dpi)

    -- Revert to the previous settings if the DPI happens to accidentally match.
    -- For the following tests, it has to be different from the fallback.
    if screen[1].preferred_dpi == screen[1].dpi then
        fake_replay {
            {
                -- 1 has already been consumed, this makes the test more future proof.
                id       = 9,
                geometry = { x = 0, y = 0, width = 1337, height = 42 },
                outputs  = {{ name = "0x1ee7", mm_width = 353, mm_height = 11 }},
            }
        }
    end

    saved_dpi1 = screen[1].preferred_dpi

    -- The next test requires a different value.
    assert(saved_dpi1 ~= screen[1].dpi)

    -- Enable autodpi.
    ascreen.set_auto_dpi_enabled(true)

    return true
end,

-- Test if autodpi does something.
function()
    -- Replay with the same settings.
    fake_replay()

    -- If this isn't true, then auto-dpi didn't do its job.
    assert(screen[1].dpi == saved_dpi1)

    -- Revert to a single viewport for the next tests.
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 10,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "leet", mm_width = 353, mm_height = 11 }},
        },
    }

    saved_dpi1 = screen[1].dpi
    saved_dpi2 = screen[1].maximum_dpi
    saved_dpi3 = screen[1].minimum_dpi

    return true
end,

-- Test adding an output.
function()
    local viewport = screen[1]._private.viewport

    add_output(fake_viewports[1], {
        name      = "foobar",
        mm_width  = 353/2,
        mm_height = 11/2
    })
    add_output(fake_viewports[1], {
        name      = "foobar2",
        mm_width  = 353*2,
        mm_height = 11*2
    })

    -- It should have been kept.
    assert(viewport == screen[1]._private.viewport)

    -- If this isn't true, then auto-dpi didn't do its job.
    assert(screen[1].dpi ~= saved_dpi1)

    -- Now that there is multiple DPIs for the same viewport, the number
    -- should double.
    assert(#screen[1]._private.viewport.outputs == 3)
    assert(screen[1].maximum_dpi == saved_dpi2*2)
    assert(screen[1].minimum_dpi == saved_dpi3/2)
    assert(screen[1].dpi         == saved_dpi1/2)

    remove_output(fake_viewports[1], "leet")
    assert(#screen[1]._private.viewport.outputs == 2)
    remove_output(fake_viewports[1], "foobar")

    -- At this point, only 1 DPI is left.
    assert(#screen[1]._private.viewport.outputs == 1)
    assert(screen[1].maximum_dpi == saved_dpi1/2)
    assert(screen[1].minimum_dpi == saved_dpi1/2)
    assert(screen[1].dpi         == saved_dpi1/2)

    return true
end,

-- Test adding/removing viewports.
function()
    assert(screen.count() == 1)

    add_viewport {
        id = 11,
        geometry = { x = 1337, y = 0, width = 1337, height = 42 },
        outputs = {{
            name      = "foobar",
            mm_width  = 353/2,
            mm_height = 11/2
        }}
    }

    assert(screen.count() == 2)
    assert(screen[1]._private.viewport.id == 10)
    assert(screen[2]._private.viewport.id == 11)
    assert(grect.are_equal(screen[1]._private.viewport.geometry, screen[1].geometry))
    assert(grect.are_equal(screen[2]._private.viewport.geometry, screen[2].geometry))
    assert(#ascreen._viewports == 2)

    remove_viewport(10)

    assert(#ascreen._viewports == 1)
    assert(screen.count() == 1)
    assert(screen[1]._private.viewport.id == 11)
    assert(grect.are_equal(screen[1]._private.viewport.geometry, screen[1].geometry))

    return true
end,

-- Test resizing.
function()
    local s, sa = screen[1], screen[1]._private.viewport
    assert(screen.count() == 1)
    assert(#ascreen._viewports == 1)

    -- First, to the same size (replace internal screen with external screen).
    -- It may or may not create a different viewport in practice, but it's possible
    -- if the first screen is removed then the new one added in 2 steps.
    fake_replace {
        {
            id = 12,
            geometry = { x = 1337, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "foobar3", mm_width  = 353/2, mm_height = 11/2}}
        }
    }

    assert(screen.count() == 1)
    assert(s == screen[1])
    assert(s._private.viewport ~= sa)
    assert(s._private.viewport.id == 12)

    -- Now 2 smaller (resolution) screens side by side to make sure it doesn't
    -- go haywire with overlapping
    fake_replace {
        {
            id = 13,
            geometry = { x = 1337, y = 0, width = 680, height = 42 },
            outputs  = {{ name = "foobar4", mm_width  = 353/2, mm_height = 11/2}}
        },
        {
            id = 14,
            geometry = { x = 1337+680, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "foobar5", mm_width  = 353/2, mm_height = 11/2}}
        }
    }

    assert(screen.count() == 2)
    assert(s == screen[1])
    assert(s._private.viewport.id == 13)
    assert(s.geometry.x == 1337)
    assert(s.geometry.width == 680)

    -- Disconnect the default handler to test the fallback.
    screen.disconnect_signal("request::create", ascreen.create_screen_handler)
    screen.disconnect_signal("request::remove", ascreen.remove_screen_handler)
    screen.disconnect_signal("request::resize", ascreen.resize_screen_handler)

    return true
end,

-- Test the fallback screen creation.
function()
    fake_replay {
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 15,
            geometry = { x = 0, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "0x1ee7", mm_width = 353, mm_height = 11 }},
        },
        {
            -- 1 has already been consumed, this makes the test more future proof.
            id       = 16,
            geometry = { x = 1337, y = 0, width = 1337, height = 42 },
            outputs  = {{ name = "3leet", mm_width = 353, mm_height = 11 }},
        },
    }

    assert(screen.count() == 2)
    assert(screen[1]._private.viewport.id == 15)
    assert(screen[2]._private.viewport.id == 16)

    -- Connect custom handler and see if the internals accidently depend on
    -- implementation details.
    screen.connect_signal("request::create", function(viewport)
        assert(viewport.minimum_dpi)
        assert(viewport.maximum_dpi)
        assert(viewport.preferred_dpi)
        local geo = viewport.geometry
        local s = screen.fake_add(geo.x, geo.y, geo.width, geo.height)
        s.outputs = viewport.outputs
        called = called + 1
    end)
    screen.connect_signal("request::remove", function(viewport)
        local geo = viewport.geometry
        for s in screen do
            if grect.are_equal(geo, s.geometry) then
                s:fake_remove()
                called = called + 1
                return
            end
        end
    end)

    return true
end,

-- Test if alternate implementations don't explode the internals.
function()
    ascreen._viewports = {}

    while screen.count() > 0 do
        screen[1]:fake_remove()
    end

    assert(screen.count() == 0)

--     screen.emit_signal("scanning")
    screen.emit_signal("property::_viewports", fake_viewports)
    screen.emit_signal("scanned")
    assert(screen.count() == 2)

    assert(grect.are_equal(
        screen[1].geometry, {x = 0, y = 0, width = 1337, height = 42}
    ))
    assert(grect.are_equal(
        screen[2].geometry, {x = 1337, y = 0, width = 1337, height = 42}
    ))

    -- Call the DPI properties just to check it doesn't cause an error)
    assert(screen[1].dpi           or true)
    assert(screen[1].maximum_dpi   or true)
    assert(screen[1].minimum_dpi   or true)
    assert(screen[1].preferred_dpi or true)
    assert(screen[2].dpi           or true)
    assert(screen[2].maximum_dpi   or true)
    assert(screen[2].minimum_dpi   or true)
    assert(screen[2].preferred_dpi or true)
    assert(called == 2)

    -- Try adding an viewport.
    table.insert(fake_viewports, {
        -- 1 has already been consumed, this makes the test more future proof.
        id       = 17,
        geometry = { x = 1337, y = 42, width = 1337, height = 42 },
        outputs  = {{ name = "last", mm_width = 353, mm_height = 11 }},
    })
    screen.emit_signal("property::_viewports", fake_viewports)
    assert(screen.count() == 3)
    assert(called == 3)
    assert(grect.are_equal(
        screen[3].geometry, {x = 1337, y = 42, width = 1337, height = 42}
    ))

    -- Remove the middle one.
    table.remove(fake_viewports, 2)
    screen.emit_signal("property::_viewports", fake_viewports)

    assert(screen.count() == 2)
    assert(called == 4)
    assert(grect.are_equal(
        screen[1].geometry, {x = 0, y = 0, width = 1337, height = 42}
    ))
    assert(grect.are_equal(
        screen[2].geometry, {x = 1337, y = 42, width = 1337, height = 42}
    ))

    return true
end
}

runner.run_steps(steps)
