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

function loadedFile(
  content: string,
  language?: string,
): GitCodeEditorLoadedFile {
  return { content, language }
}

describe("GitCodeEditor (props-based composition)", () => {
  it("mounts without error", async () => {
    const wrapper = mount(GitCodeEditor, { props: { label: "Editor" } })
    await flushPromises()
    expect(wrapper.exists()).toBe(true)
  })

  it("renders the default label", async () => {
    const wrapper = mount(GitCodeEditor)
    await flushPromises()
    expect(wrapper.text()).toContain("Git Code Editor")
  })

  it("renders the repository chip when provided", async () => {
    const wrapper = mount(GitCodeEditor, {
      props: { repository: "gramercy/plugins" },
    })
    await flushPromises()
    const chip = wrapper.find("[data-testid='git-code-editor-repository']")
    expect(chip.exists()).toBe(true)
    expect(chip.text()).toContain("gramercy/plugins")
  })

  it("calls onLoadBranches on mount", async () => {
    const onLoadBranches = vi.fn().mockResolvedValue(["main", "develop"])
    mount(GitCodeEditor, { props: { onLoadBranches } })
    await flushPromises()
    expect(onLoadBranches).toHaveBeenCalledTimes(1)
  })

  it("calls onLoadFile on mount when path and branch are set", async () => {
    const onLoadFile = vi
      .fn<
        (p: string, b: string) => Promise<GitCodeEditorLoadedFile>
      >()
      .mockResolvedValue(loadedFile("hello"))
    mount(GitCodeEditor, {
      props: { path: "README.md", branch: "main", onLoadFile },
    })
    await flushPromises()
    expect(onLoadFile).toHaveBeenCalledWith("README.md", "main")
  })

  it("populates modelValue from the loaded file", async () => {
    const onLoadFile = vi
      .fn<
        (p: string, b: string) => Promise<GitCodeEditorLoadedFile>
      >()
      .mockResolvedValue(loadedFile("file body", "markdown"))
    const wrapper = mount(GitCodeEditor, {
      props: { path: "README.md", branch: "main", onLoadFile },
    })
    await flushPromises()
    const ta = wrapper.find(
      "[data-testid='git-code-editor-textarea']",
    ).element as HTMLTextAreaElement
    expect(ta.value).toBe("file body")
    expect(wrapper.emitted("update:modelValue")?.flat()).toContain("file body")
    expect(wrapper.text()).toContain("markdown")
  })

  it("re-loads when path changes", async () => {
    const onLoadFile = vi
      .fn<
        (p: string, b: string) => Promise<GitCodeEditorLoadedFile>
      >()
      .mockResolvedValue(loadedFile("a"))
    const wrapper = mount(GitCodeEditor, {
      props: { path: "a.txt", branch: "main", onLoadFile },
    })
    await flushPromises()
    expect(onLoadFile).toHaveBeenCalledTimes(1)
    await wrapper.setProps({ path: "b.txt" })
    await flushPromises()
    expect(onLoadFile).toHaveBeenCalledWith("b.txt", "main")
    expect(onLoadFile).toHaveBeenCalledTimes(2)
  })

  it("re-loads when branch changes", async () => {
    const onLoadFile = vi
      .fn<
        (p: string, b: string) => Promise<GitCodeEditorLoadedFile>
      >()
      .mockResolvedValue(loadedFile("a"))
    const wrapper = mount(GitCodeEditor, {
      props: { path: "a.txt", branch: "main", onLoadFile },
    })
    await flushPromises()
    expect(onLoadFile).toHaveBeenCalledTimes(1)
    await wrapper.setProps({ branch: "develop" })
    await flushPromises()
    expect(onLoadFile).toHaveBeenCalledWith("a.txt", "develop")
    expect(onLoadFile).toHaveBeenCalledTimes(2)
  })

  it("renders a loading indicator while the file is being loaded", async () => {
    let resolve: (v: GitCodeEditorLoadedFile) => void = () => {}
    const onLoadFile = vi.fn(
      () =>
        new Promise<GitCodeEditorLoadedFile>((r) => {
          resolve = r
        }),
    )
    const wrapper = mount(GitCodeEditor, {
      props: { path: "a.txt", branch: "main", onLoadFile },
    })
    await flushPromises()
    expect(wrapper.find("[data-testid='git-code-editor-loading']").exists()).toBe(
      true,
    )
    resolve(loadedFile("hello"))
    await flushPromises()
    expect(wrapper.find("[data-testid='git-code-editor-loading']").exists()).toBe(
      false,
    )
  })

  it("shows a file-load error in the FieldFrame errors area", async () => {
    const onLoadFile = vi
      .fn<
        (p: string, b: string) => Promise<GitCodeEditorLoadedFile>
      >()
      .mockRejectedValue(new Error("file gone"))
    const wrapper = mount(GitCodeEditor, {
      props: { path: "a.txt", branch: "main", onLoadFile },
    })
    await flushPromises()
    expect(wrapper.text()).toContain("file gone")
  })

  it("hides the Save button when onSaveFile is not provided", async () => {
    const wrapper = mount(GitCodeEditor, {
      props: { path: "a.txt", branch: "main" },
    })
    await flushPromises()
    expect(wrapper.find("[data-testid='git-code-editor-save']").exists()).toBe(
      false,
    )
  })

  it("calls onSaveFile when the Save button is clicked", async () => {
    const onLoadFile = vi
      .fn<
        (p: string, b: string) => Promise<GitCodeEditorLoadedFile>
      >()
      .mockResolvedValue(loadedFile("hello"))
    const onSaveFile = vi
      .fn<(p: string, b: string, c: string) => Promise<void>>()
      .mockResolvedValue(undefined)
    const wrapper = mount(GitCodeEditor, {
      props: { path: "a.txt", branch: "main", onLoadFile, onSaveFile },
    })
    await flushPromises()
    const btn = wrapper.find("[data-testid='git-code-editor-save']")
    expect(btn.exists()).toBe(true)
    await btn.trigger("click")
    await flushPromises()
    expect(onSaveFile).toHaveBeenCalledWith("a.txt", "main", "hello")
  })

  it("emits update:modelValue when the textarea changes", async () => {
    const wrapper = mount(GitCodeEditor, {
      props: { branch: "main" },
    })
    await flushPromises()
    const ta = wrapper.find("[data-testid='git-code-editor-textarea']")
    ;(ta.element as HTMLTextAreaElement).value = "new contents"
    await ta.trigger("input")
    expect(wrapper.emitted("update:modelValue")?.flat()).toContain(
      "new contents",
    )
  })
})
