// @vitest-environment jsdom

import {
  cleanup,
  fireEvent,
  render,
  screen,
  waitFor,
  within,
} from "@testing-library/react"
import { afterEach, describe, expect, it } from "vitest"

import {
  Date,
  defineLexicalAction,
  DraggableBlocks,
  Image,
  LexicalActions,
  LexicalContent,
  LexicalFixedToolbar,
  LexicalRoot,
  Link,
  Video,
} from "."

afterEach(cleanup)

function RegisteredPlugin() {
  return <span data-testid="registered-plugin">registered</span>
}

const RegisteredCapability = defineLexicalAction({
  name: "registered-capability",
  label: "Registered capability",
  hidden: true,
  plugins: [RegisteredPlugin],
  execute: () => undefined,
})

describe("Lexical advanced actions", () => {
  it("registers non-visual plugins through an action declaration", () => {
    render(
      <LexicalRoot value="" onChange={() => undefined}>
        <LexicalActions>
          <RegisteredCapability />
        </LexicalActions>
        <LexicalContent />
      </LexicalRoot>
    )

    expect(screen.getByTestId("registered-plugin").textContent).toBe(
      "registered"
    )
  })

  it("registers draggable blocks declaratively", async () => {
    const { container } = render(
      <LexicalRoot value="<p>可拖动内容</p>" onChange={() => undefined}>
        <LexicalActions>
          <DraggableBlocks />
        </LexicalActions>
        <LexicalContent />
      </LexicalRoot>
    )

    await waitFor(() => {
      expect(
        container
          .querySelector("[contenteditable=true]")
          ?.classList.contains("lexical-draggable-content")
      ).toBe(true)
    })
  })

  it("registers link nodes and plugins through the Link action", async () => {
    const { container } = render(
      <LexicalRoot
        value='<p><a href="https://lexical.dev/">Lexical</a></p>'
        onChange={() => undefined}
      >
        <LexicalActions>
          <Link />
        </LexicalActions>
        <LexicalContent />
      </LexicalRoot>
    )

    const link = await waitFor(() => {
      const element = container.querySelector("a")
      expect(element).not.toBeNull()
      return element!
    })
    Object.defineProperty(link, "getBoundingClientRect", {
      value: () => ({
        bottom: 80,
        height: 20,
        left: 20,
        right: 120,
        top: 60,
        width: 100,
        x: 20,
        y: 60,
        toJSON: () => undefined,
      }),
    })
    fireEvent.click(link)

    expect(
      await screen.findByRole("button", { name: "Edit link" })
    ).not.toBeNull()
  })

  it("registers the Date node and popover through the Date action", async () => {
    const { container } = render(
      <LexicalRoot
        value='<p><time datetime="2026-07-30" data-lexical-date="2026-07-30">July 30, 2026</time></p>'
        onChange={() => undefined}
      >
        <LexicalActions>
          <Date />
        </LexicalActions>
        <LexicalContent />
      </LexicalRoot>
    )

    const date = await waitFor(() => {
      const element = container.querySelector<HTMLElement>(
        "[data-lexical-date]"
      )
      expect(element).not.toBeNull()
      return element!
    })
    fireEvent.click(date)

    expect(await screen.findByRole("grid")).not.toBeNull()
  })

  it("lets a custom Image control insert and operate on an image", async () => {
    const { container } = render(
      <LexicalRoot value="<p>before</p>" onChange={() => undefined}>
        <LexicalActions>
          <Image>
            {({ execute }) => (
              <button
                type="button"
                onClick={() =>
                  execute({
                    alt: "Custom image",
                    caption: "Uploaded externally",
                    src: "/custom-image.png",
                  })
                }
              >
                Upload image
              </button>
            )}
          </Image>
        </LexicalActions>
        <LexicalFixedToolbar />
        <LexicalContent />
      </LexicalRoot>
    )

    fireEvent.click(screen.getByRole("button", { name: "Upload image" }))

    const image = await screen.findByRole("img", { name: "Custom image" })
    expect(image.getAttribute("src")).toBe("/custom-image.png")
    expect(screen.getByText("Uploaded externally")).not.toBeNull()

    fireEvent.click(image)

    await waitFor(() => {
      expect(
        container.querySelectorAll("[data-lexical-image-resize-handle]")
      ).toHaveLength(8)
    })

    fireEvent.click(screen.getByRole("button", { name: "Align image right" }))
    await waitFor(() => {
      expect(image.closest("figure")?.classList.contains("items-end")).toBe(
        true
      )
    })

    expect(screen.getByRole("button", { name: "Delete image" })).not.toBeNull()

    fireEvent.click(screen.getByRole("button", { name: "Edit caption" }))
    const caption = screen.getByRole("textbox", { name: "Caption" })
    fireEvent.change(caption, { target: { value: "Updated caption" } })

    expect((caption as HTMLTextAreaElement).value).toBe("Updated caption")
  })

  it("reads an image selected from the insert dialog as a Base64 data URL", async () => {
    const { container } = render(
      <LexicalRoot value="<p>before</p>" onChange={() => undefined}>
        <LexicalActions>
          <Image />
        </LexicalActions>
        <LexicalFixedToolbar />
        <LexicalContent />
      </LexicalRoot>
    )

    fireEvent.click(screen.getByRole("button", { name: "Insert image" }))
    const dialog = await screen.findByRole("dialog")

    const file = new File([new Uint8Array([137, 80, 78, 71])], "local.png", {
      type: "image/png",
    })
    fireEvent.change(within(dialog).getByLabelText("Image file"), {
      target: { files: [file] },
    })

    const urlInput = within(dialog).getByRole("textbox", {
      name: "Image URL",
    })
    await waitFor(() => {
      expect((urlInput as HTMLInputElement).value).toMatch(
        /^data:image\/png;base64,/
      )
    })

    fireEvent.change(
      within(dialog).getByRole("textbox", { name: "Alternative text" }),
      {
        target: { value: "Local image" },
      }
    )
    fireEvent.submit(dialog.querySelector("form")!)

    await waitFor(() => {
      expect(
        screen.getByRole("img", { name: "Local image" }).getAttribute("src")
      ).toMatch(/^data:image\/png;base64,/)
    })
    expect(container.querySelector("figure")).not.toBeNull()
  })

  it("lets a custom Video control insert a video through execute", async () => {
    const { container } = render(
      <LexicalRoot value="<p>before</p>" onChange={() => undefined}>
        <LexicalActions>
          <Video>
            {({ execute }) => (
              <button
                type="button"
                onClick={() =>
                  execute({
                    caption: "Uploaded externally",
                    poster: "/custom-poster.png",
                    src: "/custom-video.mp4",
                  })
                }
              >
                Upload video
              </button>
            )}
          </Video>
        </LexicalActions>
        <LexicalFixedToolbar />
        <LexicalContent />
      </LexicalRoot>
    )

    fireEvent.click(screen.getByRole("button", { name: "Upload video" }))

    await waitFor(() => {
      const video = container.querySelector("video")
      expect(video?.getAttribute("src")).toBe("/custom-video.mp4")
      expect(video?.getAttribute("poster")).toBe("/custom-poster.png")
    })
  })
})
