pcall(require, "luacov")

local Layout = require("nui.layout")
local Popup = require("nui.popup")
local Split = require("nui.split")
local h = require("tests.helpers")
local spy = require("luassert.spy")

local eq, tbl_pick = h.eq, h.tbl_pick

local function create_popups(...)
  local popups = {}
  for _, popup_options in ipairs({ ... }) do
    table.insert(popups, Popup(popup_options))
  end
  return popups
end

local function create_splits(...)
  local splits = {}
  for _, split_options in ipairs({ ... }) do
    table.insert(splits, Split(split_options))
  end
  return splits
end

local function percent(number, percentage)
  return math.floor(number * percentage / 100)
end

local function get_assert_component(layout)
  local layout_winid = layout.winid
  assert(layout_winid, "missing layout.winid, forgot to mount it?")

  return function(component, expected)
    eq(type(component.bufnr), "number")
    eq(type(component.winid), "number")

    local win_config, border_win_config =
      vim.api.nvim_win_get_config(component.winid),
      component.border.winid and vim.api.nvim_win_get_config(component.border.winid)
    if border_win_config then
      eq(border_win_config.relative, "win")
      eq(border_win_config.win, layout_winid)

      eq(win_config.relative, "win")
      eq(win_config.win, component.border.winid)
    else
      eq(win_config.relative, "win")
      eq(win_config.win, layout_winid)
    end

    if border_win_config then
      local border_row, border_col = border_win_config.row, border_win_config.col
      eq(border_row, expected.position.row)
      eq(border_col, expected.position.col)

      local row, col = win_config.row, win_config.col
      eq(row, math.floor(component.border._.size_delta.width / 2 + 0.5))
      eq(col, math.floor(component.border._.size_delta.height / 2 + 0.5))
    else
      local row, col = win_config.row, win_config.col
      eq(row, expected.position.row)
      eq(col, expected.position.col)
    end

    local expected_width, expected_height = expected.size.width, expected.size.height
    if component.border then
      expected_width = expected_width - component.border._.size_delta.width
      expected_height = expected_height - component.border._.size_delta.height
    end
    eq(vim.api.nvim_win_get_width(component.winid), expected_width)
    eq(vim.api.nvim_win_get_height(component.winid), expected_height)
  end
end

describe("nui.layout", function()
  local layout

  after_each(function()
    if layout then
      layout:unmount()
      layout = nil
    end
  end)

  describe("param box", function()
    it("throws if empty table", function()
      local ok, err = pcall(function()
        Layout({}, {})
      end)

      eq(ok, false)
      eq(type(string.match(err, "unexpected empty box")), "string")
    end)

    it("throws if empty box", function()
      local ok, err = pcall(function()
        Layout(
          {},
          Layout.Box({
            Layout.Box({
              Layout.Box({}, { size = "50%" }),
              Layout.Box({}, { size = "50%" }),
            }, { size = "100%" }),
          })
        )
      end)

      eq(ok, false)
      eq(type(string.match(err, "unexpected empty box")), "string")
    end)

    it("does not throw if non-empty", function()
      local p1 = unpack(create_popups({}))

      local ok, err = pcall(function()
        Layout(
          { position = "50%", size = "100%" },
          Layout.Box({
            Layout.Box({
              Layout.Box(p1, { size = "50%" }),
              Layout.Box({}, { size = "50%" }),
            }, { size = "100%" }),
          })
        )
      end)

      eq(ok, true)
      eq(err, nil)
    end)
  end)

  describe("box", function()
    it("throws if unknown component", function()
      local unknown = require("nui.object")("Unknown")()
      function unknown.mount() end

      local ok, err = pcall(function()
        Layout.Box({
          Layout.Box(unknown, { size = "100%" }),
        })
      end)

      eq(ok, false)
      eq(type(string.match(err, "unsupported component")), "string")
    end)

    it("requires child.size if child.grow is missing", function()
      local p1, p2 = unpack(create_popups({}, {}))

      local ok, result = pcall(function()
        Layout.Box({
          Layout.Box(p1, { size = "50%" }),
          Layout.Box(p2, {}),
        })
      end)

      eq(ok, false)
      eq(type(string.match(result, "missing child.size")), "string")
    end)

    it("does not require child.size if child.grow is present", function()
      local p1, p2 = unpack(create_popups({}, {}))

      local ok = pcall(function()
        Layout.Box({
          Layout.Box(p1, { size = "50%" }),
          Layout.Box(p2, { grow = 1 }),
        })
      end)

      eq(ok, true)
    end)

    describe("size (table)", function()
      it("missing height is set to 100% if dir=row", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local box = Layout.Box({
          Layout.Box(p1, { size = { width = "40%" } }),
          Layout.Box(p2, { size = { width = "60%", height = "80%" } }),
        }, { dir = "row" })

        eq(box.box[1].size, {
          width = "40%",
          height = "100%",
        })
        eq(box.box[2].size, {
          width = "60%",
          height = "80%",
        })
      end)

      it("missing width is set to 100% if dir=col", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local box = Layout.Box({
          Layout.Box(p1, { size = { height = "40%" } }),
          Layout.Box(p2, { size = { width = "60%", height = "80%" } }),
        }, { dir = "col" })

        eq(box.box[1].size, {
          width = "100%",
          height = "40%",
        })
        eq(box.box[2].size, {
          width = "60%",
          height = "80%",
        })
      end)
    end)

    describe("size (percentage string)", function()
      it("is set to width if dir=row", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local box = Layout.Box({
          Layout.Box(p1, { size = "40%" }),
          Layout.Box(p2, { size = "60%" }),
        }, { dir = "row" })

        eq(box.box[1].size, {
          width = "40%",
          height = "100%",
        })
        eq(box.box[2].size, {
          width = "60%",
          height = "100%",
        })
      end)

      it("is set to height if dir=col", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local box = Layout.Box({
          Layout.Box(p1, { size = "40%" }),
          Layout.Box(p2, { size = "60%" }),
        }, { dir = "col" })

        eq(box.box[1].size, {
          width = "100%",
          height = "40%",
        })
        eq(box.box[2].size, {
          width = "100%",
          height = "60%",
        })
      end)
    end)
  end)

  describe("[float]", function()
    describe("o.size", function()
      local box

      before_each(function()
        local p1 = unpack(create_popups({}))
        box = Layout.Box({ Layout.Box(p1, { size = "100%" }) })
      end)

      local function assert_size(size)
        local win_config = vim.api.nvim_win_get_config(layout.winid)

        eq(tbl_pick(win_config, { "width", "height" }), {
          width = math.floor(size.width),
          height = math.floor(size.height),
        })
      end

      it("supports number", function()
        local size = 20

        layout = Layout({
          position = "50%",
          size = size,
        }, box)

        layout:mount()

        assert_size({ width = size, height = size })
      end)

      it("supports percentage string", function()
        local percentage = 50

        layout = Layout({
          position = "50%",
          size = string.format("%s%%", percentage),
        }, box)

        local winid = vim.api.nvim_get_current_win()
        local win_width = vim.api.nvim_win_get_width(winid)
        local win_height = vim.api.nvim_win_get_height(winid)

        layout:mount()

        assert_size({
          width = win_width * percentage / 100,
          height = win_height * percentage / 100,
        })
      end)

      it("supports table", function()
        local width = 10
        local height_percentage = 50

        layout = Layout({
          position = "50%",
          size = {
            width = width,
            height = string.format("%s%%", height_percentage),
          },
        }, box)

        local winid = vim.api.nvim_get_current_win()
        local win_height = vim.api.nvim_win_get_height(winid)

        layout:mount()

        assert_size({
          width = width,
          height = win_height * height_percentage / 100,
        })
      end)
    end)

    describe("o.position", function()
      local box

      before_each(function()
        local p1 = unpack(create_popups({}))
        box = Layout.Box({ Layout.Box(p1, { size = "100%" }) })
      end)

      local function assert_position(position)
        local row, col = unpack(vim.api.nvim_win_get_position(layout.winid))

        eq(row, math.floor(position.row))
        eq(col, math.floor(position.col))
      end

      it("supports number", function()
        local position = 5

        layout = Layout({
          position = position,
          size = 10,
        }, box)

        layout:mount()

        assert_position({ row = position, col = position })
      end)

      it("supports percentage string", function()
        local size = 10
        local percentage = 50

        layout = Layout({
          position = string.format("%s%%", percentage),
          size = size,
        }, box)

        layout:mount()

        local winid = vim.api.nvim_get_current_win()
        local win_width = vim.api.nvim_win_get_width(winid)
        local win_height = vim.api.nvim_win_get_height(winid)

        assert_position({
          row = (win_height - size) * percentage / 100,
          col = (win_width - size) * percentage / 100,
        })
      end)

      it("supports table", function()
        local size = 10
        local row = 5
        local col_percentage = 50

        layout = Layout({
          position = {
            row = row,
            col = string.format("%s%%", col_percentage),
          },
          size = size,
        }, box)

        layout:mount()

        local winid = vim.api.nvim_get_current_win()
        local win_width = vim.api.nvim_win_get_width(winid)

        assert_position({
          row = row,
          col = (win_width - size) * col_percentage / 100,
        })
      end)
    end)

    describe("method :mount", function()
      it("mounts all components", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local p1_mount = spy.on(p1, "mount")
        local p2_mount = spy.on(p2, "mount")

        layout = Layout(
          {
            position = "50%",
            size = {
              height = 20,
              width = 100,
            },
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box(p2, { size = "50%" }),
          })
        )

        layout:mount()

        eq(type(layout.bufnr), "number")
        eq(type(layout.winid), "number")

        assert.spy(p1_mount).was_called()
        assert.spy(p2_mount).was_called()
      end)

      it("is idempotent", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local p1_mount = spy.on(p1, "mount")
        local p2_mount = spy.on(p2, "mount")

        layout = Layout(
          {
            position = "50%",
            size = 20,
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box(p2, { size = "50%" }),
          })
        )

        layout:mount()

        assert.spy(p1_mount).was_called(1)
        assert.spy(p2_mount).was_called(1)

        layout:mount()

        assert.spy(p1_mount).was_called(1)
        assert.spy(p2_mount).was_called(1)
      end)

      it("supports container component", function()
        local p1, p2 = unpack(create_popups({}, {}))

        local split = Split({
          relative = "editor",
          position = "bottom",
          size = 10,
        })

        local split_mount = spy.on(split, "mount")

        layout = Layout(
          split,
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box(p2, { size = "50%" }),
          })
        )

        layout:mount()

        assert.spy(split_mount).was_called(1)

        local win_config = vim.api.nvim_win_get_config(layout.winid)
        eq(win_config.relative, "win")
        eq(win_config.row, 0)
        eq(win_config.col, 0)
        eq(win_config.width, vim.o.columns)
        eq(win_config.height, 10)

        split:unmount()
      end)

      it("throws if missing config 'size'", function()
        local p1 = unpack(create_popups({}, {}))

        local ok, result = pcall(function()
          layout = Layout({}, { Layout.Box(p1, { size = "100%" }) })
        end)

        eq(ok, false)
        eq(type(string.match(result, "missing layout config: size")), "string")
      end)

      it("throws if missing config 'position'", function()
        local p1 = unpack(create_popups({}, {}))

        local ok, result = pcall(function()
          layout = Layout({
            size = "50%",
          }, { Layout.Box(p1, { size = "100%" }) })
        end)

        eq(ok, false)
        eq(type(string.match(result, "missing layout config: position")), "string")
      end)
    end)

    h.describe_flipping_feature("lua_autocmd", "method :unmount", function()
      it("is called if any popup is unmounted", function()
        local p1, p2 = unpack(create_popups({}, {}, {}))

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box(p2, { size = "50%" }),
          })
        )

        local layout_unmount = spy.on(layout, "unmount")

        layout:mount()

        p2:unmount()

        vim.wait(100, function()
          return not layout._.mounted
        end, 10)

        assert.spy(layout_unmount).was_called()
      end)

      it("is called if any popup is quitted", function()
        local p1, p2 = unpack(create_popups({}, {}))

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box(p2, { size = "50%" }),
          })
        )

        local layout_unmount = spy.on(layout, "unmount")

        layout:mount()

        vim.api.nvim_buf_call(p2.bufnr, function()
          vim.cmd([[quit]])
        end)

        vim.wait(100, function()
          return not layout._.mounted
        end, 10)

        assert.spy(layout_unmount).was_called()
      end)
    end)

    h.describe_flipping_feature("lua_autocmd", "method :hide", function()
      it("does nothing if not mounted", function()
        local p1 = unpack(create_popups({}))

        local p1_hide = spy.on(p1, "hide")

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "100%" }),
          })
        )

        layout:hide()

        assert.spy(p1_hide).was_not_called()
      end)

      it("hides all components", function()
        local p1, p2, p3 = unpack(create_popups({}, {}, {}))

        local p1_hide = spy.on(p1, "hide")
        local p2_hide = spy.on(p2, "hide")
        local p3_hide = spy.on(p3, "hide")

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box({
              Layout.Box(p2, { size = "50%" }),
              Layout.Box({
                Layout.Box(p3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        layout:mount()

        eq(type(layout.winid), "number")

        layout:hide()

        eq(type(layout.winid), "nil")

        assert.spy(p1_hide).was_called()
        assert.spy(p2_hide).was_called()
        assert.spy(p3_hide).was_called()
      end)

      it("is called if any popup is hidden", function()
        local p1, p2, p3 = unpack(create_popups({}, {}, {}))

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box({
              Layout.Box(p2, { size = "50%" }),
              Layout.Box({
                Layout.Box(p3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        local layout_hide = spy.on(layout, "hide")

        layout:mount()

        p2:hide()

        assert.spy(layout_hide).was_called()
      end)
    end)

    describe("method :show", function()
      it("mounts if not mounted", function()
        local p1 = unpack(create_popups({}))

        local p1_mount = spy.on(p1, "mount")
        local p1_show = spy.on(p1, "show")

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "100%" }),
          })
        )

        layout:hide()
        layout:show()

        assert.spy(p1_mount).was_called()
        assert.spy(p1_show).was_not_called()
      end)

      it("shows all components", function()
        local p1, p2, p3 = unpack(create_popups({}, {}, {}))

        local p1_show = spy.on(p1, "show")
        local p2_show = spy.on(p2, "show")
        local p3_show = spy.on(p3, "show")

        layout = Layout(
          {
            position = "50%",
            size = 10,
          },
          Layout.Box({
            Layout.Box(p1, { size = "50%" }),
            Layout.Box({
              Layout.Box(p2, { size = "50%" }),
              Layout.Box({
                Layout.Box(p3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        layout:mount()

        layout:hide()
        layout:show()

        eq(type(layout.winid), "number")

        assert.spy(p1_show).was_called()
        assert.spy(p2_show).was_called()
        assert.spy(p3_show).was_called()
      end)
    end)

    describe("method :update", function()
      local winid, win_width, win_height
      local p1, p2, p3, p4
      local assert_component

      before_each(function()
        winid = vim.api.nvim_get_current_win()
        win_width = vim.api.nvim_win_get_width(winid)
        win_height = vim.api.nvim_win_get_height(winid)

        p1, p2, p3, p4 = unpack(create_popups({
          border = { style = "single" },
        }, {}, {
          border = { style = "rounded", text = {} },
        }, {}))
      end)

      local function get_initial_layout(config)
        return Layout(
          config,
          Layout.Box({
            Layout.Box(p1, { size = "20%" }),
            Layout.Box({
              Layout.Box(p3, { size = "50%" }),
              Layout.Box(p4, { size = "50%" }),
            }, { dir = "col", size = "60%" }),
            Layout.Box(p2, { size = "20%" }),
          }, { dir = "row" })
        )
      end

      local function assert_layout_config(config)
        local relative, position, size = config.relative, config.position, config.size

        local win_config = vim.api.nvim_win_get_config(layout.winid)
        eq(win_config.anchor, config.anchor or "NW")
        eq(win_config.relative, relative.type)
        eq(win_config.win, relative.winid)

        local row, col = unpack(vim.api.nvim_win_get_position(layout.winid))
        eq(row, position.row)
        eq(col, position.col)

        eq(vim.api.nvim_win_get_width(layout.winid), size.width)
        eq(vim.api.nvim_win_get_height(layout.winid), size.height)
      end

      local function assert_initial_layout_components()
        local size = {
          width = vim.api.nvim_win_get_width(layout.winid),
          height = vim.api.nvim_win_get_height(layout.winid),
        }

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = percent(size.width, 20),
            height = size.height,
          },
        })

        assert_component(p3, {
          position = {
            row = 0,
            col = percent(size.width, 20),
          },
          size = {
            width = percent(size.width, 60),
            height = percent(size.height, 50),
          },
        })

        assert_component(p4, {
          position = {
            row = percent(size.height, 50),
            col = percent(size.width, 20),
          },
          size = {
            width = percent(size.width, 60),
            height = percent(size.height, 50),
          },
        })

        assert_component(p2, {
          position = {
            row = 0,
            col = percent(size.width, 20) + percent(size.width, 60),
          },
          size = {
            width = percent(size.width, 20),
            height = size.height,
          },
        })
      end

      it("processes layout correctly on mount", function()
        local layout_update_spy = spy.on(Layout, "update")

        layout = get_initial_layout({ position = 0, size = "100%" })

        layout:mount()

        layout_update_spy:revert()
        assert.spy(layout_update_spy).was_called(1)

        local expected_layout_config = {
          relative = {
            type = "win",
            winid = winid,
          },
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = win_height,
          },
        }

        assert_layout_config(expected_layout_config)

        assert_component = get_assert_component(layout)

        assert_initial_layout_components()
      end)

      it("can update layout win_config w/o rearranging boxes", function()
        layout = get_initial_layout({ position = 0, size = "100%" })

        layout:mount()

        layout:update({
          position = {
            row = 2,
            col = 4,
          },
          size = "80%",
        })

        local expected_layout_config = {
          relative = {
            type = "win",
            winid = winid,
          },
          position = {
            row = 2,
            col = 4,
          },
          size = {
            width = percent(win_width, 80),
            height = percent(win_height, 80),
          },
        }

        assert_layout_config(expected_layout_config)

        assert_component = get_assert_component(layout)

        assert_initial_layout_components()
      end)

      it("can rearrange boxes w/o changing layout win_config", function()
        layout = get_initial_layout({ position = 0, size = "100%" })

        layout:mount()

        layout:update(Layout.Box({
          Layout.Box(p2, { size = "30%" }),
          Layout.Box({
            Layout.Box(p4, { size = "40%" }),
            Layout.Box(p3, { size = "60%" }),
          }, { dir = "row", size = "30%" }),
          Layout.Box(p1, { size = "40%" }),
        }, { dir = "col" }))

        local expected_layout_config = {
          relative = {
            type = "win",
            winid = winid,
          },
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = win_height,
          },
        }

        assert_layout_config(expected_layout_config)

        assert_component = get_assert_component(layout)

        assert_component(p2, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 30),
          },
        })

        assert_component(p4, {
          position = {
            row = percent(win_height, 30),
            col = 0,
          },
          size = {
            width = percent(win_width, 40),
            height = percent(win_height, 30),
          },
        })

        assert_component(p3, {
          position = {
            row = percent(win_height, 30),
            col = percent(win_width, 40),
          },
          size = {
            width = percent(win_width, 60),
            height = percent(win_height, 30),
          },
        })

        assert_component(p1, {
          position = {
            row = percent(win_height, 30) + percent(win_height, 30),
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 40),
          },
        })
      end)

      it("refreshes layout if container size changes", function()
        local popup = Popup({
          position = 0,
          size = "100%",
        })

        popup:mount()

        layout = get_initial_layout({
          relative = {
            type = "win",
            winid = popup.winid,
          },
          position = 0,
          size = "80%",
        })

        layout:mount()

        local expected_layout_config = {
          relative = {
            type = "win",
            winid = popup.winid,
          },
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = percent(win_width, 80),
            height = percent(win_height, 80),
          },
        }

        assert_layout_config(expected_layout_config)

        assert_component = get_assert_component(layout)

        assert_initial_layout_components()

        popup:update_layout({
          size = "80%",
        })

        layout:update()

        expected_layout_config.size = {
          width = percent(percent(win_width, 80), 80),
          height = percent(percent(win_height, 80), 80),
        }

        assert_layout_config(expected_layout_config)

        assert_initial_layout_components()
      end)

      it("supports child with child.grow", function()
        layout = get_initial_layout({ position = 0, size = "100%" })

        layout:mount()

        layout:update(Layout.Box({
          Layout.Box(p1, { size = "20%" }),
          Layout.Box({
            Layout.Box({}, { size = 4 }),
            Layout.Box(p3, { grow = 1 }),
            Layout.Box({}, { size = 8 }),
            Layout.Box(p4, { grow = 1 }),
          }, { dir = "col", size = "60%" }),
          Layout.Box(p2, { grow = 1 }),
        }, { dir = "row" }))

        local expected_layout_config = {
          relative = {
            type = "win",
            winid = winid,
          },
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = win_height,
          },
        }

        assert_layout_config(expected_layout_config)

        assert_component = get_assert_component(layout)

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = percent(win_width, 20),
            height = win_height,
          },
        })

        assert_component(p3, {
          position = {
            row = 4,
            col = percent(win_width, 20),
          },
          size = {
            width = percent(win_width, 60),
            height = percent(win_height - 4 - 8, 100 / 2),
          },
        })

        assert_component(p4, {
          position = {
            row = 4 + 8 + percent(win_height - 4 - 8, 100 / 2),
            col = percent(win_width, 20),
          },
          size = {
            width = percent(win_width, 60),
            height = percent(win_height - 4 - 8, 100 / 2),
          },
        })

        assert_component(p2, {
          position = {
            row = 0,
            col = percent(win_width, 20) + percent(win_width, 60),
          },
          size = {
            width = percent(win_width, 100 - 20 - 60),
            height = win_height,
          },
        })
      end)

      it("handles if child.grow results in height <= 0", function()
        layout = get_initial_layout({ position = 0, size = "100%" })

        layout:update(Layout.Box({
          Layout.Box(p1, { size = win_height }),
          Layout.Box(p2, { grow = 1 }),
        }, { dir = "col" }))

        layout:mount()

        local expected_layout_config = {
          relative = {
            type = "win",
            winid = winid,
          },
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = win_height,
          },
        }

        assert_layout_config(expected_layout_config)

        assert_component = get_assert_component(layout)

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = win_height,
          },
        })

        assert_component(p2, {
          position = {
            row = win_height,
            col = 0,
          },
          size = {
            width = win_width,
            height = 1,
          },
        })
      end)

      it("can change boxes", function()
        layout = Layout(
          { position = 0, size = "100%" },
          Layout.Box({
            Layout.Box(p1, { size = "40%" }),
            Layout.Box(p2, { size = "60%" }),
          }, { dir = "col" })
        )

        layout:mount()

        assert_component = get_assert_component(layout)

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 40),
          },
        })

        assert_component(p2, {
          position = {
            row = percent(win_height, 40),
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 60),
          },
        })

        layout:update(Layout.Box({
          Layout.Box({
            Layout.Box(p1, { size = "40%" }),
            Layout.Box(p2, { size = "60%" }),
          }, { dir = "col", size = "60%" }),
          Layout.Box(p3, { size = "40%" }),
        }, { dir = "row" }))

        assert_component = get_assert_component(layout)

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = percent(win_width, 60),
            height = percent(win_height, 40),
          },
        })

        assert_component(p2, {
          position = {
            row = percent(win_height, 40),
            col = 0,
          },
          size = {
            width = percent(win_width, 60),
            height = percent(win_height, 60),
          },
        })

        assert_component(p3, {
          position = {
            row = 0,
            col = percent(win_width, 60),
          },
          size = {
            width = percent(win_width, 40),
            height = win_height,
          },
        })

        layout:update(Layout.Box({
          Layout.Box({
            Layout.Box(p1, { size = "40%" }),
            Layout.Box(p2, { size = "60%" }),
          }, { dir = "col", size = "60%" }),
          Layout.Box(p4, { size = "40%" }),
        }, { dir = "row" }))

        assert_component(p4, {
          position = {
            row = 0,
            col = percent(win_width, 60),
          },
          size = {
            width = percent(win_width, 40),
            height = win_height,
          },
        })

        eq(p3.winid, nil)

        layout:update(Layout.Box({
          Layout.Box(p3, { size = "40%" }),
          Layout.Box(p4, { size = "60%" }),
        }, { dir = "col" }))

        eq(p1.winid, nil)
        eq(p2.winid, nil)

        assert_component(p3, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 40),
          },
        })

        assert_component(p4, {
          position = {
            row = percent(win_height, 40),
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 60),
          },
        })
      end)

      it("positions popup with complex border correctly", function()
        p1 = unpack(create_popups({
          border = {
            style = "single",
            text = {
              top = "text",
            },
            padding = { 1 },
          },
        }))

        layout = Layout(
          { position = 0, size = "100%" },
          Layout.Box({
            Layout.Box(p1, { size = "100%" }),
          }, { dir = "col" })
        )

        layout:mount()

        assert_component = get_assert_component(layout)

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 100),
          },
        })
      end)

      it("can change anchor", function()
        layout = Layout({
          position = 0,
          size = 10,
        }, {
          Layout.Box(p1, { size = "100%" }),
        })

        layout:mount()

        layout:update({ anchor = "SW" })

        assert_layout_config({
          anchor = "SW",
          relative = { type = "win", winid = winid },
          position = { row = 0, col = 0 },
          size = { height = 10, width = 10 },
        })
      end)

      it("can handle col[box,row[box,col[box,box]]]", function()
        layout = get_initial_layout({ position = 0, size = "100%" })

        layout:mount()

        layout:update(Layout.Box({
          Layout.Box(p1, { size = "50%" }),
          Layout.Box({
            Layout.Box(p2, { size = "50%" }),
            Layout.Box({
              Layout.Box(p3, { size = "50%" }),
              Layout.Box(p4, { size = "50%" }),
            }, { dir = "col", size = "50%" }),
          }, { dir = "row", size = "50%" }),
        }, { dir = "col" }))

        assert_component = get_assert_component(layout)

        assert_component(p1, {
          position = {
            row = 0,
            col = 0,
          },
          size = {
            width = win_width,
            height = percent(win_height, 50),
          },
        })

        assert_component(p2, {
          position = {
            row = percent(win_height, 50),
            col = 0,
          },
          size = {
            width = percent(win_width, 50),
            height = percent(win_height, 50),
          },
        })

        assert_component(p3, {
          position = {
            row = percent(win_height, 50),
            col = percent(win_width, 50),
          },
          size = {
            width = percent(win_width, 50),
            height = percent(percent(win_height, 50), 50),
          },
        })

        assert_component(p4, {
          position = {
            row = percent(win_height, 50 + 25),
            col = percent(win_width, 50),
          },
          size = {
            width = percent(win_width, 50),
            height = percent(percent(win_height, 50), 50),
          },
        })
      end)
    end)
  end)

  describe("[split]", function()
    local function assert_size(winid, expected, tolerance)
      h.approx(vim.api.nvim_win_get_width(winid), expected.width, tolerance or 0)
      h.approx(vim.api.nvim_win_get_height(winid), expected.height, tolerance or 0)
    end

    describe("method :mount", function()
      it("mounts all components", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        local s1_mount = spy.on(s1, "mount")
        local s2_mount = spy.on(s2, "mount")
        local s3_mount = spy.on(s3, "mount")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        layout:mount()

        eq(type(layout.bufnr), "nil")
        eq(type(layout.winid), "nil")

        assert.spy(s1_mount).was_called()
        assert.spy(s2_mount).was_called()
        assert.spy(s3_mount).was_called()
      end)

      it("is idempotent", function()
        local s1, s2 = unpack(create_splits({}, {}))

        local s1_mount = spy.on(s1, "mount")
        local s2_mount = spy.on(s2, "mount")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box(s2, { size = "50%" }),
          })
        )

        layout:mount()

        assert.spy(s1_mount).was_called(1)
        assert.spy(s2_mount).was_called(1)

        layout:mount()

        assert.spy(s1_mount).was_called(1)
        assert.spy(s2_mount).was_called(1)
      end)

      it("mounts with correct layout", function()
        local winid = vim.api.nvim_get_current_win()

        local s1, s2, s3, s4, s5 = unpack(create_splits({}, {}, {}, {}, {}))

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "100%" }),
            Layout.Box({
              Layout.Box({
                Layout.Box(s2, { size = "100%" }),
              }, { dir = "col", size = "60%" }),
              Layout.Box({
                Layout.Box(s3, { size = "40%" }),
                Layout.Box(s4, { size = "60%" }),
              }, { dir = "row", size = "40%" }),
              Layout.Box({}, { size = "0%" }),
            }, { dir = "col", size = "40%" }),
            Layout.Box(s5, { size = "35%" }),
          })
        )

        layout:mount()

        eq(vim.fn.winlayout(), {
          "col",
          {
            { "leaf", winid },
            {
              "row",
              {
                { "leaf", s1.winid },
                {
                  "col",
                  {
                    { "leaf", s2.winid },
                    {
                      "row",
                      {
                        { "leaf", s3.winid },
                        { "leaf", s4.winid },
                      },
                    },
                  },
                },
                { "leaf", s5.winid },
              },
            },
          },
        })
      end)

      it("mounts with acceptable sizes", function()
        local winid = vim.api.nvim_get_current_win()
        local base_size = {
          width = vim.api.nvim_win_get_width(winid),
          height = vim.api.nvim_win_get_height(winid),
        }

        local s1, s2, s3, s4, s5 = unpack(create_splits({}, {}, {}, {}, {}))

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "25%" }),
            Layout.Box({
              Layout.Box({
                Layout.Box(s2, { size = "100%" }),
              }, { dir = "col", size = "60%" }),
              Layout.Box({
                Layout.Box(s3, { size = "40%" }),
                Layout.Box(s4, { size = "60%" }),
              }, { dir = "row", size = "40%" }),
              Layout.Box({}, { size = "0%" }),
            }, { dir = "col", size = "40%" }),
            Layout.Box(s5, { size = "35%" }),
          })
        )

        layout:mount()

        assert_size(s1.winid, {
          width = percent(base_size.width, 25),
          height = percent(20, 100),
        }, 2)

        assert_size(s2.winid, {
          width = percent(base_size.width, 40),
          height = percent(20, 60),
        }, 1)

        assert_size(s3.winid, {
          width = percent(percent(base_size.width, 40), 40),
          height = percent(20, 40),
        }, 1)

        assert_size(s4.winid, {
          width = percent(percent(base_size.width, 40), 60),
          height = percent(20, 40),
        }, 1)

        assert_size(s5.winid, {
          width = percent(base_size.width, 35),
          height = percent(20, 100),
        })
      end)
    end)

    describe("method :unmount", function()
      it("unmounts all components", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        local s1_unmount = spy.on(s1, "unmount")
        local s2_unmount = spy.on(s2, "unmount")
        local s3_unmount = spy.on(s3, "unmount")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        layout:mount()
        layout:unmount()

        assert.spy(s1_unmount).was_called()
        assert.spy(s2_unmount).was_called()
        assert.spy(s3_unmount).was_called()
      end)

      it("is called if any split is unmounted", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        local layout_unmount = spy.on(layout, "unmount")

        layout:mount()

        s2:unmount()

        vim.wait(100, function()
          return not layout._.mounted
        end, 10)

        assert.spy(layout_unmount).was_called()
      end)

      it("is called if any split is quitted", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        local layout_unmount = spy.on(layout, "unmount")

        layout:mount()

        vim.api.nvim_buf_call(s2.bufnr, function()
          vim.cmd([[quit]])
        end)

        vim.wait(100, function()
          return not layout._.mounted
        end, 10)

        assert.spy(layout_unmount).was_called()
      end)
    end)

    describe("method :hide", function()
      it("does nothing if not mounted", function()
        local s1 = unpack(create_splits({}))

        local s1_hide = spy.on(s1, "hide")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "100%" }),
          })
        )

        layout:hide()

        assert.spy(s1_hide).was_not_called()
      end)

      it("hides all components", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        local s1_hide = spy.on(s1, "hide")
        local s2_hide = spy.on(s2, "hide")
        local s3_hide = spy.on(s3, "hide")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        layout:mount()

        layout:hide()

        assert.spy(s1_hide).was_called()
        assert.spy(s2_hide).was_called()
        assert.spy(s3_hide).was_called()
      end)

      it("is called if any split is hidden", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        local layout_hide = spy.on(layout, "hide")

        layout:mount()

        s2:hide()

        assert.spy(layout_hide).was_called()
      end)
    end)

    describe("method :show", function()
      it("mounts if not mounted", function()
        local s1 = unpack(create_splits({}))

        local s1_mount = spy.on(s1, "mount")
        local s1_show = spy.on(s1, "show")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "100%" }),
          })
        )

        layout:hide()
        layout:show()

        assert.spy(s1_mount).was_called()
        assert.spy(s1_show).was_not_called()
      end)

      it("shows all components", function()
        local s1, s2, s3 = unpack(create_splits({}, {}, {}))

        local s1_show = spy.on(s1, "show")
        local s2_show = spy.on(s2, "show")
        local s3_show = spy.on(s3, "show")

        layout = Layout(
          {
            position = "bottom",
            size = 20,
          },
          Layout.Box({
            Layout.Box(s1, { size = "50%" }),
            Layout.Box({
              Layout.Box(s2, { size = "50%" }),
              Layout.Box({
                Layout.Box(s3, { size = "100%" }),
              }, { size = "50%" }),
            }, { size = "50%" }),
          })
        )

        layout:mount()

        layout:hide()
        layout:show()

        assert.spy(s1_show).was_called()
        assert.spy(s2_show).was_called()
        assert.spy(s3_show).was_called()
      end)
    end)

    describe("method :update", function()
      local winid, base_size
      local s1, s2, s3, s4

      before_each(function()
        winid = vim.api.nvim_get_current_win()
        base_size = {
          width = vim.api.nvim_win_get_width(winid),
          height = vim.api.nvim_win_get_height(winid),
        }

        s1, s2, s3, s4 = unpack(create_splits({}, {}, {}, {}))
      end)

      local function get_initial_layout(config)
        return Layout(
          config,
          Layout.Box({
            Layout.Box(s1, { size = "20%" }),
            Layout.Box({
              Layout.Box(s2, { size = "40%" }),
              Layout.Box(s3, { size = "60%" }),
            }, { dir = "col", size = "50%" }),
            Layout.Box(s4, { size = "30%" }),
          }, { dir = "row" })
        )
      end

      it("can update layout win_config w/o rearranging boxes", function()
        layout = get_initial_layout({
          position = "bottom",
          size = 10,
        })

        layout:mount()

        assert_size(s1.winid, {
          width = percent(base_size.width, 20),
          height = percent(10, 100),
        }, 2)

        assert_size(s2.winid, {
          width = percent(base_size.width, 50),
          height = percent(10, 40),
        }, 1)

        assert_size(s3.winid, {
          width = percent(base_size.width, 50),
          height = percent(10, 60),
        })

        assert_size(s4.winid, {
          width = percent(base_size.width, 30),
          height = percent(10, 100),
        })

        layout:update({ size = 20 })

        assert_size(s1.winid, {
          width = percent(base_size.width, 20),
          height = percent(20, 100),
        }, 2)

        assert_size(s2.winid, {
          width = percent(base_size.width, 50),
          height = percent(20, 40),
        }, 2)

        assert_size(s3.winid, {
          width = percent(base_size.width, 50),
          height = percent(20, 60),
        }, 2)

        assert_size(s4.winid, {
          width = percent(base_size.width, 30),
          height = percent(20, 100),
        })
      end)

      it("supports child with child.grow", function()
        layout = get_initial_layout({
          position = "bottom",
          size = 10,
        })

        layout:update(Layout.Box({
          Layout.Box(s1, { size = 20 }),
          Layout.Box({
            Layout.Box(s2, { grow = 1 }),
            Layout.Box(s3, { grow = 2 }),
          }, { dir = "col", grow = 2 }),
          Layout.Box(s4, { grow = 1 }),
        }, { dir = "row" }))

        layout:mount()

        assert_size(s1.winid, {
          width = 20,
          height = 10,
        }, 2)

        assert_size(s2.winid, {
          width = ((base_size.width - 20) / (2 + 1)) * 2,
          height = (10 / (1 + 2)) * 1,
        }, 1)

        assert_size(s3.winid, {
          width = ((base_size.width - 20) / (2 + 1)) * 2,
          height = (10 / (1 + 2)) * 2,
        }, 1)

        assert_size(s4.winid, {
          width = ((base_size.width - 20) / (2 + 1)) * 1,
          height = 10,
        })
      end)

      it("can change boxes", function()
        layout = Layout(
          { position = "bottom", size = 10 },
          Layout.Box({
            Layout.Box(s1, { size = "40%" }),
            Layout.Box(s2, { size = "60%" }),
          }, { dir = "row" })
        )

        layout:mount()

        eq(vim.fn.winlayout(), {
          "col",
          {
            { "leaf", winid },
            {
              "row",
              {
                { "leaf", s1.winid },
                { "leaf", s2.winid },
              },
            },
          },
        })

        layout:update(Layout.Box({
          Layout.Box({
            Layout.Box(s1, { size = "40%" }),
            Layout.Box(s2, { size = "60%" }),
          }, { dir = "col", size = "60%" }),
          Layout.Box(s3, { size = "40%" }),
        }, { dir = "row" }))

        eq(vim.fn.winlayout(), {
          "col",
          {
            { "leaf", winid },
            {
              "row",
              {
                {
                  "col",
                  {
                    { "leaf", s1.winid },
                    { "leaf", s2.winid },
                  },
                },
                { "leaf", s3.winid },
              },
            },
          },
        })

        layout:update(Layout.Box({
          Layout.Box({
            Layout.Box(s1, { size = "40%" }),
            Layout.Box(s2, { size = "60%" }),
          }, { dir = "col", size = "60%" }),
          Layout.Box(s4, { size = "40%" }),
        }, { dir = "row" }))

        eq(vim.fn.winlayout(), {
          "col",
          {
            { "leaf", winid },
            {
              "row",
              {
                {
                  "col",
                  {
                    { "leaf", s1.winid },
                    { "leaf", s2.winid },
                  },
                },
                { "leaf", s4.winid },
              },
            },
          },
        })

        eq(s3.winid, nil)
      end)
    end)
  end)
end)
