import { describe, it, expect, beforeEach } from "vitest"
import { mount } from "@vue/test-utils"
import { nextTick } from "vue"
import { Toaster, toast, useToast } from "."

describe("Toaster", () => {
  beforeEach(() => {
    // Dismiss any lingering toasts between tests so they don't bleed.
    toast.dismiss()
    document.body.innerHTML = ""
  })

  describe("rendering", () => {
    it("renders without errors", () => {
      const wrapper = mount(Toaster)
      expect(wrapper.exists()).toBe(true)
    })

    it("renders a section element", () => {
      const wrapper = mount(Toaster)
      expect(wrapper.find("section").exists()).toBe(true)
    })

    it("renders an ordered list", () => {
      const wrapper = mount(Toaster)
      expect(wrapper.find("ol").exists()).toBe(true)
    })

    it("accepts position prop", () => {
      const wrapper = mount(Toaster, { props: { position: "top-right" } })
      expect(wrapper.exists()).toBe(true)
    })

    it("accepts theme prop", () => {
      const wrapper = mount(Toaster, { props: { theme: "dark" } })
      expect(wrapper.exists()).toBe(true)
    })

    it("accepts richColors prop", () => {
      const wrapper = mount(Toaster, { props: { richColors: true } })
      expect(wrapper.exists()).toBe(true)
    })

    it("accepts closeButton prop", () => {
      const wrapper = mount(Toaster, { props: { closeButton: true } })
      expect(wrapper.exists()).toBe(true)
    })
  })

  describe("toast functions", () => {
    it("toast() exists and is callable", () => {
      mount(Toaster, { attachTo: document.body })
      expect(typeof toast).toBe("function")
      const id = toast("Hello")
      expect(id).toBeDefined()
    })

    it("toast.success works", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast.success("Saved")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("toast.error works", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast.error("Boom")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("toast.warning works", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast.warning("Watch out")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("toast.info works", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast.info("FYI")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("toast.loading works", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast.loading("Loading...")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("toast.message works (default style)", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast.message("Message body")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("supports description option", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast("Title", { description: "Body" })
      await nextTick()
      expect(id).toBeDefined()
    })

    it("supports action option", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast("Title", {
        action: { label: "Undo", onClick: () => {} },
      })
      await nextTick()
      expect(id).toBeDefined()
    })

    it("supports custom duration", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast("Quick", { duration: 100 })
      await nextTick()
      expect(id).toBeDefined()
    })

    it("toast.promise resolves and returns id-ish", async () => {
      mount(Toaster, { attachTo: document.body })
      const promise = Promise.resolve("ok")
      const id = toast.promise(promise, {
        loading: "loading",
        success: "done",
        error: "boom",
      })
      // toast.promise can return a Promise or id-shaped value.
      expect(id).toBeDefined()
    })
  })

  describe("dismiss", () => {
    it("toast.dismiss(id) removes a specific toast", async () => {
      mount(Toaster, { attachTo: document.body })
      const id = toast("X")
      await nextTick()
      toast.dismiss(id)
      await nextTick()
      expect(true).toBe(true)
    })

    it("toast.dismiss() removes all toasts", async () => {
      mount(Toaster, { attachTo: document.body })
      toast("a")
      toast("b")
      await nextTick()
      toast.dismiss()
      await nextTick()
      expect(true).toBe(true)
    })
  })

  describe("useToast composable", () => {
    it("returns toast helpers", () => {
      const api = useToast()
      expect(typeof api.toast).toBe("function")
      expect(typeof api.success).toBe("function")
      expect(typeof api.error).toBe("function")
      expect(typeof api.warning).toBe("function")
      expect(typeof api.info).toBe("function")
      expect(typeof api.loading).toBe("function")
      expect(typeof api.message).toBe("function")
      expect(typeof api.dismiss).toBe("function")
    })

    it("merges default options when invoking success", async () => {
      mount(Toaster, { attachTo: document.body })
      const api = useToast()
      const id = api.success("Hi")
      await nextTick()
      expect(id).toBeDefined()
    })

    it("allows overriding default duration", async () => {
      mount(Toaster, { attachTo: document.body })
      const api = useToast()
      const id = api.info("ping", { duration: 50 })
      await nextTick()
      expect(id).toBeDefined()
    })

    it("dismiss helper passes through to toast.dismiss", async () => {
      mount(Toaster, { attachTo: document.body })
      const api = useToast()
      const id = api.success("X")
      await nextTick()
      api.dismiss(id)
      await nextTick()
      expect(true).toBe(true)
    })
  })
})
