import { describe, expect, it } from "vitest"
import { flushPromises, mount } from "@vue/test-utils"
import ChatMessage from "./ChatMessage.vue"

describe("ChatMessage (props-based composition)", () => {
  it("mounts with no props and renders an empty bubble cleanly", () => {
    const wrapper = mount(ChatMessage)
    expect(wrapper.exists()).toBe(true)
    expect(wrapper.find('[data-testid="chat-message"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="chat-message-bubble"]').exists()).toBe(
      true,
    )
    // No body when no message and not pending.
    expect(wrapper.find('[data-testid="chat-message-body"]').exists()).toBe(
      false,
    )
    expect(wrapper.find('[data-testid="chat-message-pending"]').exists()).toBe(
      false,
    )
  })

  it("renders the message text", () => {
    const wrapper = mount(ChatMessage, {
      props: { message: "Hello world" },
    })
    expect(wrapper.find('[data-testid="chat-message-body"]').text()).toBe(
      "Hello world",
    )
  })

  it("renders the authorName", () => {
    const wrapper = mount(ChatMessage, {
      props: { authorName: "Ada Lovelace", message: "Hi" },
    })
    expect(wrapper.find('[data-testid="chat-message-author"]').text()).toBe(
      "Ada Lovelace",
    )
  })

  it("derives the avatar fallback initial when no fallback is provided", () => {
    const wrapper = mount(ChatMessage, {
      props: { authorName: "ada lovelace" },
    })
    // No avatar URL provided → fallback renders.
    expect(wrapper.text()).toContain("A")
  })

  it("renders a role badge for non-user roles and hides it for the user role", () => {
    const userWrapper = mount(ChatMessage, {
      props: { role: "user", message: "Hi" },
    })
    expect(
      userWrapper.find('[data-testid="chat-message-role-badge"]').exists(),
    ).toBe(false)

    const assistantWrapper = mount(ChatMessage, {
      props: { role: "assistant", message: "Hi" },
    })
    expect(
      assistantWrapper.find('[data-testid="chat-message-role-badge"]').exists(),
    ).toBe(true)
    expect(
      assistantWrapper.find('[data-testid="chat-message-role-badge"]').text(),
    ).toBe("assistant")
  })

  it("formats a timestamp (Date and ISO string) as local time", () => {
    const dateWrapper = mount(ChatMessage, {
      props: { timestamp: new Date("2024-06-01T15:30:00Z") },
    })
    const dateText = dateWrapper
      .find('[data-testid="chat-message-timestamp"]')
      .text()
    expect(dateText.length).toBeGreaterThan(0)

    const isoWrapper = mount(ChatMessage, {
      props: { timestamp: "2024-06-01T15:30:00Z" },
    })
    const isoText = isoWrapper
      .find('[data-testid="chat-message-timestamp"]')
      .text()
    expect(isoText).toBe(dateText)
  })

  it("shows the typing indicator when pending and hides the message body", () => {
    const wrapper = mount(ChatMessage, {
      props: { pending: true, message: "Should be hidden" },
    })
    expect(wrapper.find('[data-testid="chat-message-pending"]').exists()).toBe(
      true,
    )
    expect(wrapper.find('[data-testid="chat-message-body"]').exists()).toBe(
      false,
    )
    // Three dots.
    expect(
      wrapper
        .find('[data-testid="chat-message-pending"]')
        .findAll("span").length,
    ).toBe(3)
  })

  it("renders an error line when error is provided", () => {
    const wrapper = mount(ChatMessage, {
      props: { message: "Hi", error: "Failed to deliver" },
    })
    const err = wrapper.find('[data-testid="chat-message-error"]')
    expect(err.exists()).toBe(true)
    expect(err.text()).toBe("Failed to deliver")
    expect(err.attributes("class")).toContain("text-destructive")
  })

  it("renders action buttons and emits 'action' with the right id when clicked", async () => {
    const wrapper = mount(ChatMessage, {
      props: {
        message: "Hi",
        actions: [
          { id: "copy", label: "Copy" },
          { id: "regenerate", label: "Regenerate" },
        ],
      },
    })
    const buttons = wrapper.findAll('[data-action-id]')
    expect(buttons.length).toBe(2)
    expect(buttons[0].text()).toBe("Copy")
    expect(buttons[1].text()).toBe("Regenerate")

    await buttons[1].trigger("click")
    const emitted = wrapper.emitted("action")
    expect(emitted).toBeTruthy()
    expect(emitted?.[0]).toEqual([{ id: "regenerate" }])
  })

  it("`align` overrides role-based alignment", () => {
    // Role is "assistant" → default start, but align="end" forces end.
    const wrapper = mount(ChatMessage, {
      props: { role: "assistant", align: "end", message: "Hi" },
    })
    expect(
      wrapper.find('[data-testid="chat-message"]').attributes("data-align"),
    ).toBe("end")
  })

  it("`variant` overrides role-based variant", () => {
    // Role "user" defaults to "accent", but variant="destructive" wins.
    const wrapper = mount(ChatMessage, {
      props: { role: "user", variant: "destructive", message: "Hi" },
    })
    expect(
      wrapper.find('[data-testid="chat-message"]').attributes("data-variant"),
    ).toBe("destructive")
    expect(
      wrapper
        .find('[data-testid="chat-message-bubble"]')
        .attributes("class"),
    ).toContain("bg-destructive")
  })

  it("renders HTML via v-html when allowHtml is true", () => {
    const wrapper = mount(ChatMessage, {
      props: {
        message: "<strong data-testid='inner-strong'>Bold</strong>",
        allowHtml: true,
      },
    })
    const html = wrapper.html()
    expect(html).toContain("<strong")
    expect(html).toContain("Bold")
    // Sanity: when allowHtml is false the tag is escaped.
    const escaped = mount(ChatMessage, {
      props: { message: "<strong>Bold</strong>", allowHtml: false },
    })
    expect(escaped.html()).not.toContain("<strong>Bold</strong>")
    expect(escaped.text()).toContain("<strong>Bold</strong>")
  })

  it("loads lucide icons for action buttons when an icon is supplied", async () => {
    const wrapper = mount(ChatMessage, {
      props: {
        message: "Hi",
        actions: [{ id: "copy", label: "Copy", icon: "copy" }],
      },
    })
    for (let i = 0; i < 5; i += 1) {
      await flushPromises()
    }
    // The lucide SVG is racy in jsdom; tolerate either an <svg> or just
    // the button being present with the right data-action-id.
    const btn = wrapper.find('[data-action-id="copy"]')
    expect(btn.exists()).toBe(true)
    expect(btn.text()).toContain("Copy")
  })

  it("user role defaults to right alignment and accent variant", () => {
    const wrapper = mount(ChatMessage, {
      props: { role: "user", message: "Hi" },
    })
    const root = wrapper.find('[data-testid="chat-message"]')
    expect(root.attributes("data-align")).toBe("end")
    expect(root.attributes("data-variant")).toBe("accent")
  })

  it("applies classList (string and array) to the root", () => {
    const stringWrapper = mount(ChatMessage, {
      props: { message: "Hi", classList: "extra-1" },
    })
    expect(
      stringWrapper.find('[data-testid="chat-message"]').attributes("class"),
    ).toContain("extra-1")

    const arrayWrapper = mount(ChatMessage, {
      props: { message: "Hi", classList: ["extra-1", "extra-2"] },
    })
    const cls = arrayWrapper
      .find('[data-testid="chat-message"]')
      .attributes("class")
    expect(cls).toContain("extra-1")
    expect(cls).toContain("extra-2")
  })
})
