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

type LoadPRs = (filter?: PullRequestsListFilter) => Promise<PullRequest[]>

const SAMPLE: PullRequest[] = [
  {
    id: "1",
    number: 101,
    title: "Add login form",
    authorName: "Alice",
    status: "open",
    branchName: "feat/login",
    targetBranch: "main",
    updatedAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
  },
  {
    id: "2",
    number: 102,
    title: "Fix dashboard crash",
    authorName: "Bob",
    status: "merged",
    branchName: "fix/dashboard",
    targetBranch: "main",
  },
  {
    id: "3",
    number: 103,
    title: "WIP: New theme",
    authorName: "Carol",
    status: "draft",
  },
]

function loader(): Mock<LoadPRs> {
  return vi.fn<LoadPRs>(
    async (_filter?: PullRequestsListFilter) => SAMPLE,
  )
}

describe("PullRequestsList (props-based composition)", () => {
  it("mounts without error", async () => {
    const onLoadPullRequests = loader()
    const wrapper = mount(PullRequestsList, {
      props: { onLoadPullRequests, label: "PRs" },
    })
    await flushPromises()
    expect(wrapper.exists()).toBe(true)
  })

  it("calls onLoadPullRequests on mount", async () => {
    const onLoadPullRequests = loader()
    mount(PullRequestsList, { props: { onLoadPullRequests } })
    await flushPromises()
    expect(onLoadPullRequests).toHaveBeenCalledTimes(1)
    expect(onLoadPullRequests).toHaveBeenCalledWith({})
  })

  it("applies defaultFilter on mount", async () => {
    const onLoadPullRequests = loader()
    mount(PullRequestsList, {
      props: {
        onLoadPullRequests,
        defaultFilter: { status: "open", query: "log" },
      },
    })
    await flushPromises()
    expect(onLoadPullRequests).toHaveBeenCalledWith({
      status: "open",
      query: "log",
    })
  })

  it("renders rows after the loader resolves", async () => {
    const onLoadPullRequests = loader()
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await flushPromises()
    expect(wrapper.text()).toContain("Add login form")
    expect(wrapper.text()).toContain("Fix dashboard crash")
    expect(wrapper.text()).toContain("Alice")
  })

  it("renders a loading skeleton while pending", async () => {
    let resolve: (v: PullRequest[]) => void = () => {}
    const onLoadPullRequests = vi.fn(
      () =>
        new Promise<PullRequest[]>((r) => {
          resolve = r
        }),
    )
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await wrapper.vm.$nextTick()
    expect(
      wrapper.find("[data-testid='pull-requests-list-loading']").exists(),
    ).toBe(true)
    resolve(SAMPLE)
    await flushPromises()
    expect(
      wrapper.find("[data-testid='pull-requests-list-loading']").exists(),
    ).toBe(false)
  })

  it("renders an error message when the loader rejects", async () => {
    const onLoadPullRequests = vi
      .fn<
        (f?: PullRequestsListFilter) => Promise<PullRequest[]>
      >()
      .mockRejectedValue(new Error("API down"))
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await flushPromises()
    const err = wrapper.find("[data-testid='pull-requests-list-error']")
    expect(err.exists()).toBe(true)
    expect(err.text()).toContain("API down")
  })

  it("renders an empty state when there are no PRs", async () => {
    const onLoadPullRequests = vi
      .fn<
        (f?: PullRequestsListFilter) => Promise<PullRequest[]>
      >()
      .mockResolvedValue([])
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await flushPromises()
    expect(
      wrapper.find("[data-testid='pull-requests-list-empty']").exists(),
    ).toBe(true)
  })

  it("filter chip change re-calls onLoadPullRequests with the new filter", async () => {
    const onLoadPullRequests = loader()
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await flushPromises()
    expect(onLoadPullRequests).toHaveBeenCalledTimes(1)
    const chip = wrapper.find("[data-testid='pull-requests-list-chip-open']")
    await chip.trigger("click")
    await flushPromises()
    expect(onLoadPullRequests).toHaveBeenCalledTimes(2)
    expect(onLoadPullRequests).toHaveBeenLastCalledWith({ status: "open" })
  })

  it("typing in the search input debounces a re-load with the query", async () => {
    vi.useFakeTimers()
    const onLoadPullRequests = loader()
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await Promise.resolve()
    await Promise.resolve()
    expect(onLoadPullRequests).toHaveBeenCalledTimes(1)
    const input = wrapper.find(
      "[data-testid='pull-requests-list-search']",
    )
    ;(input.element as HTMLInputElement).value = "login"
    await input.trigger("input")
    expect(onLoadPullRequests).toHaveBeenCalledTimes(1)
    vi.advanceTimersByTime(300)
    await Promise.resolve()
    await Promise.resolve()
    expect(onLoadPullRequests).toHaveBeenCalledTimes(2)
    expect(onLoadPullRequests).toHaveBeenLastCalledWith({ query: "login" })
    vi.useRealTimers()
  })

  it("emits update:selectedId and onSelect when a row is clicked", async () => {
    const onLoadPullRequests = loader()
    const onSelect = vi.fn()
    const wrapper = mount(PullRequestsList, {
      props: { onLoadPullRequests, onSelect },
    })
    await flushPromises()
    const row = wrapper.find("[data-testid='pull-requests-list-row-1']")
    expect(row.exists()).toBe(true)
    await row.trigger("click")
    expect(wrapper.emitted("update:selectedId")?.[0]).toEqual(["1"])
    expect(onSelect).toHaveBeenCalledTimes(1)
    expect(onSelect.mock.calls[0]?.[0]?.id).toBe("1")
  })

  it("marks the selected row with aria-selected", async () => {
    const onLoadPullRequests = loader()
    const wrapper = mount(PullRequestsList, {
      props: { onLoadPullRequests, selectedId: "2" },
    })
    await flushPromises()
    const row = wrapper.find("[data-testid='pull-requests-list-row-2']")
    expect(row.attributes("aria-selected")).toBe("true")
  })

  it("renders the default label", async () => {
    const onLoadPullRequests = loader()
    const wrapper = mount(PullRequestsList, { props: { onLoadPullRequests } })
    await flushPromises()
    expect(wrapper.text()).toContain("Pull Requests")
  })
})
