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

type StreamListener = (type: string, listener: (e: { data: unknown }) => void) => void

interface FakeStream {
  addEventListener: Mock<StreamListener>
  removeEventListener: Mock<StreamListener>
  emit: (data: unknown) => void
}

function fakeStream(): FakeStream {
  let handler: ((e: { data: unknown }) => void) | null = null
  return {
    addEventListener: vi.fn((_t: string, h: (e: { data: unknown }) => void) => {
      handler = h
    }),
    removeEventListener: vi.fn(
      (_t: string, _h: (e: { data: unknown }) => void) => {
        handler = null
      },
    ),
    emit(data: unknown) {
      if (handler) {
        handler({ data })
      }
    },
  }
}

describe("BuildStatus (props-based composition)", () => {
  it("mounts without error using only an initial event", () => {
    const wrapper = mount(BuildStatus, {
      props: {
        initial: {
          status: "running",
          commitMessage: "WIP",
        } satisfies BuildStatusEvent,
      },
    })
    expect(wrapper.exists()).toBe(true)
    expect(wrapper.text()).toContain("running")
    expect(wrapper.text()).toContain("WIP")
  })

  it("renders the default label", () => {
    const wrapper = mount(BuildStatus)
    expect(wrapper.text()).toContain("Build Status")
  })

  it("renders the custom label when provided", () => {
    const wrapper = mount(BuildStatus, { props: { label: "Pipeline" } })
    expect(wrapper.text()).toContain("Pipeline")
  })

  it("shows a spinner when the status is running", () => {
    const wrapper = mount(BuildStatus, {
      props: { initial: { status: "running" } },
    })
    expect(wrapper.find("[data-testid='build-status-spinner']").exists()).toBe(
      true,
    )
  })

  it("shows a non-animated icon when not running", () => {
    const wrapper = mount(BuildStatus, {
      props: { initial: { status: "success" } },
    })
    expect(wrapper.find("[data-testid='build-status-spinner']").exists()).toBe(
      false,
    )
    expect(wrapper.find("[data-testid='build-status-icon']").exists()).toBe(
      true,
    )
  })

  it("attaches a message listener to eventStream on mount", () => {
    const stream = fakeStream()
    mount(BuildStatus, {
      props: { eventStream: stream },
    })
    expect(stream.addEventListener).toHaveBeenCalledWith(
      "message",
      expect.any(Function),
    )
  })

  it("attaches a message listener to socket on mount", () => {
    const sock = fakeStream()
    mount(BuildStatus, {
      props: { socket: sock },
    })
    expect(sock.addEventListener).toHaveBeenCalledWith(
      "message",
      expect.any(Function),
    )
  })

  it("removes the listener on unmount and calls onUnsubscribe", () => {
    const stream = fakeStream()
    const onUnsubscribe = vi.fn()
    const wrapper = mount(BuildStatus, {
      props: { eventStream: stream, onUnsubscribe },
    })
    wrapper.unmount()
    expect(stream.removeEventListener).toHaveBeenCalledWith(
      "message",
      expect.any(Function),
    )
    expect(onUnsubscribe).toHaveBeenCalledTimes(1)
  })

  it("pushing a message through eventStream updates the rendered status", async () => {
    const stream = fakeStream()
    const wrapper = mount(BuildStatus, {
      props: { eventStream: stream, initial: { status: "pending" } },
    })
    expect(wrapper.text()).toContain("pending")
    stream.emit(
      JSON.stringify({
        status: "running",
        commitMessage: "Implement feature X",
        commitSha: "abcdef1234567",
        triggeredBy: "alice",
      }),
    )
    await flushPromises()
    expect(wrapper.text()).toContain("running")
    expect(wrapper.text()).toContain("Implement feature X")
    expect(wrapper.text()).toContain("abcdef1")
    expect(wrapper.text()).toContain("alice")
  })

  it("accepts already-parsed object messages from a socket", async () => {
    const sock = fakeStream()
    const wrapper = mount(BuildStatus, {
      props: { socket: sock, initial: { status: "pending" } },
    })
    sock.emit({ status: "success", durationMs: 65_000 })
    await flushPromises()
    expect(wrapper.text()).toContain("success")
    expect(wrapper.text()).toContain("1m 5s")
  })

  it("renders logs when showLogs is true and logs are present", () => {
    const wrapper = mount(BuildStatus, {
      props: {
        initial: { status: "success", logs: ["line one", "line two"] },
      },
    })
    expect(wrapper.find("[data-testid='build-status-logs']").exists()).toBe(
      true,
    )
    expect(wrapper.text()).toContain("line one")
    expect(wrapper.text()).toContain("line two")
  })

  it("collapses the logs panel when the toggle is clicked", async () => {
    const wrapper = mount(BuildStatus, {
      props: {
        initial: { status: "success", logs: ["a", "b"] },
      },
    })
    const toggle = wrapper.find("[data-testid='build-status-logs-toggle']")
    expect(toggle.exists()).toBe(true)
    expect(wrapper.find("[data-testid='build-status-logs']").exists()).toBe(
      true,
    )
    await toggle.trigger("click")
    expect(wrapper.find("[data-testid='build-status-logs']").exists()).toBe(
      false,
    )
  })

  it("hides the logs panel when showLogs is false", () => {
    const wrapper = mount(BuildStatus, {
      props: {
        showLogs: false,
        initial: { status: "success", logs: ["x"] },
      },
    })
    expect(wrapper.find("[data-testid='build-status-logs']").exists()).toBe(
      false,
    )
  })

  it("shows a parse error when the message JSON is invalid", async () => {
    const stream = fakeStream()
    const wrapper = mount(BuildStatus, {
      props: { eventStream: stream },
    })
    stream.emit("{not json")
    await flushPromises()
    expect(
      wrapper.find("[data-testid='build-status-parse-error']").exists(),
    ).toBe(true)
  })
})
