import { describe, expect, it, vi } from "vitest"
import type { Mock } from "vitest"
import { flushPromises, mount } from "@vue/test-utils"
import FileTree from "./FileTree.vue"
import type { TreeNode } from "./FileTree.vue"

const ROOT_NODES: TreeNode[] = [
  { path: "src", name: "src", kind: "directory" },
  { path: "README.md", name: "README.md", kind: "file" },
  { path: "package.json", name: "package.json", kind: "file" },
]

const SRC_CHILDREN: TreeNode[] = [
  { path: "src/Button.vue", name: "Button.vue", kind: "file" },
  { path: "src/Card.vue", name: "Card.vue", kind: "file" },
  { path: "src/lib", name: "lib", kind: "directory" },
]

type LoadTree = (path: string) => Promise<TreeNode[]>

function makeLoader(): { fn: Mock<LoadTree> } {
  const fn = vi.fn<LoadTree>(async (path: string) => {
    if (path === "" || path === "/") {
      return ROOT_NODES
    }
    if (path === "src") {
      return SRC_CHILDREN
    }
    return []
  })
  return { fn }
}

describe("FileTree (props-based composition)", () => {
  it("calls onLoadTree on mount with the root path", async () => {
    const { fn } = makeLoader()
    mount(FileTree, { props: { onLoadTree: fn } })
    await flushPromises()
    expect(fn).toHaveBeenCalledWith("")
  })

  it("renders the default label", async () => {
    const { fn } = makeLoader()
    const wrapper = mount(FileTree, { props: { onLoadTree: fn } })
    await flushPromises()
    expect(wrapper.text()).toContain("File Tree")
  })

  it("uses a custom label when provided", async () => {
    const { fn } = makeLoader()
    const wrapper = mount(FileTree, {
      props: { onLoadTree: fn, label: "Project files" },
    })
    await flushPromises()
    expect(wrapper.text()).toContain("Project files")
  })

  it("renders skeletons while loading", async () => {
    let resolve: (v: TreeNode[]) => void = () => {}
    const onLoadTree = vi.fn(
      () =>
        new Promise<TreeNode[]>((r) => {
          resolve = r
        }),
    )
    const wrapper = mount(FileTree, { props: { onLoadTree } })
    await wrapper.vm.$nextTick()
    expect(wrapper.find("[data-testid='file-tree-loading']").exists()).toBe(
      true,
    )
    resolve(ROOT_NODES)
    await flushPromises()
    expect(wrapper.find("[data-testid='file-tree-loading']").exists()).toBe(
      false,
    )
  })

  it("renders root nodes after the loader resolves", async () => {
    const { fn } = makeLoader()
    const wrapper = mount(FileTree, { props: { onLoadTree: fn } })
    await flushPromises()
    expect(wrapper.text()).toContain("src")
    expect(wrapper.text()).toContain("README.md")
    expect(wrapper.text()).toContain("package.json")
  })

  it("shows an error message when the loader rejects", async () => {
    const onLoadTree = vi
      .fn<(path: string) => Promise<TreeNode[]>>()
      .mockRejectedValue(new Error("boom"))
    const wrapper = mount(FileTree, { props: { onLoadTree } })
    await flushPromises()
    const err = wrapper.find("[data-testid='file-tree-error']")
    expect(err.exists()).toBe(true)
    expect(err.text()).toContain("boom")
  })

  it("calls onSelectPath and emits update:selectedPath when a file row is clicked", async () => {
    const { fn } = makeLoader()
    const onSelectPath = vi.fn()
    const wrapper = mount(FileTree, {
      props: { onLoadTree: fn, onSelectPath },
    })
    await flushPromises()
    const fileRow = wrapper.find("[data-testid='file-tree-row-README.md']")
    expect(fileRow.exists()).toBe(true)
    await fileRow.trigger("click")
    expect(onSelectPath).toHaveBeenCalledWith("README.md")
    expect(wrapper.emitted("update:selectedPath")).toEqual([["README.md"]])
  })

  it("highlights the selected file via aria-selected", async () => {
    const { fn } = makeLoader()
    const wrapper = mount(FileTree, {
      props: { onLoadTree: fn, selectedPath: "README.md" },
    })
    await flushPromises()
    const fileRow = wrapper.find("[data-testid='file-tree-row-README.md']")
    expect(fileRow.attributes("aria-selected")).toBe("true")
  })

  it("expanding a directory triggers a deeper load", async () => {
    const { fn } = makeLoader()
    const wrapper = mount(FileTree, { props: { onLoadTree: fn } })
    await flushPromises()
    // First call was for the root
    expect(fn).toHaveBeenCalledTimes(1)
    const dirRow = wrapper.find("[data-testid='file-tree-row-src']")
    await dirRow.trigger("click")
    await flushPromises()
    expect(fn).toHaveBeenCalledWith("src")
    expect(fn).toHaveBeenCalledTimes(2)
    expect(wrapper.text()).toContain("Button.vue")
    expect(wrapper.text()).toContain("Card.vue")
  })

  it("toggles a directory closed without re-loading", async () => {
    const { fn } = makeLoader()
    const wrapper = mount(FileTree, { props: { onLoadTree: fn } })
    await flushPromises()
    const dirRow = wrapper.find("[data-testid='file-tree-row-src']")
    await dirRow.trigger("click")
    await flushPromises()
    expect(fn).toHaveBeenCalledTimes(2)
    // Toggle closed
    await dirRow.trigger("click")
    await flushPromises()
    expect(fn).toHaveBeenCalledTimes(2)
    expect(wrapper.text()).not.toContain("Button.vue")
  })

  it("emits update:expandedPaths and calls onExpandedChange on expand", async () => {
    const { fn } = makeLoader()
    const onExpandedChange = vi.fn()
    const wrapper = mount(FileTree, {
      props: { onLoadTree: fn, onExpandedChange },
    })
    await flushPromises()
    const dirRow = wrapper.find("[data-testid='file-tree-row-src']")
    await dirRow.trigger("click")
    await flushPromises()
    expect(onExpandedChange).toHaveBeenCalledWith(["src"])
    const emitted = wrapper.emitted("update:expandedPaths")
    expect(emitted?.[emitted.length - 1]).toEqual([["src"]])
  })

  it("renders pre-loaded children without invoking the loader for that path", async () => {
    const onLoadTree = vi.fn(async (path: string) => {
      if (path === "") {
        return [
          {
            path: "src",
            name: "src",
            kind: "directory",
            children: [{ path: "src/x.vue", name: "x.vue", kind: "file" }],
          },
        ] as TreeNode[]
      }
      return [] as TreeNode[]
    })
    const wrapper = mount(FileTree, {
      props: { onLoadTree, expandedPaths: ["src"] },
    })
    await flushPromises()
    expect(wrapper.text()).toContain("x.vue")
    // Loader called once for root only (pre-loaded children should not re-trigger).
    expect(onLoadTree).toHaveBeenCalledTimes(1)
  })
})
