pcall(require, "luacov")

local Menu = require("nui.menu")
local Layout = require("nui.layout")
local Line = require("nui.line")
local Text = require("nui.text")
local h = require("tests.helpers")
local spy = require("luassert.spy")

local eq, feedkeys = h.eq, h.feedkeys

describe("nui.menu", function()
  local callbacks
  local popup_options
  local menu

  before_each(function()
    callbacks = {
      on_change = function() end,
      on_submit = function() end,
    }

    popup_options = {
      relative = "win",
      position = "50%",
    }
  end)

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

  describe("method :new", function()
    it("works with menu", function()
      menu = Menu:new(popup_options, {
        lines = {
          Menu.item("a"),
        },
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "a",
      })
    end)
  end)

  describe("o.keymap", function()
    it("supports multiple keys as table", function()
      local on_change = spy.on(callbacks, "on_change")

      local lines = {
        Menu.item("Item 1", { id = 1 }),
        Menu.item("Item 2", { id = 2 }),
        Menu.item("Item 3", { id = 3 }),
      }

      menu = Menu(popup_options, {
        keymap = {
          focus_next = { "j", "s" },
          focus_prev = { "k", "w" },
        },
        lines = lines,
        on_change = on_change,
      })

      menu:mount()

      feedkeys("j", "x")
      assert.spy(on_change).called_with(lines[2], menu)
      on_change:clear()

      feedkeys("s", "x")
      assert.spy(on_change).called_with(lines[3], menu)
      on_change:clear()

      feedkeys("w", "x")
      assert.spy(on_change).called_with(lines[2], menu)
      on_change:clear()

      feedkeys("k", "x")
      assert.spy(on_change).called_with(lines[1], menu)
      on_change:clear()
    end)

    it("supports single key as string", function()
      local on_change = spy.on(callbacks, "on_change")

      local lines = {
        Menu.item("Item 1", { id = 1 }),
        Menu.item("Item 2", { id = 2 }),
        Menu.item("Item 3", { id = 3 }),
      }

      menu = Menu(popup_options, {
        keymap = {
          focus_next = "s",
          focus_prev = "w",
        },
        lines = lines,
        on_change = on_change,
      })

      menu:mount()

      feedkeys("s", "x")
      assert.spy(on_change).called_with(lines[2], menu)
      on_change:clear()

      feedkeys("w", "x")
      assert.spy(on_change).called_with(lines[1], menu)
      on_change:clear()
    end)
  end)

  describe("size", function()
    it("respects o.min_width", function()
      local min_width = 3

      local items = {
        Menu.item("A"),
        Menu.separator("*"),
        Menu.item("B"),
      }

      menu = Menu(popup_options, {
        lines = items,
        min_width = min_width,
      })

      menu:mount()

      eq(vim.api.nvim_win_get_width(menu.winid), min_width)

      h.assert_buf_lines(menu.bufnr, {
        "A",
        " * ",
        "B",
      })
    end)

    it("respects o.max_width", function()
      local max_width = 6

      local items = {
        Menu.item("Item 1"),
        Menu.separator("*"),
        Menu.item("Item Number Two"),
      }

      menu = Menu(popup_options, {
        lines = items,
        max_width = max_width,
      })

      menu:mount()

      eq(vim.api.nvim_win_get_width(menu.winid), max_width)

      h.assert_buf_lines(menu.bufnr, {
        "Item 1",
        " *    ",
        "Item …",
      })
    end)

    it("respects o.min_height", function()
      local min_height = 3

      local items = {
        Menu.item("A"),
        Menu.separator("*"),
        Menu.item("B"),
      }

      menu = Menu(popup_options, {
        lines = items,
        min_height = min_height,
      })

      menu:mount()

      eq(vim.api.nvim_win_get_height(menu.winid), min_height)
    end)

    it("respects o.max_height", function()
      local max_height = 2

      local items = {
        Menu.item("A"),
        Menu.separator("*"),
        Menu.item("B"),
      }

      menu = Menu(popup_options, {
        lines = items,
        max_height = max_height,
      })

      menu:mount()

      eq(vim.api.nvim_win_get_height(menu.winid), max_height)
    end)
  end)

  it("calls o.on_change item focus is changed", function()
    local on_change = spy.on(callbacks, "on_change")

    local lines = {
      Menu.item("Item 1", { id = 1 }),
      Menu.item("Item 2", { id = 2 }),
    }

    menu = Menu(popup_options, {
      lines = lines,
      on_change = on_change,
    })

    menu:mount()

    -- initial focus
    assert.spy(on_change).called_with(lines[1], menu)
    on_change:clear()

    feedkeys("j", "x")
    assert.spy(on_change).called_with(lines[2], menu)
    on_change:clear()

    feedkeys("j", "x")
    assert.spy(on_change).called_with(lines[1], menu)
    on_change:clear()

    feedkeys("k", "x")
    assert.spy(on_change).called_with(lines[2], menu)
    on_change:clear()
  end)

  it("calls o.on_submit when item is submitted", function()
    local on_submit = spy.on(callbacks, "on_submit")

    local lines = {
      Menu.item("Item 1", { id = 1 }),
      Menu.item("Item 2", { id = 2 }),
    }

    menu = Menu(popup_options, {
      lines = lines,
      on_submit = on_submit,
    })

    menu:mount()

    feedkeys("j", "x")
    feedkeys("<CR>", "x")

    assert.spy(on_submit).called_with(lines[2])
  end)

  it("calls o.on_close when menu is closed", function()
    local on_close = spy.on(callbacks, "on_close")

    local lines = {
      Menu.item("Item 1", { id = 1 }),
      Menu.item("Item 2", { id = 2 }),
    }

    menu = Menu(popup_options, {
      lines = lines,
      on_close = on_close,
    })

    menu:mount()

    feedkeys("<Esc>", "x")

    assert.spy(on_close).called_with()
  end)

  describe("item", function()
    it("is prepared using o.prepare_item if provided", function()
      local items = {
        Menu.item("A"),
        Menu.separator("*"),
        Menu.item("B"),
      }

      local function prepare_item(item)
        return "-" .. item.text .. "-"
      end

      menu = Menu(popup_options, {
        lines = items,
        prepare_item = prepare_item,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, vim.tbl_map(prepare_item, items))
    end)

    it("is prepared when o.prepare_item is not provided", function()
      local items = {
        Menu.item("A"),
        Menu.separator("*"),
        Menu.item("B"),
      }

      popup_options.border = "single"

      menu = Menu(popup_options, {
        lines = items,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        "─*──",
        "B",
      })
    end)

    it("is skipped respecting o.should_skip_item if provided", function()
      local on_change = spy.on(callbacks, "on_change")

      local items = {
        Menu.item("-"),
        Menu.item("A", { id = 1 }),
        Menu.item("-"),
        Menu.item("B", { id = 2 }),
        Menu.item("-"),
      }

      menu = Menu(popup_options, {
        lines = items,
        on_change = on_change,
        should_skip_item = function(item)
          return not item.id
        end,
      })

      menu:mount()

      assert.spy(on_change).called_with(items[2], menu)
      on_change:clear()

      feedkeys("j", "x")
      assert.spy(on_change).called_with(items[4], menu)
      on_change:clear()

      feedkeys("j", "x")
      assert.spy(on_change).called_with(items[2], menu)
      on_change:clear()
    end)

    it("supports table with key .text", function()
      local text = "text"

      local items = {
        Menu.item({ text = text }),
      }

      menu = Menu(popup_options, {
        lines = items,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        text,
      })
    end)

    it("supports nui.text", function()
      local hl_group = "NuiMenuTest"
      local text = "text"
      local items = {
        Menu.item(Text(text, hl_group)),
      }

      menu = Menu(popup_options, {
        lines = items,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        text,
      })

      h.assert_highlight(menu.bufnr, menu.ns_id, 1, text, hl_group)
    end)

    it("supports nui.line", function()
      local hl_group = "NuiMenuTest"
      local text = "text"
      local items = {
        Menu.item(Line({ Text(text, hl_group) })),
      }

      menu = Menu(popup_options, {
        lines = items,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        text,
      })

      h.assert_highlight(menu.bufnr, menu.ns_id, 1, text, hl_group)
    end)

    it("content longer than max_width is truncated", function()
      local items = {
        Menu.item({ text = "Item 10 -" }),
        Menu.item(Text("Item 20 -")),
        Menu.item(Line({ Text("Item 30 -") })),
        Menu.item(Line({ Text("Item 40"), Text(" -") })),
        Menu.item(Line({ Text("Item 50 -"), Text(" -") })),
      }

      menu = Menu(popup_options, {
        max_width = 7,
        lines = items,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "Item 1…",
        "Item 2…",
        "Item 3…",
        "Item 4…",
        "Item 5…",
      })
    end)
  end)

  it("can truncate content longer than max_width w/ multi-byte chars", function()
    menu = Menu(popup_options, {
      lines = {
        Menu.item("中文长度测试"),
        Menu.item("Test中英文测试"),
        Menu.item("Long Long Group"),
      },
      max_width = 11,
    })

    menu:mount()

    h.assert_buf_lines(menu.bufnr, {
      "中文长度测…",
      "Test中英文…",
      "Long Long …",
    })
  end)

  describe("separator", function()
    it("text supports string", function()
      menu = Menu(popup_options, {
        lines = {
          Menu.item("A"),
          Menu.separator("Group"),
        },
        min_width = 10,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        " Group    ",
      })
    end)

    it("content longer than max_width is truncated", function()
      menu = Menu(popup_options, {
        lines = {
          Menu.item("A"),
          Menu.separator("Long Long Group"),
        },
        max_width = 10,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        " Long Lo… ",
      })
    end)

    it("text supports nui.text", function()
      local hl_group = "NuiMenuTest"
      local text = "Group"

      menu = Menu(popup_options, {
        lines = {
          Menu.item("A"),
          Menu.separator(Text(text, hl_group)),
        },
        min_width = 10,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        " Group    ",
      })

      h.assert_highlight(menu.bufnr, menu.ns_id, 2, text, hl_group)
    end)

    it("text supports nui.line", function()
      local hl_group = "NuiMenuTest"
      local text = "Group"

      menu = Menu(popup_options, {
        lines = {
          Menu.item("A"),
          Menu.separator(Line({ Text(text, hl_group), Text(" nui.text") })),
        },
        min_width = 10,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        " Group nui.t… ",
      })

      h.assert_highlight(menu.bufnr, menu.ns_id, 2, text, hl_group)
    end)

    it("o.char supports string", function()
      menu = Menu(popup_options, {
        lines = {
          Menu.item("A"),
          Menu.separator("Group", {
            char = "*",
            text_align = "right",
          }),
        },
        min_width = 10,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        "****Group*",
      })
    end)

    it("o.char supports nui.text", function()
      local hl_group = "NuiMenuTest"

      menu = Menu(popup_options, {
        lines = {
          Menu.item("A"),
          Menu.separator("Group", {
            char = Text("*", hl_group),
            text_align = "center",
          }),
        },
        min_width = 10,
      })

      menu:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
        "**Group***",
      })

      local linenr = 2

      local extmarks = h.get_line_extmarks(menu.bufnr, menu.ns_id, linenr)

      eq(#extmarks, 4)
      h.assert_extmark(extmarks[1], linenr, "*", hl_group)
      h.assert_extmark(extmarks[2], linenr, "*", hl_group)
      h.assert_extmark(extmarks[3], linenr, "**", hl_group)
      h.assert_extmark(extmarks[4], linenr, "*", hl_group)
    end)
  end)

  describe("w/ Layout", function()
    it("can be used", function()
      menu = Menu({}, {
        lines = {
          Menu.item("A"),
        },
      })

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

      layout:mount()

      h.assert_buf_lines(menu.bufnr, {
        "A",
      })
    end)
  end)
end)
