import { render, renderHook, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useTextField } from "./useTextField";
import type { UseTextFieldProps } from "./types";

const TEST_LABEL = "Test label";

describe("useTextField", () => {
  const WrapperComponent = (props: UseTextFieldProps) => {
    const { inputProps, labelProps } = useTextField(props);
    return (
      <div>
        <label {...labelProps}>{TEST_LABEL}</label>
        <input {...inputProps} />
      </div>
    );
  };

  it("should run without crashing", () => {
    const { result } = renderHook(() => useTextField({ label: TEST_LABEL }));
    expect(result.current).toHaveProperty("labelProps");
    expect(result.current).toHaveProperty("inputProps");
  });

  describe("uncontrolled value", () => {
    it("should default value to empty string", () => {
      render(<WrapperComponent label={TEST_LABEL} />);

      expect(screen.getByLabelText(TEST_LABEL)).toHaveValue("");
    });

    it("should set the value to the default value", () => {
      render(<WrapperComponent label={TEST_LABEL} defaultValue="default" />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveValue("default");
    });

    it("should not update the value if the initial value is defined", () => {
      const { rerender } = render(
        <WrapperComponent label={TEST_LABEL} defaultValue="default" />,
      );
      rerender(<WrapperComponent label={TEST_LABEL} defaultValue="updated" />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveValue("default");
    });
  });

  describe("controlled value", () => {
    it("should set the value to the value prop", () => {
      const handleChange = vi.fn();
      render(
        <WrapperComponent
          label={TEST_LABEL}
          value="value"
          onChange={handleChange}
        />,
      );
      expect(screen.getByLabelText(TEST_LABEL)).toHaveValue("value");
    });

    it("should call onChange with event", async () => {
      const handleChange = vi.fn();
      render(
        <WrapperComponent
          label={TEST_LABEL}
          value="value"
          onChange={handleChange}
        />,
      );
      const input = screen.getByLabelText(TEST_LABEL);
      input.focus();
      await userEvent.keyboard("a");
      expect(handleChange).toHaveBeenCalledWith(
        expect.objectContaining({
          target: input,
        }),
      );
    });
  });

  describe("isDisabled", () => {
    it("should disable the input when isDisabled is true", () => {
      render(<WrapperComponent label={TEST_LABEL} isDisabled />);
      expect(screen.getByLabelText(TEST_LABEL)).toBeDisabled();
    });

    it("should not call onChange when isDisabled is true", async () => {
      const handleChange = vi.fn();
      render(
        <WrapperComponent
          label={TEST_LABEL}
          isDisabled
          onChange={handleChange}
        />,
      );
      const input = screen.getByLabelText(TEST_LABEL);
      input.focus();
      await userEvent.keyboard("a");
      expect(handleChange).not.toHaveBeenCalled();
    });
  });

  describe("isReadOnly", () => {
    it("should be readonly", () => {
      render(<WrapperComponent isReadOnly label={TEST_LABEL} />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute("readonly");
    });

    it("should not call onChange", async () => {
      const handleChange = vi.fn();
      render(
        <WrapperComponent
          label={TEST_LABEL}
          isReadOnly
          onChange={handleChange}
        />,
      );
      const input = screen.getByLabelText(TEST_LABEL);
      input.focus();
      await userEvent.keyboard("a");
      expect(handleChange).not.toHaveBeenCalled();
    });
  });

  describe("isRequired", () => {
    it("should set aria-required to true when isRequired is true", () => {
      render(<WrapperComponent label={TEST_LABEL} isRequired />);
      expect(screen.getByLabelText(TEST_LABEL)).toBeRequired();
    });

    it("should not set aria-required when isRequired is missing or false", () => {
      render(<WrapperComponent label={TEST_LABEL} isRequired={undefined} />);
      expect(screen.getByLabelText(TEST_LABEL)).not.toBeRequired();
    });
  });

  describe("validation", () => {
    it("should set aria-invalid to true when isInvalid is set to true", () => {
      render(<WrapperComponent label={TEST_LABEL} isInvalid />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute(
        "aria-invalid",
        "true",
      );
    });

    it("should set aria-invalid to false when isInvalid is set to false", () => {
      render(<WrapperComponent label={TEST_LABEL} isInvalid={false} />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute(
        "aria-invalid",
        "false",
      );
    });

    it("should not set aria-invalid when isInvalid is not provided", () => {
      render(<WrapperComponent label={TEST_LABEL} />);
      expect(screen.getByLabelText(TEST_LABEL)).not.toHaveAttribute(
        "aria-invalid",
      );
    });
  });

  describe("description", () => {
    it("should set aria-describedby when description is set", () => {
      render(<WrapperComponent label={TEST_LABEL} description="description" />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute(
        "aria-describedby",
      );
    });

    it("should pass through aria-describedby", () => {
      render(
        <WrapperComponent
          label={TEST_LABEL}
          description="description"
          aria-describedby="describedby"
        />,
      );
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute(
        "aria-describedby",
        expect.stringMatching(/(^|\s)describedby($|\s)/),
      );
    });
  });

  describe("errorMessage", () => {
    it("should set aria-describedby when errorMessage is set", () => {
      render(<WrapperComponent label={TEST_LABEL} errorMessage="error" />);
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute(
        "aria-describedby",
      );
    });

    it("should pass through aria-errormessage", () => {
      render(
        <WrapperComponent
          label={TEST_LABEL}
          errorMessage="error"
          aria-errormessage="errormessage"
        />,
      );
      expect(screen.getByLabelText(TEST_LABEL)).toHaveAttribute(
        "aria-errormessage",
        expect.stringMatching(/(^|\s)errormessage($|\s)/),
      );
    });
  });

  // Test passthrough props
  it.each([
    "autoComplete",
    "maxLength",
    "minLength",
    "name",
    "placeholder",
    "inputMode",
    "onCopy",
    "onCut",
    "onPaste",
    "onCompositionEnd",
    "onCompositionStart",
    "onCompositionUpdate",
    "onSelect",
    "onBeforeInput",
    "onInput",
    "onBlur",
    "onFocus",
    "role",
  ])("should pass through %s", prop => {
    const { result } = renderHook(() =>
      useTextField({ label: TEST_LABEL, [prop]: "test" }),
    );
    expect(result.current.inputProps).toHaveProperty(prop, "test");
  });

  describe.each(["type", "pattern"])("with % prop", prop => {
    it("should pass prop through when inputElementType is input", () => {
      const { result } = renderHook(() =>
        useTextField({
          label: TEST_LABEL,
          [prop]: "test",
          inputElementType: "input",
        }),
      );
      expect(result.current.inputProps).toHaveProperty(prop, "test");
    });

    it("should not pass prop through when inputElementType is textarea", () => {
      const { result } = renderHook(() =>
        useTextField({
          label: TEST_LABEL,
          [prop]: "test",
          inputElementType: "textarea",
        }),
      );
      expect(result.current.inputProps).toHaveProperty(prop, undefined);
    });
  });
});
