// @vitest-environment jsdom

import * as React from "react"
import {
  act,
  cleanup,
  fireEvent,
  render,
  screen,
  waitFor,
} from "@testing-library/react"
import { afterEach, describe, expect, it, vi } from "vitest"

import { MessagePanel } from "./message-panel"
import type { DevtoolMessage, MessageRepository } from "./types"

const message: DevtoolMessage = {
  catalog: "messages",
  comments: ["Navigation title"],
  editable: true,
  id: "navigation.home",
  missing: true,
  obsolete: false,
  origins: [{ file: "src/navigation.tsx", line: 12 }],
  packageName: "@workspace/ui",
  source: "Home",
  sourceLocale: "en",
  translation: "",
}

afterEach(() => {
  cleanup()
  vi.restoreAllMocks()
  Reflect.deleteProperty(window.navigator, "clipboard")
})

describe("MessagePanel", () => {
  it("loads, filters, and saves messages through the repository", async () => {
    const updateMessage = vi.fn(async (input) => ({
      ...message,
      missing: false,
      translation: input.translation,
    }))
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [message]),
      updateMessage,
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    expect(await screen.findByText("navigation.home")).toBeTruthy()
    expect(screen.getByRole("region", { name: "@workspace/ui" })).toBeTruthy()

    const textarea = screen.getByLabelText(
      "navigation.home translation for zh-Hans"
    )
    expect(screen.queryByRole("button", { name: "Save" })).toBeNull()

    fireEvent.change(textarea, { target: { value: "首页" } })
    fireEvent.click(screen.getByRole("button", { name: "Save" }))

    await waitFor(() => {
      expect(updateMessage).toHaveBeenCalledWith({
        catalog: "messages",
        id: "navigation.home",
        locale: "zh-Hans",
        translation: "首页",
      })
    })
    expect(await screen.findByText("Saved")).toBeTruthy()

    const missingOnly = screen.getByRole("checkbox", {
      name: "Missing only",
    })
    const checkboxIcon = missingOnly.nextElementSibling

    expect(checkboxIcon?.querySelector("svg")).toBeTruthy()
    fireEvent.click(missingOnly)
    expect(missingOnly).toHaveProperty("checked", true)

    fireEvent.change(screen.getByPlaceholderText("Search messages"), {
      target: { value: "does not exist" },
    })
    expect(screen.getByText("No messages match your search.")).toBeTruthy()
  })

  it("keeps the current content visible during a background refresh", async () => {
    let resolveRefresh:
      | ((messages: readonly DevtoolMessage[]) => void)
      | undefined
    const repository: MessageRepository = {
      getMessages: vi
        .fn()
        .mockResolvedValueOnce([message])
        .mockImplementationOnce(
          () =>
            new Promise<readonly DevtoolMessage[]>((resolve) => {
              resolveRefresh = resolve
            })
        ),
      updateMessage: vi.fn(),
    }
    const view = render(
      <MessagePanel locale="zh-Hans" refreshToken={0} repository={repository} />
    )

    expect(await screen.findByText("navigation.home")).toBeTruthy()
    fireEvent.change(screen.getByPlaceholderText("Search messages"), {
      target: { value: "home" },
    })

    view.rerender(
      <MessagePanel locale="zh-Hans" refreshToken={1} repository={repository} />
    )

    expect(screen.queryByText("Loading zh-Hans…")).toBeNull()
    expect(screen.getByText("navigation.home")).toBeTruthy()
    expect(screen.getByPlaceholderText("Search messages")).toHaveProperty(
      "value",
      "home"
    )

    await act(async () => {
      resolveRefresh?.([])
    })

    expect(
      await screen.findByText(
        "No messages are available yet. Extract the project to populate this catalog."
      )
    ).toBeTruthy()
  })

  it("collapses a package without discarding an unsaved translation", async () => {
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [message]),
      updateMessage: vi.fn(),
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    const textarea = await screen.findByLabelText(
      "navigation.home translation for zh-Hans"
    )
    fireEvent.change(textarea, { target: { value: "未保存的首页" } })

    const collapseButton = screen.getByRole("button", {
      name: "Collapse @workspace/ui",
    })
    const packageContent = screen
      .getByRole("region", { name: "@workspace/ui" })
      .querySelector(".i18n-message-package__messages")
    fireEvent.click(collapseButton)

    expect(collapseButton.getAttribute("aria-expanded")).toBe("false")
    expect(textarea).toHaveProperty("value", "未保存的首页")
    expect(packageContent).toHaveProperty("hidden", true)

    fireEvent.click(
      screen.getByRole("button", {
        name: "Expand @workspace/ui",
      })
    )

    expect(textarea).toHaveProperty("value", "未保存的首页")
    expect(packageContent).toHaveProperty("hidden", false)
  })

  it("shows the application package even when it has no messages", async () => {
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [message]),
      getPackages: vi.fn(async () => [
        {
          kind: "application" as const,
          name: "web",
        },
      ]),
      updateMessage: vi.fn(),
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    expect(await screen.findByRole("region", { name: "web" })).toBeTruthy()
    expect(
      screen
        .getAllByRole("region")
        .map((region) => region.getAttribute("aria-label"))
        .filter((label) => label !== "Message editor")
    ).toEqual(["web", "@workspace/ui"])
    expect(
      screen.getByText("No messages were found in this package.")
    ).toBeTruthy()
    expect(screen.getByRole("region", { name: "web" }).textContent).toContain(
      "0 messages"
    )
  })

  it("builds an ID tree and preserves visited editor drafts", async () => {
    const activeMessage: DevtoolMessage = {
      ...message,
      id: "blocks.appearance.active",
      source: "Active",
    }
    const darkMessage: DevtoolMessage = {
      ...message,
      id: "blocks.theme.dark",
      source: "Dark",
    }
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [activeMessage, darkMessage]),
      updateMessage: vi.fn(),
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    const activeEditor = await screen.findByLabelText(
      "blocks.appearance.active translation for zh-Hans"
    )

    expect(screen.getByTitle("blocks")).toBeTruthy()
    expect(screen.getByTitle("blocks.appearance")).toBeTruthy()
    expect(screen.getByTitle("blocks.theme")).toBeTruthy()
    expect(screen.queryByTitle("blocks.appearance.active")).toBeNull()
    expect(screen.queryByTitle("blocks.theme.dark")).toBeNull()
    fireEvent.change(activeEditor, { target: { value: "使用中" } })
    fireEvent.click(screen.getByTitle("blocks.theme"))

    expect(
      await screen.findByLabelText("blocks.theme.dark translation for zh-Hans")
    ).toBeTruthy()

    fireEvent.click(screen.getByTitle("blocks.appearance"))
    expect(
      await screen.findByLabelText(
        "blocks.appearance.active translation for zh-Hans"
      )
    ).toHaveProperty("value", "使用中")
  })

  it("uses the final ID segment as right-pane content instead of a tree leaf", async () => {
    const parentMessage: DevtoolMessage = {
      ...message,
      id: "a.b",
      source: "B",
    }
    const childMessage: DevtoolMessage = {
      ...message,
      id: "a.b.c",
      source: "C",
    }
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [parentMessage, childMessage]),
      updateMessage: vi.fn(),
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    expect(
      await screen.findByLabelText("a.b translation for zh-Hans")
    ).toBeTruthy()
    expect(screen.getByTitle("a")).toBeTruthy()
    expect(screen.getByTitle("a.b")).toBeTruthy()
    expect(screen.queryByTitle("a.b.c")).toBeNull()
    expect(screen.queryByLabelText("a.b.c translation for zh-Hans")).toBeNull()

    fireEvent.click(screen.getByTitle("a.b"))

    expect(
      await screen.findByLabelText("a.b.c translation for zh-Hans")
    ).toBeTruthy()
  })

  it("resets an unsaved translation to the repository value", async () => {
    const updateMessage = vi.fn()
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [message]),
      updateMessage,
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    const textarea = await screen.findByLabelText(
      "navigation.home translation for zh-Hans"
    )

    expect(screen.getByText("src/navigation.tsx:12")).toBeTruthy()
    fireEvent.change(textarea, { target: { value: "未保存的首页" } })
    fireEvent.click(screen.getByRole("button", { name: "Reset" }))

    expect(textarea).toHaveProperty("value", "")
    expect(screen.queryByRole("button", { name: "Save" })).toBeNull()
    expect(updateMessage).not.toHaveBeenCalled()
  })

  it("keeps application source messages read-only", async () => {
    const updateMessage = vi.fn()
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [
        {
          ...message,
          editable: false,
          missing: false,
          packageName: "test-app",
          translation: "Home",
        },
      ]),
      getPackages: vi.fn(async () => [
        { kind: "application" as const, name: "test-app" },
      ]),
      updateMessage,
    }

    render(<MessagePanel locale="en" repository={repository} />)

    const textarea = await screen.findByLabelText(
      "navigation.home translation for en"
    )

    expect(textarea).toHaveProperty("readOnly", true)
    fireEvent.change(textarea, { target: { value: "Customized home" } })

    expect(textarea).toHaveProperty("value", "Home")
    expect(screen.queryByRole("button", { name: "Save" })).toBeNull()
    expect(updateMessage).not.toHaveBeenCalled()
  })

  it("copies an explicitly selected message value", async () => {
    const writeText = vi.fn(async () => undefined)
    Object.defineProperty(window.navigator, "clipboard", {
      configurable: true,
      value: { writeText },
    })
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [message]),
      updateMessage: vi.fn(),
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    await screen.findByText("navigation.home")
    fireEvent.click(
      screen.getByRole("button", {
        name: "Choose content to copy",
      })
    )

    expect(
      screen.getByRole("menuitem", { name: "Copy current translation" })
    ).toHaveProperty("disabled", true)
    expect(
      screen.getByRole("menuitem", {
        name: /Copy source location/,
      })
    ).toBeTruthy()

    fireEvent.click(
      screen.getByRole("menuitem", {
        name: "Copy message ID",
      })
    )

    await waitFor(() => {
      expect(writeText).toHaveBeenCalledWith("navigation.home")
    })
    expect(screen.getByRole("button", { name: "Copied" })).toBeTruthy()
    expect(screen.queryByRole("menu")).toBeNull()
    expect(screen.getByRole("status").textContent).toContain("Copied")
  })

  it("distinguishes an empty catalog from a fully translated catalog", async () => {
    const translatedMessage = {
      ...message,
      missing: false,
      translation: "首页",
    }
    const repository: MessageRepository = {
      getMessages: vi.fn(async () => [translatedMessage]),
      updateMessage: vi.fn(),
    }

    render(<MessagePanel locale="zh-Hans" repository={repository} />)

    expect(await screen.findByText("navigation.home")).toBeTruthy()
    fireEvent.click(
      screen.getByRole("checkbox", {
        name: "Missing only",
      })
    )

    expect(screen.getByText("All messages have translations.")).toBeTruthy()
  })
})
