import { fireEvent, render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { NumberField } from ".";

describe("NumberField", () => {
  it("should render without errors", () => {
    render(<NumberField label="NumberField" />);
  });

  it("should render with label", () => {
    const { getByText } = render(<NumberField label="Sample label" />);

    expect(getByText("Sample label")).toBeInTheDocument();
  });

  it("should render with error message", () => {
    const { getByLabelText } = render(
      <NumberField label="Number" errorMessage="Sample error message" />,
    );

    expect(getByLabelText("Number")).toHaveAccessibleDescription(
      "Sample error message",
    );
  });

  it("should render with disabled state", () => {
    const { getByLabelText } = render(
      <NumberField label="Number" isDisabled />,
    );

    expect(getByLabelText("Number")).toBeDisabled();
  });

  it("should render with custom class name", () => {
    const { getByLabelText } = render(
      <NumberField label="Number" className="sample-class" />,
    );

    expect(getByLabelText("Number")).toHaveClass("sample-class");
  });

  describe("disabled", () => {
    it("should disable the input when disabled", () => {
      const { getByLabelText } = render(
        <NumberField label="Number" isDisabled defaultValue={10} />,
      );

      expect(getByLabelText("Number")).toBeDisabled();
    });
  });

  describe("isRequired", () => {
    it("should set required attribute", () => {
      render(
        <NumberField label="Number" isDisabled defaultValue={10} isRequired />,
      );
      expect(screen.getByLabelText("Number")).toBeRequired();
    });

    it("should have className of --is-required when true", () => {
      render(
        <NumberField label="Number" isDisabled defaultValue={10} isRequired />,
      );
      expect(screen.getByLabelText("Number")).toHaveClass("--is-required");
    });

    it("should have className of --is-optional when false", () => {
      render(
        <NumberField
          label="Number"
          isDisabled
          defaultValue={10}
          isRequired={false}
        />,
      );
      expect(screen.getByLabelText("Number")).toHaveClass("--is-optional");
    });

    it("should have neither --is-required nor --is-optional when unset", () => {
      render(<NumberField label="Number" isDisabled defaultValue={10} />);
      expect(screen.getByLabelText("Number")).not.toHaveClass("--is-required");
      expect(screen.getByLabelText("Number")).not.toHaveClass("--is-optional");
    });
  });

  describe("user input", () => {
    it("only allows numbers", () => {
      const { container } = render(
        <NumberField id="test" label="Number" defaultValue={0} />,
      );
      const input = container.querySelector("#test") as HTMLInputElement;

      fireEvent.change(input, { target: { value: "ABC" } });
      expect(input.value).toEqual(""); // zero
      fireEvent.change(input, { target: { value: "-£" } });
      expect(input.value).toEqual(""); // zero

      fireEvent.change(input, { target: { value: "123" } });
      expect(input.value).toEqual("123");
    });

    it.todo("allows arrow keys to change value in steps");

    // Arrow key tests - simulating browser behavior since RTL doesn't support it natively
    it("allows arrow keys to change value", async () => {
      render(
        <NumberField id="test" label="Number" defaultValue={1} minValue={0} />,
      );
      const input = screen.getByRole<HTMLInputElement>("spinbutton");

      await userEvent.tab();
      expect(input).toHaveFocus();

      // Simulate arrow up key by directly calling stepUp() which is what browsers do
      fireEvent.keyDown(input, { key: "ArrowUp" });
      input.stepUp();
      fireEvent.change(input, { target: { value: "2" } });

      expect(input.value).toEqual("2");
    });

    it("prevents setting a value < minValue (if set)", () => {
      const mockChange = vi.fn();
      render(
        <NumberField
          id="test"
          label="Number"
          defaultValue={10}
          minValue={10}
          onChange={mockChange}
        />,
      );
      const input = screen.getByRole<HTMLInputElement>("spinbutton");

      // Try to step down below minValue
      fireEvent.keyDown(input, { key: "ArrowDown" });
      try {
        input.stepDown();
        fireEvent.change(input, { target: { value: input.value } });
      } catch {
        // stepDown() throws when it would go below minValue
      }

      expect(input.value).toEqual("10");
    });

    it("prevents setting a value > maxValue (if set)", () => {
      const mockChange = vi.fn();
      render(
        <NumberField
          id="test"
          label="Number"
          defaultValue={10}
          maxValue={10}
          onChange={mockChange}
        />,
      );
      const input = screen.getByRole<HTMLInputElement>("spinbutton");

      // Try to step up above maxValue
      fireEvent.keyDown(input, { key: "ArrowUp" });
      try {
        input.stepUp();
        fireEvent.change(input, { target: { value: input.value } });
      } catch {
        // stepUp() throws when it would go above maxValue
      }

      expect(input.value).toEqual("10");
    });

    it("validates step increments on blur", () => {
      const mockChange = vi.fn();
      render(
        <NumberField
          id="test"
          label="Number"
          step={5}
          minValue={0}
          onChange={mockChange}
        />,
      );
      const input = screen.getByRole<HTMLInputElement>("spinbutton");

      // Type an invalid step value
      fireEvent.change(input, { target: { value: "7" } });
      expect(input.value).toEqual("7");

      // On blur, should snap to nearest valid step (5)
      fireEvent.blur(input);
      expect(input.value).toEqual("5");
      expect(mockChange).toHaveBeenCalled();
    });

    it("allows any values during typing (including invalid step increments)", () => {
      const mockChange = vi.fn();
      render(
        <NumberField
          id="test"
          label="Number"
          step={5}
          minValue={0}
          onChange={mockChange}
        />,
      );
      const input = screen.getByRole<HTMLInputElement>("spinbutton");

      // Type an invalid step value (7 is not a multiple of 5)
      fireEvent.change(input, { target: { value: "7" } });
      expect(input.value).toEqual("7");
      expect(mockChange).toHaveBeenCalled();

      mockChange.mockClear();

      // Type a valid step value
      fireEvent.change(input, { target: { value: "10" } });
      expect(input.value).toEqual("10");
      expect(mockChange).toHaveBeenCalled();
    });

    it("allows typing intermediate values to reach final valid step value", () => {
      const mockChange = vi.fn();
      render(
        <NumberField
          id="test"
          label="Number"
          step={5}
          minValue={0}
          onChange={mockChange}
        />,
      );
      const input = screen.getByRole<HTMLInputElement>("spinbutton");

      // Type "2" as intermediate value (invalid step)
      fireEvent.change(input, { target: { value: "2" } });
      expect(input.value).toEqual("2");
      expect(mockChange).toHaveBeenCalledTimes(1);

      // Continue typing to make "25" (valid step)
      fireEvent.change(input, { target: { value: "25" } });
      expect(input.value).toEqual("25");
      expect(mockChange).toHaveBeenCalledTimes(2);

      // On blur, value should remain 25 (already valid)
      fireEvent.blur(input);
      expect(input.value).toEqual("25");
    });
  });
});
