import React from "react"
import { render, screen, act, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import AutoComplete from "../autocomplete"
import SelectMenu from ".."

describe("SelectMenu Wrapper", () => {
  it("renders a SelectMenu with a default placeholder 'Select'", () => {
    render(
      <SelectMenu
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")

    expect(trigger).toBeInTheDocument()
    expect(trigger.tagName).toEqual("DIV")
    expect(trigger.getAttribute("role")).toEqual("combobox")
  })

  it("uses type 'autocomplete' when async prop is set", () => {
    render(
      <SelectMenu
        async
        options={async () => {
          return [
            { label: "Option 1", value: "option1" },
            { label: "Option 2", value: "option2" },
            { label: "Option 3", value: "option3" },
          ]
        }}
      />
    )

    const trigger = screen.getByRole("combobox")

    expect(trigger).toBeInTheDocument()
    expect(trigger.tagName).toEqual("INPUT")
    expect(trigger.getAttribute("role")).toEqual("combobox")
  })

  it("uses type 'autocomplete' when create prop is set", () => {
    render(
      <SelectMenu
        create
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")

    expect(trigger).toBeInTheDocument()
    expect(trigger.tagName).toEqual("INPUT")
  })

  it("applies aria-label to the reference when passed", () => {
    render(
      <SelectMenu
        aria-label="Select an option"
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" }
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)

    const listbox = screen.getByRole("listbox")

    expect(trigger).toHaveAttribute("aria-label", "Select an option")
    expect(listbox).not.toHaveAttribute("aria-label", "Select an option")
  })

  it("applies aria-labelledby to the reference when passed", () => {
    render(
      <SelectMenu
        aria-labelledby="my-external-id"
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" }
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)

    const listbox = screen.getByRole("listbox")

    expect(trigger).toHaveAttribute("aria-labelledby", "my-external-id")
    expect(listbox).not.toHaveAttribute("aria-labelledby", "my-external-id")
  })
})

describe("Autocomplete", () => {
  it("renders a trigger with a default placeholder 'Select'", () => {
    render(
      <AutoComplete
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")

    expect(trigger).toBeInTheDocument()
    expect(trigger.placeholder).toEqual("Select")
  })

  it("allows to set a custom placeholder", () => {
    render(
      <AutoComplete
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" },
        ]}
        placeholder="Custom placeholder"
      />
    )

    const trigger = screen.getByRole("combobox")

    expect(trigger).toBeInTheDocument()
    expect(trigger.placeholder).toEqual("Custom placeholder")
  })

  it("renders a list of options", () => {
    render(
      <AutoComplete
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)
    const options = screen.getAllByRole("option")

    expect(trigger).toBeInTheDocument()
    expect(options).toHaveLength(3)
  })

  it("throws an error when 'async' is true and options is not a function", () => {
    jest.spyOn(console, "error")
    console.error.mockImplementation(() => {})

    expect(() => {
      render(
        <AutoComplete
          async
          type="autocomplete"
          options={[
            { label: "Option 1", value: "option1" },
            { label: "Option 2", value: "option2" },
            { label: "Option 3", value: "option3" },
          ]}
        />
      )
    }).toThrow("Async autocomplete requires options to be a function")

    console.error.mockRestore()
  })

  it("selects the option that matches all the input characters", () => {
    render(
      <AutoComplete
        options={[
          { label: "Option 1", value: "option1" },
          { label: "Option 2", value: "option2" },
          { label: "Option 3", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.type(trigger, "Option 1")

    const option = screen.getByRole("option", { name: "Option 2" })
    const selectedOption = screen.getByRole("option", { name: "Option 1" })

    expect(option.getAttribute("aria-selected")).toEqual("false")
    expect(selectedOption.getAttribute("aria-selected")).toEqual("true")
  })

  it("filters the options based on the input value", () => {
    render(
      <AutoComplete
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.type(trigger, "subscriber")

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(2)
    expect(options[0].textContent).toEqual("All subscribers")
    expect(options[1].textContent).toEqual("Cold subscribers")
  })

  it("shows all options when the input value is empty", () => {
    render(
      <AutoComplete
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.type(trigger, "subscriber")
    userEvent.clear(trigger)

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(3)
  })

  it("shows all options when the input value matches all characters of an option", () => {
    render(
      <AutoComplete
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.type(trigger, "All subscribers")

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(3)
    expect(options[0].getAttribute("aria-selected")).toEqual("true")
  })

  it("shows 'No result found' when there are no results", () => {
    render(
      <AutoComplete
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.type(trigger, "subscriberz")

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(1)
    expect(options[0].textContent).toEqual("No result found")
  })

  it("selects match when 'Enter' key is pressed", () => {
    render(
      <AutoComplete
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")

    userEvent.click(trigger)
    let options = screen.getAllByRole("option")

    expect(options).toHaveLength(3)

    userEvent.type(trigger, "Cold")
    options = screen.getAllByRole("option")

    expect(options).toHaveLength(1)

    userEvent.keyboard("{Enter}")
    options = screen.queryAllByRole("option")

    expect(options).toHaveLength(0)

    expect(trigger).toHaveValue("Cold subscribers")
  })

  it("does not load async options when the input value is less than 2 characters", async () => {
    const options = jest.fn(() => {
      return new Promise((resolve) => {
        resolve([
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ])
      })
    })
    render(<AutoComplete async options={options} />)

    const trigger = screen.getByRole("combobox")

    await act(async () => {
      await userEvent.type(trigger, "C", { delay: 2 })
    })

    await waitFor(() => {
      expect(options).toHaveBeenCalledTimes(0)
    })

    act(() => {
      userEvent.clear(trigger)
    })

    await act(async () => {
      await userEvent.type(trigger, "Co", { delay: 2 })
    })

    await waitFor(() => {
      expect(options).toHaveBeenCalledTimes(1)
    })
  })

  it("calls onChange callback when an option is selected", () => {
    const onChange = jest.fn()
    render(
      <AutoComplete
        name="subscribers"
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
        onChange={onChange}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)
    const options = screen.getAllByRole("option")

    userEvent.click(options[1])

    expect(onChange).toHaveBeenCalledTimes(1)
    expect(onChange).toHaveBeenCalledWith({
      target: {
        label: "Subscribed for a year",
        value: "option2",
        name: "subscribers",
      },
    })
  })

  it("calls onChange callback when an option is selected with keyboard", () => {
    const onChange = jest.fn()
    render(
      <AutoComplete
        name="subscribers"
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
        onChange={onChange}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)

    userEvent.keyboard("{ArrowDown}{ArrowDown}{Enter}")

    expect(onChange).toHaveBeenCalledTimes(1)
    expect(onChange).toHaveBeenCalledWith({
      target: {
        label: "Subscribed for a year",
        value: "option2",
        name: "subscribers",
      },
    })
  })

  it("accepts a prefilled value", () => {
    render(
      <AutoComplete
        name="testAutoComplete"
        options={[]}
        onChange={() => {}}
        value={{ value: "internalValue", label: "User-friendly label" }}
      />
    )

    const element = screen.getByRole("combobox")

    expect(element.value).toEqual("User-friendly label")
  })

  it("renders an option's component over label when provided", () => {
    render(
      <AutoComplete
        options={[
          { label: "Blowfish", value: "blowfish", component: <div>🐡 Blowfish</div> },
          { label: "Fried Shrimp", value: "fried-shrimp", component: <div>🍤 Fried Shrimp</div> },
          { label: "Octopus", value: "octopus", component: <div>🐙 Octopus</div> },
          { label: "Squid", value: "squid", component: <div>🦑 Squid</div> },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(4)
    expect(options[0].textContent).toEqual("🐡 Blowfish")
  })

  it("uses the option label over component in the input value", () => {
    render(
      <AutoComplete
        options={[
          { label: "Blowfish", value: "blowfish", component: <div>🐡 Blowfish</div> },
          { label: "Fried Shrimp", value: "fried-shrimp", component: <div>🍤 Fried Shrimp</div> },
          { label: "Octopus", value: "octopus", component: <div>🐙 Octopus</div> },
          { label: "Squid", value: "squid", component: <div>🦑 Squid</div> },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(4)

    userEvent.click(options[0])

    expect(trigger).toHaveValue("Blowfish")
  })

  it("uses text content of an option's JSX label in the input value", () => {
    render(
      <AutoComplete
        options={[
          { value: "blowfish", label: <div>🐡 Blowfish</div> },
          { value: "fried-shrimp", label: <div>🍤 Fried Shrimp</div> },
          { value: "octopus", label: <div>🐙 Octopus</div> },
          { value: "squid", label: <div>🦑 Squid</div> },
        ]}
      />
    )

    const trigger = screen.getByRole("combobox")
    userEvent.click(trigger)

    const options = screen.getAllByRole("option")

    expect(options).toHaveLength(4)

    userEvent.click(options[0])

    expect(trigger).toHaveValue("🐡 Blowfish")
  })

  it("does not allow interaction when disabled", () => {
    const onChange = jest.fn()
    render(
      <AutoComplete
        disabled
        options={[
          { label: "All subscribers", value: "option1" },
          { label: "Subscribed for a year", value: "option2" },
          { label: "Cold subscribers", value: "option3" },
        ]}
        onChange={onChange}
      />
    )

    const trigger = screen.getByRole("combobox")

    expect(trigger).toBeDisabled()
    userEvent.click(trigger)

    const options = screen.queryAllByRole("option")

    expect(options).toHaveLength(0)

    userEvent.type(trigger, "subscriber")

    expect(trigger).toHaveValue("")
    expect(onChange).not.toHaveBeenCalled()
  })

  describe("create option", () => {
    it("adds a new option when the input value does not match any option", () => {
      const onChange = jest.fn()
      render(
        <AutoComplete
          create
          options={[
            { label: "All subscribers", value: "option1" },
            { label: "Subscribed for a year", value: "option2" },
            { label: "Cold subscribers", value: "option3" },
          ]}
          onChange={onChange}
        />
      )

      const trigger = screen.getByRole("combobox")
      userEvent.type(trigger, "New subscriber")

      let options = screen.getAllByRole("option")

      expect(options).toHaveLength(1)
      expect(options[0].textContent).toEqual("Create “New subscriber”")

      userEvent.click(options[0])

      expect(onChange).toHaveBeenCalledTimes(1)
      expect(onChange).toHaveBeenCalledWith({
        target: {
          label: "New subscriber",
          value: "New subscriber1",
          created: true,
          name: undefined,
        },
      })

      userEvent.click(trigger)
      options = screen.queryAllByRole("option")
      expect(options).toHaveLength(4)
      const lastOption = screen.getByRole("option", { name: "New subscriber" })
      expect(lastOption.getAttribute("aria-selected")).toEqual("true")
    })

    it("allows the use of Enter key to create a new option", () => {
      const onChange = jest.fn()
      render(
        <AutoComplete
          create
          options={[
            { label: "All subscribers", value: "option1" },
            { label: "Subscribed for a year", value: "option2" },
            { label: "Cold subscribers", value: "option3" },
          ]}
          onChange={onChange}
        />
      )

      const trigger = screen.getByRole("combobox")
      userEvent.type(trigger, "New subscriber")
      userEvent.keyboard("{Enter}")

      expect(onChange).toHaveBeenCalledTimes(1)
      expect(onChange).toHaveBeenCalledWith({
        target: {
          label: "New subscriber",
          value: "New subscriber1",
          created: true,
          name: undefined,
        },
      })

      userEvent.click(trigger)
      const options = screen.getAllByRole("option")
      expect(options).toHaveLength(4)
      const lastOption = screen.getByRole("option", { name: "New subscriber" })
      expect(lastOption.getAttribute("aria-selected")).toEqual("true")
    })
  })
})
