import React from "react";
import { Select } from "./index";
import { SelectOption } from "./types";
import * as utils from "../../utils";

import { fireEvent, render, screen } from "@testing-library/react";

// Mock react-select to simplify testing
jest.mock("react-select", () => {
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const { forwardRef } = require("react");
  // eslint-disable-next-line react/display-name
  const MockSelect = forwardRef((props: any, ref: any) => {
    const {
      options,
      value,
      placeholder,
      onChange,
      className,
      classNames,
      ...rest
    } = props;

    // Call classNames functions to exercise them for coverage
    if (classNames) {
      classNames.control?.({ isFocused: false });
      classNames.control?.({ isFocused: true });
      classNames.indicatorSeparator?.();
      classNames.dropdownIndicator?.({ selectProps: { value: null } });
      classNames.dropdownIndicator?.({ selectProps: { value: "test" } });
      classNames.singleValue?.();
      classNames.menu?.();
      classNames.option?.({ isFocused: false, isSelected: false, label: "A" });
      classNames.option?.({ isFocused: true, isSelected: false, label: "A" });
      classNames.option?.({ isFocused: false, isSelected: true, label: "A" });
      classNames.placeholder?.();
      classNames.input?.();
      classNames.valueContainer?.();
    }

    return (
      <select
        ref={ref}
        data-testid={rest["data-testid"] || "mock-select"}
        data-cy={rest["data-cy"]}
        className={className}
        value={value?.value || ""}
        onChange={e => {
          const selected = options?.find(
            (o: SelectOption) => o.value === e.target.value
          );
          onChange?.(selected, { action: "select-option", option: selected });
        }}
      >
        {placeholder && (
          <option value="" disabled={true}>
            {placeholder}
          </option>
        )}
        {options?.map((opt: SelectOption) => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
    );
  });

  return {
    __esModule: true,
    default: MockSelect,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    createFilter: (filterConfig: any) => () => true,
  };
});

const defaultOptions: SelectOption[] = [
  { label: "Option A", value: "a" },
  { label: "Option B", value: "b" },
  { label: "Option C", value: "c" },
];

describe("Select", () => {
  it("renders with options", () => {
    render(<Select options={defaultOptions} data-testid="my-select" />);
    expect(screen.getByTestId("my-select")).toBeInTheDocument();
  });

  it("renders placeholder", () => {
    render(<Select options={defaultOptions} placeholder="Pick one" />);
    expect(screen.getByText("Pick one")).toBeInTheDocument();
  });

  it("appends * to placeholder when required", () => {
    render(
      <Select options={defaultOptions} placeholder="Pick" required={true} />
    );
    expect(screen.getByText("Pick*")).toBeInTheDocument();
  });

  it("calls onChange when an option is selected", () => {
    const onChange = jest.fn();
    render(
      <Select options={defaultOptions} onChange={onChange} data-testid="sel" />
    );
    fireEvent.change(screen.getByTestId("sel"), { target: { value: "b" } });
    expect(onChange).toHaveBeenCalledWith(
      defaultOptions[1],
      expect.objectContaining({ action: "select-option" })
    );
  });

  it("renders label when provided", () => {
    render(<Select options={defaultOptions} label="My Label" />);
    expect(screen.getByText("My Label")).toBeInTheDocument();
  });

  it("renders label with custom className", () => {
    render(
      <Select
        options={defaultOptions}
        label="Label"
        labelClassName="custom-class"
      />
    );
    const label = screen.getByText("Label");
    expect(label).toHaveClass("custom-class");
  });

  it("does not render label when not provided", () => {
    const { container } = render(<Select options={defaultOptions} />);
    expect(container.querySelector("label")).not.toBeInTheDocument();
  });

  it("renders error message when error prop is provided", () => {
    render(<Select options={defaultOptions} error="Required field" />);
    expect(screen.getByText("Required field")).toBeInTheDocument();
  });

  it("renders helperText when no error", () => {
    render(<Select options={defaultOptions} helperText="Choose wisely" />);
    expect(screen.getByText("Choose wisely")).toBeInTheDocument();
  });

  it("does not render helperText when error is present", () => {
    render(
      <Select
        options={defaultOptions}
        error="Error"
        helperText="Choose wisely"
      />
    );
    expect(screen.getByText("Error")).toBeInTheDocument();
    expect(screen.queryByText("Choose wisely")).not.toBeInTheDocument();
  });

  it("renders unstyled variant without label/error wrapper", () => {
    const { container } = render(
      <Select
        options={defaultOptions}
        variant="unstyled"
        label="Should not appear"
        error="Should not appear"
      />
    );
    expect(screen.queryByText("Should not appear")).not.toBeInTheDocument();
    // unstyled variant doesn't wrap in a div.w-full
    expect(container.querySelector(".w-full")).not.toBeInTheDocument();
  });

  it("applies size sm classes via classNames", () => {
    const cxSpy = jest.spyOn(utils, "cx");
    render(
      <Select options={defaultOptions} size="sm" data-testid="sm-select" />
    );
    expect(screen.getByTestId("sm-select")).toBeInTheDocument();
    expect(
      cxSpy.mock.calls.some(call => call[0] === "h-12 px-3 rounded-lg")
    ).toBe(true);
    cxSpy.mockRestore();
  });

  it("applies default (md) size classes", () => {
    const cxSpy = jest.spyOn(utils, "cx");
    render(
      <Select options={defaultOptions} size="md" data-testid="md-select" />
    );
    expect(screen.getByTestId("md-select")).toBeInTheDocument();
    expect(
      cxSpy.mock.calls.some(call => call[0] === "h-14 px-3 rounded-xl")
    ).toBe(true);
    cxSpy.mockRestore();
  });

  it("applies size lg classes via classNames", () => {
    const cxSpy = jest.spyOn(utils, "cx");
    render(
      <Select options={defaultOptions} size="lg" data-testid="lg-select" />
    );
    expect(screen.getByTestId("lg-select")).toBeInTheDocument();
    expect(
      cxSpy.mock.calls.some(call => call[0] === "h-16 px-3 rounded-2xl")
    ).toBe(true);
    cxSpy.mockRestore();
  });

  it("falls back to md size classes when size is not provided", () => {
    const cxSpy = jest.spyOn(utils, "cx");
    render(
      <Select options={defaultOptions} data-testid="default-select" />
    );
    expect(screen.getByTestId("default-select")).toBeInTheDocument();
    expect(
      cxSpy.mock.calls.some(call => call[0] === "h-14 px-3 rounded-xl")
    ).toBe(true);
    cxSpy.mockRestore();
  });

  it("handles singleValueClassName prop", () => {
    render(
      <Select
        options={defaultOptions}
        singleValueClassName="custom-single-value"
        data-testid="sv-select"
      />
    );
    expect(screen.getByTestId("sv-select")).toBeInTheDocument();
  });

  it("passes singleValueClassName into the singleValue cx computation", () => {
    const cxSpy = jest.spyOn(utils, "cx");

    render(
      <Select
        options={defaultOptions}
        singleValueClassName="custom-single-value"
        data-testid="sv-select-cx"
      />
    );

    // The mock invokes classNames.singleValue(), which builds its class list
    // via cx(...). Assert the call originates from the singleValue computation
    // (first arg is the base "text-text-secondary" class in the default,
    // non-custom style) and that the prop is passed through as the second arg.
    expect(cxSpy).toHaveBeenCalledWith(
      "text-text-secondary",
      "custom-single-value"
    );

    cxSpy.mockRestore();
  });

  it("handles isCustomStyle prop", () => {
    render(
      <Select
        options={defaultOptions}
        isCustomStyle={true}
        data-testid="custom-select"
      />
    );
    expect(screen.getByTestId("custom-select")).toBeInTheDocument();
  });

  it("handles hasError prop", () => {
    render(
      <Select
        options={defaultOptions}
        hasError={true}
        data-testid="err-select"
      />
    );
    expect(screen.getByTestId("err-select")).toBeInTheDocument();
  });

  it("handles filterOptions prop", () => {
    render(
      <Select
        options={defaultOptions}
        filterOptions={{ matchFrom: "start", ignoreCase: true }}
        data-testid="filter-select"
      />
    );
    expect(screen.getByTestId("filter-select")).toBeInTheDocument();
  });

  it("passes data-cy prop", () => {
    render(<Select options={defaultOptions} data-cy="cypress-sel" />);
    expect(
      document.querySelector('[data-cy="cypress-sel"]')
    ).toBeInTheDocument();
  });

  it("renders with a selected value", () => {
    render(
      <Select
        options={defaultOptions}
        value={defaultOptions[2]}
        data-testid="val-select"
      />
    );
    const select = screen.getByTestId("val-select") as HTMLSelectElement;
    expect(select.value).toBe("c");
  });

  it("has the correct displayName", () => {
    expect(Select.displayName).toBe("Select");
  });

  it("exercises option classNames with last option label", () => {
    // Ensures the last-option border logic is covered
    const opts = [
      { label: "First", value: "1" },
      { label: "Last", value: "2" },
    ];
    render(
      <Select
        options={opts}
        isCustomStyle={true}
        data-testid="last-opt-select"
      />
    );
    expect(screen.getByTestId("last-opt-select")).toBeInTheDocument();
  });
});
