pcall(require, "luacov")

local Text = require("nui.text")
local Tree = require("nui.tree")
local h = require("tests.helpers")

local eq = h.eq

describe("nui.tree", function()
  local winid, bufnr

  before_each(function()
    winid = vim.api.nvim_get_current_win()
    bufnr = vim.api.nvim_create_buf(false, true)

    vim.api.nvim_win_set_buf(winid, bufnr)
  end)

  after_each(function()
    vim.api.nvim_buf_delete(bufnr, { force = true })
  end)

  describe("(#deprecated) o.winid", function()
    it("throws if missing", function()
      local ok, err = pcall(function()
        return Tree({})
      end)
      eq(ok, false)
      eq(type(string.match(err, "missing bufnr")), "string")
    end)

    it("throws if invalid", function()
      local ok, err = pcall(function()
        return Tree({ winid = 999 })
      end)
      eq(ok, false)
      eq(type(string.match(err, "invalid winid ")), "string")
    end)

    it("sets t.winid and t.bufnr properly", function()
      local tree = Tree({ winid = winid })

      eq(tree.winid, winid)
      eq(tree.bufnr, bufnr)
    end)
  end)

  describe("o.bufnr", function()
    it("throws if missing", function()
      local ok, err = pcall(function()
        return Tree({})
      end)
      eq(ok, false)
      eq(type(string.match(err, "missing bufnr")), "string")
    end)

    it("throws if invalid", function()
      local ok, err = pcall(function()
        return Tree({ bufnr = 999 })
      end)
      eq(ok, false)
      eq(type(string.match(err, "invalid bufnr ")), "string")
    end)

    it("sets t.bufnr properly", function()
      local tree = Tree({ bufnr = bufnr })

      eq(tree.winid, nil)
      eq(tree.bufnr, bufnr)
    end)
  end)

  it("throws on duplicated node id", function()
    local ok, err = pcall(function()
      return Tree({
        bufnr = bufnr,
        nodes = {
          Tree.Node({ id = "id", text = "text" }),
          Tree.Node({ id = "id", text = "text" }),
        },
      })
    end)
    eq(ok, false)
    eq(type(err), "string")
  end)

  it("sets default buf options emulating scratch-buffer", function()
    local tree = Tree({ bufnr = bufnr })

    h.assert_buf_options(tree.bufnr, {
      bufhidden = "hide",
      buflisted = false,
      buftype = "nofile",
      swapfile = false,
    })
  end)

  describe("(#deprecated) o.win_options", function()
    it("sets default values for handling folds", function()
      local tree = Tree({ winid = winid })

      h.assert_win_options(tree.winid, {
        foldmethod = "manual",
        foldcolumn = "0",
        wrap = false,
      })
    end)

    it("sets values", function()
      local initial_statusline = vim.api.nvim_win_get_option(winid, "statusline")

      local statusline = "test: win_options " .. math.random()
      local tree = Tree({
        winid = winid,
        win_options = {
          statusline = statusline,
        },
      })

      h.assert_win_options(tree.winid, {
        statusline = statusline,
      })

      vim.api.nvim_win_set_option(tree.winid, "statusline", initial_statusline)
    end)

    it("has no effect if o.bufnr is present", function()
      local initial_statusline = vim.api.nvim_win_get_option(winid, "statusline")

      Tree({
        bufnr = bufnr,
        win_options = {
          statusline = "test: win_options" .. math.random(),
        },
      })

      h.assert_win_options(winid, {
        statusline = initial_statusline,
      })
    end)
  end)

  it("sets t.ns_id if o.ns_id is string", function()
    local ns = "NuiTreeTest"
    local tree = Tree({ bufnr = bufnr, ns_id = ns })

    local namespaces = vim.api.nvim_get_namespaces()

    eq(tree.ns_id, namespaces[ns])
  end)

  it("sets t.ns_id if o.ns_id is number", function()
    local ns = "NuiTreeTest"
    local ns_id = vim.api.nvim_create_namespace(ns)
    local tree = Tree({ bufnr = bufnr, ns_id = ns_id })

    eq(tree.ns_id, ns_id)
  end)

  it("uses o.get_node_id if provided", function()
    local node_d2 = Tree.Node({ key = "depth two" })
    local node_d1 = Tree.Node({ key = "depth one" }, { node_d2 })
    Tree({
      bufnr = bufnr,
      nodes = { node_d1 },
      get_node_id = function(node)
        return node.key
      end,
    })

    eq(node_d1:get_id(), node_d1.key)
    eq(node_d2:get_id(), node_d2.key)
  end)

  describe("default get_node_id", function()
    it("returns id using n.id", function()
      local node = Tree.Node({ id = "id", text = "text" })
      Tree({ bufnr = bufnr, nodes = { node } })

      eq(node:get_id(), "-id")
    end)

    it("returns id using parent_id + depth + n.text", function()
      local node_d2 = Tree.Node({ text = { "depth two a", Text("depth two b") } })
      local node_d1 = Tree.Node({ text = "depth one" }, { node_d2 })
      Tree({ bufnr = bufnr, nodes = { node_d1 } })

      eq(node_d1:get_id(), string.format("-%s-%s", node_d1:get_depth(), node_d1.text))
      eq(
        node_d2:get_id(),
        string.format(
          "%s-%s-%s",
          node_d2:get_parent_id(),
          node_d2:get_depth(),
          table.concat({ node_d2.text[1], node_d2.text[2]:content() }, "-")
        )
      )
    end)

    it("returns id using random number", function()
      math.randomseed(0)
      local expected_id = "-" .. math.random()
      math.randomseed(0)

      local node = Tree.Node({})
      Tree({ bufnr = bufnr, nodes = { node } })

      eq(node:get_id(), expected_id)
    end)
  end)

  it("uses o.prepare_node if provided", function()
    local function prepare_node(node, parent_node)
      if not parent_node then
        return node.text
      end

      return parent_node.text .. ":" .. node.text
    end

    local nodes = {
      Tree.Node({ text = "a" }),
      Tree.Node({ text = "b" }, {
        Tree.Node({ text = "b-1" }),
        Tree.Node({ text = "b-2" }),
      }),
      Tree.Node({ text = "c" }),
    }

    nodes[2]:expand()

    local tree = Tree({
      bufnr = bufnr,
      nodes = nodes,
      prepare_node = prepare_node,
    })

    tree:render()

    h.assert_buf_lines(tree.bufnr, {
      "a",
      "b",
      "b:b-1",
      "b:b-2",
      "c",
    })
  end)

  describe("default prepare_node", function()
    it("throws if missing n.text", function()
      local nodes = {
        Tree.Node({ txt = "a" }),
        Tree.Node({ txt = "b" }),
        Tree.Node({ txt = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
      })

      local ok, err = pcall(tree.render, tree)
      eq(ok, false)
      eq(type(err), "string")
    end)

    it("uses n.text", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = { "b-1", "b-2" } }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
      })

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        "  b-1",
        "  b-2",
        "  c",
      })
    end)

    it("renders arrow if children are present", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, {
          Tree.Node({ text = "b-1" }),
          Tree.Node({ text = { "b-2", "b-3" } }),
        }),
        Tree.Node({ text = "c" }),
      }
      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
      })

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        " b",
        "  c",
      })

      nodes[2]:expand()
      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        " b",
        "    b-1",
        "    b-2",
        "    b-3",
        "  c",
      })
    end)
  end)

  describe("method :get_node", function()
    it("can get node under cursor", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
      })

      tree:render()

      local linenr = 3

      vim.api.nvim_win_set_cursor(winid, { linenr, 0 })

      eq({ tree:get_node() }, { nodes[3], linenr, linenr })
    end)

    it("can get node with id", function()
      local b_node_children = {
        Tree.Node({ text = "b-1" }),
        Tree.Node({ text = { "b-2", "b-3" } }),
      }

      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, b_node_children),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return type(node.text) == "table" and table.concat(node.text, "-") or node.text
        end,
      })

      tree:render()

      eq({ tree:get_node("b") }, { nodes[2], 2, 2 })

      tree:get_node("b"):expand()
      tree:render()

      eq({ tree:get_node("b-2-b-3") }, { b_node_children[2], 4, 5 })
    end)

    it("can get node on linenr", function()
      local b_node_children = {
        Tree.Node({ id = "b-1-b-2", text = { "b-1", "b-2" } }),
      }

      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, b_node_children),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
      })

      tree:render()

      eq({ tree:get_node(1) }, { nodes[1], 1, 1 })

      tree:get_node(2):expand()
      tree:render()

      eq({ tree:get_node(3) }, { b_node_children[1], 3, 4 })
      eq({ tree:get_node(4) }, { b_node_children[1], 3, 4 })
    end)
  end)

  describe("method :get_nodes", function()
    it("can get nodes at root", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, {
          Tree.Node({ text = "b-1" }),
        }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      eq(tree:get_nodes(), nodes)
    end)

    it("can get nodes under parent node", function()
      local child_nodes = {
        Tree.Node({ text = "b-1" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = {
          Tree.Node({ text = "a" }),
          Tree.Node({ text = "b" }, child_nodes),
        },
        get_node_id = function(node)
          return node.text
        end,
      })

      eq(tree:get_nodes("b"), child_nodes)
    end)
  end)

  describe("method :add_node", function()
    it("throw if invalid parent_id", function()
      local tree = Tree({
        bufnr = bufnr,
        nodes = {
          Tree.Node({ text = "x" }),
        },
      })

      local ok, err = pcall(tree.add_node, tree, Tree.Node({ text = "y" }), "invalid_parent_id")
      eq(ok, false)
      eq(type(err), "string")
    end)

    it("can add node at root", function()
      local tree = Tree({
        bufnr = bufnr,
        nodes = {
          Tree.Node({ text = "x" }),
        },
      })

      tree:add_node(Tree.Node({ text = "y" }))

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  x",
        "  y",
      })

      tree:add_node(Tree.Node({ text = "z" }))

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  x",
        "  y",
        "  z",
      })
    end)

    it("can add node under parent node", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, {
          Tree.Node({ text = "b-1" }),
        }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      tree:add_node(Tree.Node({ text = "b-2" }), "b")

      tree:get_node("b"):expand()

      tree:add_node(Tree.Node({ text = "c-1" }), "c")

      tree:get_node("c"):expand()

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        " b",
        "    b-1",
        "    b-2",
        " c",
        "    c-1",
      })
    end)
  end)

  describe("method :set_nodes", function()
    it("throw if invalid parent_id", function()
      local tree = Tree({
        bufnr = bufnr,
        nodes = {
          Tree.Node({ text = "x" }),
        },
      })

      local ok, err = pcall(tree.set_nodes, tree, {}, "invalid_parent_id")
      eq(ok, false)
      eq(type(err), "string")
    end)

    it("can set nodes at root", function()
      local tree = Tree({
        bufnr = bufnr,
        nodes = {
          Tree.Node({ text = "x" }),
        },
      })

      tree:set_nodes({
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }),
      })

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        "  b",
      })

      tree:set_nodes({
        Tree.Node({ text = "c" }),
      })

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  c",
      })
    end)

    it("can set nodes under parent node", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, {
          Tree.Node({ text = "b-1" }),
        }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      tree:set_nodes({
        Tree.Node({ text = "b-2" }),
      }, "b")

      tree:get_node("b"):expand()

      tree:set_nodes({
        Tree.Node({ text = "c-1" }),
        Tree.Node({ text = "c-2" }),
      }, "c")

      tree:get_node("c"):expand()

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        " b",
        "    b-2",
        " c",
        "    c-1",
        "    c-2",
      })
    end)
  end)

  describe("method :remove_node", function()
    it("can remove node w/o parent", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, {
          Tree.Node({ text = "b-1" }),
        }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      tree:remove_node("a")

      tree:get_node("b"):expand()

      tree:render()

      eq(
        vim.tbl_map(function(node)
          return node:get_id()
        end, tree:get_nodes()),
        { "b", "c" }
      )

      h.assert_buf_lines(tree.bufnr, {
        " b",
        "    b-1",
        "  c",
      })
    end)

    it("can remove node w/ parent", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, {
          Tree.Node({ text = "b-1" }),
        }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      tree:remove_node("b-1")

      tree:render()

      eq(tree:get_node("b"):get_child_ids(), {})

      h.assert_buf_lines(tree.bufnr, {
        "  a",
        "  b",
        "  c",
      })
    end)

    it("removes children nodes recursively", function()
      local nodes = {
        Tree.Node({ text = "a" }, {
          Tree.Node({ text = "a-1" }, {
            Tree.Node({ text = "a-1-x" }),
          }),
        }),
      }
      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })
      h.neq(tree:get_node("a"), nil)
      h.neq(tree:get_node("a-1"), nil)
      h.neq(tree:get_node("a-1-x"), nil)

      tree:remove_node("a")

      eq(tree:get_node("a"), nil)
      eq(tree:get_node("a-1"), nil)
      eq(tree:get_node("a-1-x"), nil)
    end)
  end)

  describe("method :render", function()
    it("handles unexpected case of missing node", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      -- this should not happen normally
      tree.nodes.by_id["a"] = nil

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "  b",
        "  c",
      })
    end)

    it("skips node if o.prepare_node returns nil", function()
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }),
        Tree.Node({ text = "c" }),
      }

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
        prepare_node = function(node)
          if node:get_id() == "b" then
            return nil
          end

          return node.text
        end,
      })

      tree:render()

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

    it("supports param linenr_start", function()
      local b_node_children = {
        Tree.Node({ text = "b-1" }),
        Tree.Node({ text = "b-2" }),
      }
      local nodes = {
        Tree.Node({ text = "a" }),
        Tree.Node({ text = "b" }, b_node_children),
      }

      vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
        "NuiTreeTest",
        "",
        "NuiTreeTest",
      })

      local tree = Tree({
        bufnr = bufnr,
        nodes = nodes,
        get_node_id = function(node)
          return node.text
        end,
      })

      tree:render(2)

      h.assert_buf_lines(tree.bufnr, {
        "NuiTreeTest",
        "  a",
        " b",
        "NuiTreeTest",
      })

      nodes[2]:expand()

      tree:render()

      h.assert_buf_lines(tree.bufnr, {
        "NuiTreeTest",
        "  a",
        " b",
        "    b-1",
        "    b-2",
        "NuiTreeTest",
      })

      nodes[2]:collapse()

      tree:render(3)

      h.assert_buf_lines(tree.bufnr, {
        "NuiTreeTest",
        "",
        "  a",
        " b",
        "NuiTreeTest",
      })
    end)
  end)
end)

describe("nui.tree.Node", function()
  describe("method :has_children", function()
    it("works before initialization", function()
      local node_wo_children = Tree.Node({ text = "a" })
      local node_w_children = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })

      eq(node_wo_children._initialized, false)
      eq(node_wo_children:has_children(), false)

      eq(node_w_children._initialized, false)
      eq(type(node_w_children.__children), "table")
      eq(node_w_children:has_children(), true)
    end)

    it("works after initialization", function()
      local node_wo_children = Tree.Node({ text = "a" })
      local node_w_children = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })

      Tree({
        bufnr = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()),
        nodes = { node_wo_children, node_w_children },
      })

      eq(node_wo_children._initialized, true)
      eq(node_wo_children:has_children(), false)

      eq(node_w_children._initialized, true)
      eq(type(node_w_children.__children), "nil")
      eq(node_w_children:has_children(), true)
    end)
  end)

  describe("method :expand", function()
    it("returns true if not already expanded", function()
      local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
      eq(node:is_expanded(), false)
      eq(node:expand(), true)
      eq(node:is_expanded(), true)
    end)

    it("returns false if already expanded", function()
      local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
      node:expand()
      eq(node:is_expanded(), true)
      eq(node:expand(), false)
      eq(node:is_expanded(), true)
    end)

    it("does work w/ zero child", function()
      local node = Tree.Node({ text = "a" }, {})
      eq(node:is_expanded(), false)
      eq(node:expand(), true)
      eq(node:is_expanded(), true)
    end)

    it("does not work w/o children", function()
      local node = Tree.Node({ text = "a" })
      eq(node:is_expanded(), false)
      eq(node:expand(), false)
      eq(node:is_expanded(), false)
    end)
  end)

  describe("method :collapse", function()
    it("returns true if not already collapsed", function()
      local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
      node:expand()
      eq(node:is_expanded(), true)
      eq(node:collapse(), true)
      eq(node:is_expanded(), false)
    end)

    it("returns false if already collapsed", function()
      local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) })
      eq(node:is_expanded(), false)
      eq(node:collapse(), false)
      eq(node:is_expanded(), false)
    end)

    it("does not work w/o children", function()
      local node = Tree.Node({ text = "a" })
      eq(node:is_expanded(), false)
      eq(node:collapse(), false)
      eq(node:is_expanded(), false)
    end)
  end)
end)
