import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Checkbox, CheckboxGroup } from ".";
import type { CheckboxGroupProps } from "./types";

const CHECKBOX_GROUP_CLASS_NAME = "mobius-checkbox-group";
const CHECKBOX_CLASS_NAME = "mobius-checkbox__input";

describe("CheckboxGroup", () => {
  it("renders a Checkbox inside a CheckboxGroup", () => {
    const labelText = "Favourite fruit";

    const { container } = render(
      <CheckboxGroup label="Checkbox Group">
        <Checkbox label={labelText} value="value" />
      </CheckboxGroup>,
    );

    const outerContainer = container.firstChild;
    const checkbox = screen.getByRole("checkbox");

    expect(outerContainer).toHaveClass(CHECKBOX_GROUP_CLASS_NAME);
    expect(checkbox).toHaveClass(CHECKBOX_CLASS_NAME);
  });

  it("renders a non-Checkbox outside a CheckboxGroup", () => {
    const labelText = "Favourite fruit";

    const { container } = render(<Checkbox label={labelText} value="value" />);

    const outerContainer = container.firstChild;
    const checkbox = screen.getByRole("checkbox");

    expect(outerContainer).not.toHaveClass(CHECKBOX_GROUP_CLASS_NAME);
    expect(checkbox).toHaveClass(CHECKBOX_CLASS_NAME);
  });

  it("displays an error message when errorMessage is set", () => {
    const errorMessageText = "Error message";

    render(
      <CheckboxGroup label="Checkbox Group" errorMessage={errorMessageText}>
        <Checkbox label="Checkbox" value="value" />
      </CheckboxGroup>,
    );

    const errorMessage = screen.getByText(errorMessageText);

    expect(errorMessage).toBeInTheDocument();
  });

  it("does not call onChange on initial render", () => {
    const callback = vi.fn();

    render(
      <CheckboxGroup onChange={callback}>
        <Checkbox label="Checkbox" value="value" />
      </CheckboxGroup>,
    );

    expect(callback).not.toHaveBeenCalled();
  });

  it("sets aria-describedby for all checkboxes when errorMessage is set", () => {
    const errorMessageText = "Error message";

    render(
      <CheckboxGroup label="Checkbox Group" errorMessage={errorMessageText}>
        <Checkbox label="Checkbox 1" value="value1" />
        <Checkbox label="Checkbox 2" value="value2" />
        <Checkbox label="Checkbox 3" value="value3" />
      </CheckboxGroup>,
    );

    const checkboxes = screen.getAllByRole("checkbox");

    checkboxes.forEach(checkbox => {
      expect(checkbox).toHaveAccessibleDescription(`${errorMessageText}`);
    });
  });

  describe("onChange", () => {
    describe("given an item is toggled", () => {
      it("should call onChange with a list of toggled items", async () => {
        const callback = vi.fn();
        const option1Text = "Checkbox 1";
        const option2Text = "Checkbox 2";
        const option3Text = "Checkbox 3";
        const option1Value = "value1";
        const option2Value = "value2";
        const option3Value = "value3";
        const checkedOptions = [option1Value];

        const { getByLabelText } = render(
          <CheckboxGroup onChange={callback} defaultValue={checkedOptions}>
            <Checkbox label={option1Text} value={option1Value} />
            <Checkbox label={option2Text} value={option2Value} />
            <Checkbox label={option3Text} value={option3Value} />
          </CheckboxGroup>,
        );

        const option1 = getByLabelText(option1Text);
        const option2 = getByLabelText(option2Text);
        const option3 = getByLabelText(option3Text);

        expect(option1).toBeChecked();
        expect(option2).not.toBeChecked();
        expect(option3).not.toBeChecked();

        await userEvent.click(option3);

        expect(callback).toHaveBeenCalledWith([
          ...checkedOptions,
          option3Value,
        ]);
        expect(option3).toBeChecked();
      });
    });

    describe("given an item is untoggled", () => {
      it("should call onChange with a list of toggled items", async () => {
        const callback = vi.fn();
        const option1Text = "Checkbox 1";
        const option2Text = "Checkbox 2";
        const option3Text = "Checkbox 3";
        const option1Value = "value1";
        const option2Value = "value2";
        const option3Value = "value3";
        const checkedOptions = [option2Value, option3Value];

        const { getByLabelText } = render(
          <CheckboxGroup onChange={callback} defaultValue={checkedOptions}>
            <Checkbox label={option1Text} value={option1Value} />
            <Checkbox label={option2Text} value={option2Value} />
            <Checkbox label={option3Text} value={option3Value} />
          </CheckboxGroup>,
        );

        const option1 = getByLabelText(option1Text);
        const option2 = getByLabelText(option2Text);
        const option3 = getByLabelText(option3Text);

        expect(option1).not.toBeChecked();
        expect(option2).toBeChecked();
        expect(option3).toBeChecked();

        await userEvent.click(option3);

        expect(callback).toHaveBeenCalledWith([option2Value]);
      });
    });
  });

  it("should render without error", () => {
    const optionText = "Agree";
    const view = render(
      <CheckboxGroup>
        <Checkbox label={optionText} value="value" />
      </CheckboxGroup>,
    );

    expect(view).toBeTruthy();
  });

  it("should be selectable via keyboard", async () => {
    const callback = vi.fn();
    const optionText = "Agree";

    const { getByLabelText } = render(
      <CheckboxGroup onChange={callback}>
        <Checkbox label={optionText} value="value" />
      </CheckboxGroup>,
    );

    const option = getByLabelText(optionText);

    await userEvent.tab();
    await userEvent.keyboard("[Space]");

    expect(callback).toHaveBeenCalled();
    expect(option).toBeChecked();

    await userEvent.keyboard("[Space]");

    expect(callback).toHaveBeenCalled();
    expect(option).not.toBeChecked();
  });

  it("should set the correct aria-describedby attribute", () => {
    const optionText = "Agree";
    const errorText = "This field is required";

    const { getByLabelText } = render(
      <CheckboxGroup>
        <Checkbox label={optionText} errorMessage={errorText} />
      </CheckboxGroup>,
    );

    const option = getByLabelText(optionText);

    expect(option).toHaveAccessibleDescription(`${errorText}`);
  });

  describe("isRequired", () => {
    it("should set required attribute", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isRequired>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toBeRequired();
    });

    it("should have className of --is-required when true", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isRequired>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass("--is-required");
    });

    it("should have className of --is-optional when false", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isRequired={false}>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass("--is-optional");
    });

    it("should have neither --is-required nor --is-optional when unset", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).not.toHaveClass("--is-required");
      expect(option).not.toHaveClass("--is-optional");
    });

    it("should have className of --is-required when true", () => {
      const outerContainerLabel = "Checkbox Group";
      const REQUIRED_CLASS_NAME = "--is-required";

      render(
        <CheckboxGroup
          label={outerContainerLabel}
          errorMessage="Error message"
          isRequired
        >
          <Checkbox label="Checkbox 1" value="value1" />
          <Checkbox label="Checkbox 2" value="value2" />
        </CheckboxGroup>,
      );

      const outerContainer = screen.getByLabelText(outerContainerLabel);
      const checkboxes = screen.getAllByRole("checkbox");

      expect(outerContainer).toHaveClass(REQUIRED_CLASS_NAME);

      checkboxes.forEach(checkbox => {
        expect(checkbox).toHaveClass(REQUIRED_CLASS_NAME);
      });
    });

    it("should have className of --is-optional when false", () => {
      const outerContainerLabel = "Checkbox Group";
      const OPTIONAL_CLASS_NAME = "--is-optional";

      render(
        <CheckboxGroup
          label={outerContainerLabel}
          errorMessage="Error message"
          isRequired={false}
        >
          <Checkbox label="Checkbox 1" value="value1" />
          <Checkbox label="Checkbox 2" value="value2" />
        </CheckboxGroup>,
      );

      const outerContainer = screen.getByLabelText(outerContainerLabel);
      const checkboxes = screen.getAllByRole("checkbox");

      expect(outerContainer).toHaveClass(OPTIONAL_CLASS_NAME);

      checkboxes.forEach(checkbox => {
        expect(checkbox).toHaveClass(OPTIONAL_CLASS_NAME);
      });
    });

    it("should have neither --is-required nor --is-optional when unset", () => {
      const outerContainerLabel = "Checkbox Group";
      const REQUIRED_CLASS_NAME = "--is-required";
      const OPTIONAL_CLASS_NAME = "--is-optional";

      render(
        <CheckboxGroup label={outerContainerLabel} errorMessage="Error message">
          <Checkbox label="Checkbox 1" value="value1" />
          <Checkbox label="Checkbox 2" value="value2" />
        </CheckboxGroup>,
      );

      const outerContainer = screen.getByLabelText(outerContainerLabel);

      expect(outerContainer).not.toHaveClass(REQUIRED_CLASS_NAME);
      expect(outerContainer).not.toHaveClass(OPTIONAL_CLASS_NAME);
    });
  });

  describe("uses correct class names", () => {
    it("uses mobius Mobius class", () => {
      const optionText = "Agree";

      const { container } = render(
        <CheckboxGroup>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      expect(container.firstChild).toHaveClass("mobius");
    });

    it("uses base Mobius class", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass(CHECKBOX_CLASS_NAME);
    });

    it("includes disabled class name", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isDisabled>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass("--is-disabled");
    });

    describe("includes orientation class name", () => {
      it.each(["horizontal", "vertical"] as const)(
        "when orientation is %s",
        orientation => {
          const props = { orientation } as CheckboxGroupProps;
          const { container } = render(
            <CheckboxGroup {...props}>
              <Checkbox label="Agree" value="value" />
            </CheckboxGroup>,
          );

          expect(container.firstChild).toHaveClass(`--is-${orientation}`);
        },
      );
    });

    it("includes valid class name", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isInvalid={false}>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass("--is-valid");
    });

    it("includes invalid class name", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isInvalid errorMessage="There is a problem">
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass("--is-invalid");
    });

    it("includes custom class name", () => {
      const optionText = "Agree";
      const customClass = "my-class";

      const { getByLabelText } = render(
        <CheckboxGroup>
          <Checkbox label={optionText} className={customClass} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveClass(CHECKBOX_CLASS_NAME);
    });

    it("only sets id on the input element", () => {
      const optionText = "Agree";
      const testId = "test";

      const { getByLabelText } = render(
        <CheckboxGroup>
          <Checkbox id={testId} label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveAttribute("id", testId);
    });

    it("includes focused and selected class names", async () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      await userEvent.tab();

      expect(option).toHaveClass(CHECKBOX_CLASS_NAME);

      await userEvent.keyboard("[Space]");

      expect(option).toHaveClass("--is-selected");
    });
  });

  describe("items per row", () => {
    describe("when props is set", () => {
      it("provides CSS variable with the number of items per row", () => {
        const { container } = render(
          <CheckboxGroup itemsPerRow={2}>
            <Checkbox label="Option 1" value="value1" />
            <Checkbox label="Option 2" value="value2" />
            <Checkbox label="Option 3" value="value3" />
            <Checkbox label="Option 4" value="value4" />
          </CheckboxGroup>,
        );

        expect(container.firstChild).toHaveStyle("--checkbox-items-per-row: 2");
      });
    });

    describe("when props is not set", () => {
      it("provides CSS variable with the number of items per row", () => {
        const numberOfItems = 3;

        const { container } = render(
          <CheckboxGroup>
            <Checkbox label="Option 1" value="value1" />
            <Checkbox label="Option 2" value="value2" />
            <Checkbox label="Option 3" value="value3" />
          </CheckboxGroup>,
        );

        expect(container.firstChild).toHaveStyle(
          `--checkbox-items-per-row: ${numberOfItems}`,
        );
      });
    });
  });

  describe("given the component is read only", () => {
    it("renders readonly attribute", () => {
      const optionText = "Agree";

      const { getByLabelText } = render(
        <CheckboxGroup isReadOnly>
          <Checkbox label={optionText} value="value" />
        </CheckboxGroup>,
      );

      const option = getByLabelText(optionText);

      expect(option).toHaveAttribute("readonly");
    });
  });

  describe("last item disables", () => {
    it("should disable all checkboxes except the last one when the last item is selected", async () => {
      const option1Text = "Checkbox 1";
      const option2Text = "Checkbox 2";
      const option3Text = "Checkbox 3";
      const option1Value = "value1";
      const option2Value = "value2";
      const option3Value = "value3";

      render(
        <CheckboxGroup lastItemDisables>
          <Checkbox label={option1Text} value={option1Value} />
          <Checkbox label={option2Text} value={option2Value} />
          <Checkbox label={option3Text} value={option3Value} />
        </CheckboxGroup>,
      );

      const option1 = screen.getByLabelText(option1Text);
      const option2 = screen.getByLabelText(option2Text);
      const option3 = screen.getByLabelText(option3Text);

      await userEvent.click(option3);

      expect(option1).toBeDisabled();
      expect(option2).toBeDisabled();
      expect(option3).toBeEnabled();
    });

    it("should unchecked all checkboxes except the last one when the last item is selected", async () => {
      const option1Text = "Checkbox 1";
      const option2Text = "Checkbox 2";
      const option3Text = "Checkbox 3";
      const option1Value = "value1";
      const option2Value = "value2";
      const option3Value = "value3";

      render(
        <CheckboxGroup lastItemDisables>
          <Checkbox label={option1Text} value={option1Value} />
          <Checkbox label={option2Text} value={option2Value} />
          <Checkbox label={option3Text} value={option3Value} />
        </CheckboxGroup>,
      );

      const option1 = screen.getByLabelText(option1Text);
      const option2 = screen.getByLabelText(option2Text);
      const option3 = screen.getByLabelText(option3Text);

      await userEvent.click(option1);
      await userEvent.click(option3);

      expect(option1).not.toBeChecked();
      expect(option2).not.toBeChecked();
      expect(option3).toBeChecked();
    });

    it("should overwrite group value when last item is selected", async () => {
      const callback = vi.fn();

      const option1Text = "Checkbox 1";
      const option2Text = "Checkbox 2";
      const option3Text = "Checkbox 3";
      const option1Value = "value1";
      const option2Value = "value2";
      const option3Value = "value3";

      render(
        <CheckboxGroup lastItemDisables onChange={callback}>
          <Checkbox label={option1Text} value={option1Value} />
          <Checkbox label={option2Text} value={option2Value} />
          <Checkbox label={option3Text} value={option3Value} />
        </CheckboxGroup>,
      );

      const option1 = screen.getByLabelText(option1Text);
      const option2 = screen.getByLabelText(option2Text);
      const option3 = screen.getByLabelText(option3Text);

      await userEvent.click(option1);
      await userEvent.click(option2);

      expect(callback).toHaveBeenCalledWith([option1Value, option2Value]);

      await userEvent.click(option3);

      expect(callback).toHaveBeenCalledWith([option3Value]);

      // Resets to empty array when last item unchecked again
      await userEvent.click(option3);

      expect(callback).toHaveBeenCalledWith([]);
    });
  });

  describe("defaultValue changes", () => {
    it("should respect defaultValue prop changes without infinite re-renders", () => {
      const option1Text = "Option 1";
      const option2Text = "Option 2";
      const option1Value = "value1";
      const option2Value = "value2";

      const { rerender } = render(
        <CheckboxGroup defaultValue={[option1Value]}>
          <Checkbox label={option1Text} value={option1Value} />
          <Checkbox label={option2Text} value={option2Value} />
        </CheckboxGroup>,
      );

      const option1 = screen.getByLabelText(option1Text);
      const option2 = screen.getByLabelText(option2Text);

      // Initially, option1 should be checked
      expect(option1).toBeChecked();
      expect(option2).not.toBeChecked();

      // Re-render with different defaultValue
      rerender(
        <CheckboxGroup defaultValue={[option2Value]}>
          <Checkbox label={option1Text} value={option1Value} />
          <Checkbox label={option2Text} value={option2Value} />
        </CheckboxGroup>,
      );

      // After re-render, option2 should be checked
      expect(option1).not.toBeChecked();
      expect(option2).toBeChecked();
    });
  });
});
