/*
 * Copyright 2023 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { format, parse } from "date-fns";
import * as Locales from "date-fns/locale";
import esLocale from "date-fns/locale/es";
import { mount, type ReactWrapper } from "enzyme";
import { act } from "react";
import * as TestUtils from "react-dom/test-utils";

import {
    Boundary,
    Classes as CoreClasses,
    type HTMLDivProps,
    type HTMLInputProps,
    InputGroup,
    type InputGroupProps,
    Popover,
    type PopoverProps,
} from "@blueprintjs/core";
import { expectPropValidationError } from "@blueprintjs/test-commons";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";

import { Classes, type DateFormatProps, type DateRange, Months, TimePrecision } from "../..";
import { ReactDayPickerClasses } from "../../common/classes";
import { loadDateFnsLocaleFake } from "../../common/loadDateFnsLocaleFake";
import { DateRangeInput, type DateRangeInputProps } from "../date-range-input/dateRangeInput";
import { DateRangePicker } from "../date-range-picker/dateRangePicker";

type NullableRange<T> = [T | null, T | null];
type DateStringRange = NullableRange<string>;

type WrappedComponentRoot = ReactWrapper<any>;
type WrappedComponentInput = ReactWrapper<HTMLInputProps, any>;
type WrappedComponentDayElement = ReactWrapper<HTMLDivProps, any>;

type OutOfRangeTestFunction = (
    inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput,
    inputString: string,
    boundary?: Boundary,
) => void;

type InvalidDateTestFunction = (
    inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput,
    boundary: Boundary,
    otherInputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput,
) => void;

// Change the default for testability
DateRangeInput.defaultProps.popoverProps = { usePortal: false };
(DateRangeInput.defaultProps as DateRangeInputProps).dateFnsLocaleLoader = loadDateFnsLocaleFake;

const DATE_FORMAT = getDateFnsFormatter("M/d/yyyy");
const DATETIME_FORMAT = getDateFnsFormatter("M/d/yyyy HH:mm:ss");

describe("<DateRangeInput>", () => {
    let containerElement: HTMLElement;
    let mountedWrappers: ReactWrapper[] = [];

    beforeEach(() => {
        containerElement = document.createElement("div");
        document.body.appendChild(containerElement);
    });

    afterEach(() => {
        // Unmount all Enzyme wrappers before removing the container to prevent
        // React from trying to commit updates to removed DOM nodes, which causes
        // "window is not defined" and "Should not already be working" errors.
        try {
            for (const wrapper of mountedWrappers) {
                try {
                    wrapper.unmount();
                } catch {
                    // best-effort: continue unmounting remaining wrappers
                }
            }
        } finally {
            mountedWrappers = [];
            containerElement.remove();
        }
    });

    const YEAR = 2022;
    const START_DAY = 22;
    const START_DATE = new Date(YEAR, Months.JANUARY, START_DAY);
    const START_STR = DATE_FORMAT.formatDate(START_DATE);
    const END_DAY = 24;
    const END_DATE = new Date(YEAR, Months.JANUARY, END_DAY);
    const END_STR = DATE_FORMAT.formatDate(END_DATE);
    const DATE_RANGE = [START_DATE, END_DATE] as DateRange;

    const START_DATE_2 = new Date(YEAR, Months.JANUARY, 1);
    const START_STR_2 = DATE_FORMAT.formatDate(START_DATE_2);
    const START_STR_2_ES_LOCALE = "1 de enero de 2022";
    const END_DATE_2 = new Date(YEAR, Months.JANUARY, 31);
    const END_STR_2 = DATE_FORMAT.formatDate(END_DATE_2);
    const END_STR_2_ES_LOCALE = "31 de enero de 2022";
    const DATE_RANGE_2 = [START_DATE_2, END_DATE_2] as DateRange;

    const INVALID_STR = "<this is an invalid date string>";
    const INVALID_MESSAGE = "Custom invalid-date message";

    const OUT_OF_RANGE_TEST_MIN = new Date(2000, 1, 1);
    const OUT_OF_RANGE_TEST_MAX = new Date(2030, 1, 1);
    const OUT_OF_RANGE_START_DATE = new Date(1000, 1, 1);
    const OUT_OF_RANGE_START_STR = DATE_FORMAT.formatDate(OUT_OF_RANGE_START_DATE);
    const OUT_OF_RANGE_END_DATE = new Date(3000, 1, 1);
    const OUT_OF_RANGE_END_STR = DATE_FORMAT.formatDate(OUT_OF_RANGE_END_DATE);
    const OUT_OF_RANGE_MESSAGE = "Custom out-of-range message";

    const OVERLAPPING_DATES_MESSAGE = "Custom overlapping-dates message";
    const OVERLAPPING_START_DATE = END_DATE_2; // should be later then END_DATE
    const OVERLAPPING_END_DATE = START_DATE_2; // should be earlier then START_DATE
    const OVERLAPPING_START_STR = DATE_FORMAT.formatDate(OVERLAPPING_START_DATE);
    const OVERLAPPING_END_STR = DATE_FORMAT.formatDate(OVERLAPPING_END_DATE);

    const OVERLAPPING_START_DATETIME = new Date(2022, Months.JANUARY, 1, 9); // should be same date but later time
    const OVERLAPPING_END_DATETIME = new Date(2022, Months.JANUARY, 1, 1); // should be same date but earlier time
    const OVERLAPPING_START_DT_STR = DATETIME_FORMAT.formatDate(OVERLAPPING_START_DATETIME);
    const OVERLAPPING_END_DT_STR = DATETIME_FORMAT.formatDate(OVERLAPPING_END_DATETIME);
    const DATE_RANGE_3 = [OVERLAPPING_END_DATETIME, OVERLAPPING_START_DATETIME] as DateRange; // initial state should be correct

    // a custom string representation for `new Date(undefined)` that we use in
    // date-range equality checks just in this file
    const UNDEFINED_DATE_STR = "<UNDEFINED DATE>";

    it("should render with two InputGroup children", () => {
        const component = mount(<DateRangeInput {...DATE_FORMAT} />);
        expect(component.find(InputGroup)).toHaveLength(2);
    });

    it("should pass custom classNames to popover wrapper", () => {
        const CLASS_1 = "foo";
        const CLASS_2 = "bar";

        const wrapper = mount(
            <DateRangeInput
                {...DATE_FORMAT}
                className={CLASS_1}
                popoverProps={{ className: CLASS_2, usePortal: false }}
            />,
        );
        act(() => {
            wrapper.setState({ isOpen: true });
        });

        const popoverTarget = wrapper.find(`.${CoreClasses.POPOVER_TARGET}`).hostNodes();
        expect(popoverTarget.hasClass(CLASS_1)).toBe(true);
        expect(popoverTarget.hasClass(CLASS_2)).toBe(true);
    });

    it("should pass all supported props to inner DateRangePicker", () => {
        const component = mount(<DateRangeInput {...DATE_FORMAT} locale="uk" contiguousCalendarMonths={false} />);
        act(() => {
            component.setState({ isOpen: true });
        });
        component.update();
        const picker = component.find(DateRangePicker);
        expect(picker.prop("locale")).toBe("uk");
        expect(picker.prop("contiguousCalendarMonths")).toBe(false);
    });

    it("should show empty fields when no date range is selected", () => {
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} />);
        assertInputValuesEqual(root, "", "");
    });

    it("should throw error if value === null", () => {
        expectPropValidationError(DateRangeInput, { ...DATE_FORMAT, value: null! });
    });

    describe("timePrecision prop", () => {
        it("<TimePicker /> should not lose focus on increment/decrement with up/down arrows", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} timePrecision={TimePrecision.MINUTE} />, true);

            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();
            expect(root.find(Popover).prop("isOpen")).toBe(true);

            keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp");
            expect(isStartInputFocused(root)).toBe(false);
            expect(isEndInputFocused(root)).toBe(false);
        });

        it("when timePrecision != null && closeOnSelection=true && <TimePicker /> values is changed popover should not close", () => {
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} timePrecision={TimePrecision.MINUTE} />,
                true,
            );

            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();

            getDayElement(1).simulate("click");
            getDayElement(10).simulate("click");

            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();

            keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp");
            root.update();
            expect(root.find(Popover).prop("isOpen")).toBe(true);
        });

        it("when timePrecision != null && closeOnSelection=true && end <TimePicker /> values is changed directly (without setting the selectedEnd date) - popover should not close", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} timePrecision={TimePrecision.MINUTE} />, true);

            act(() => {
                root.setState({ isOpen: true });
            });
            keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp");
            root.update();
            keyDownOnInput(Classes.TIMEPICKER_HOUR, "ArrowUp", 1);
            root.update();
            expect(root.find(Popover).prop("isOpen")).toBe(true);
        });

        function keyDownOnInput(className: string, key: string, inputElementIndex: number = 0) {
            TestUtils.Simulate.keyDown(findTimePickerInputElement(className, inputElementIndex), { key });
        }

        function findTimePickerInputElement(className: string, inputElementIndex: number = 0) {
            return document.querySelectorAll<HTMLInputElement>(`.${Classes.TIMEPICKER_INPUT}.${className}`)[
                inputElementIndex
            ];
        }
    });

    describe("startInputProps and endInputProps", () => {
        it("should disable startInput when startInputProps={ disabled: true }", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} startInputProps={{ disabled: true }} />);
            const startInput = getStartInput(root);

            startInput.simulate("click");
            expect(root.find(Popover).prop("isOpen")).toBe(false);
            expect(startInput.prop("disabled")).toBe(true);
        });

        it("should not disable endInput when startInputProps={ disabled: true }", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} startInputProps={{ disabled: true }} />);
            const endInput = getEndInput(root);
            expect(endInput.prop("disabled")).toBe(false);
        });

        it("should disable endInput when endInputProps={ disabled: true }", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} endInputProps={{ disabled: true }} />);
            const endInput = getEndInput(root);

            endInput.simulate("click");
            expect(root.find(Popover).prop("isOpen")).toBe(false);
            expect(endInput.prop("disabled")).toBe(true);
        });

        it("should not disable startInput when endInputProps={ disabled: true }", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} endInputProps={{ disabled: true }} />);
            const startInput = getStartInput(root);
            expect(startInput.prop("disabled")).toBe(false);
        });

        describe("startInputProps", () => {
            runTestSuite(getStartInput, inputGroupProps => {
                return mount(<DateRangeInput {...DATE_FORMAT} startInputProps={inputGroupProps} />);
            });
        });

        describe("endInputProps", () => {
            runTestSuite(getEndInput, inputGroupProps => {
                return mount(<DateRangeInput {...DATE_FORMAT} endInputProps={inputGroupProps} />);
            });
        });

        function runTestSuite(
            inputGetterFn: (root: WrappedComponentRoot) => WrappedComponentInput,
            mountFn: (inputGroupProps: InputGroupProps) => any,
        ) {
            it("should allow custom placeholder text", () => {
                const root = mountFn({ placeholder: "Hello" });
                expect(getInputPlaceholderText(inputGetterFn(root))).toBe("Hello");
            });

            it("should support custom style", () => {
                const root = mountFn({ style: { background: "yellow" } });
                const inputElement = inputGetterFn(root).getDOMNode<HTMLElement>();
                expect(inputElement.style.background).toBe("yellow");
            });

            // verify custom callbacks are called for each event that we listen for internally.
            // (note: we could be more clever and accept just one string param here, but this
            // approach keeps both string params grep-able in the codebase.)
            runCallbackTest("onChange", "change");
            runCallbackTest("onFocus", "focus");
            runCallbackTest("onBlur", "blur");
            runCallbackTest("onClick", "click");
            runCallbackTest("onKeyDown", "keydown");
            runCallbackTest("onMouseDown", "mousedown");

            function runCallbackTest(callbackName: string, eventName: string) {
                it(`should fire custom ${callbackName} callback`, () => {
                    const spy = vi.fn();
                    const component = mountFn({ [callbackName]: spy });
                    const input = inputGetterFn(component);
                    input.simulate(eventName);
                    expect(spy).toHaveBeenCalledOnce();
                });
            }
        }
    });

    // NOTE: Enzyme simulate("focus") doesn't trigger placeholder changes under jsdom. Needs RTL migration.
    describe.skip("placeholder text", () => {
        it("should show proper placeholder text when empty inputs are focused and unfocused", () => {
            // arbitrarily choose the out-of-range tests' min/max dates for this test
            const MIN_DATE = new Date(2022, Months.JANUARY, 1);
            const MAX_DATE = new Date(2022, Months.JANUARY, 31);
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} minDate={MIN_DATE} maxDate={MAX_DATE} />);

            const startInput = getStartInput(root);
            const endInput = getEndInput(root);

            expect(getInputPlaceholderText(startInput)).toBe("Start date");
            expect(getInputPlaceholderText(endInput)).toBe("End date");

            startInput.simulate("focus");
            expect(getInputPlaceholderText(startInput)).toBe(DATE_FORMAT.formatDate(MIN_DATE));
            startInput.simulate("blur");
            endInput.simulate("focus");
            expect(getInputPlaceholderText(endInput)).toBe(DATE_FORMAT.formatDate(MAX_DATE));
        });

        // need to check this case, because formatted min/max date strings are cached internally
        // until props change again
        it("should update placeholder text properly when min/max dates change", () => {
            const MIN_DATE_1 = new Date(2022, Months.JANUARY, 1);
            const MAX_DATE_1 = new Date(2022, Months.JANUARY, 31);
            const MIN_DATE_2 = new Date(2022, Months.JANUARY, 2);
            const MAX_DATE_2 = new Date(2022, Months.FEBRUARY, 1);
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} minDate={MIN_DATE_1} maxDate={MAX_DATE_1} />);

            const startInput = getStartInput(root);
            const endInput = getEndInput(root);

            // change while end input is still focused to make sure things change properly in spite of that
            endInput.simulate("focus");
            root.setProps({ maxDate: MAX_DATE_2, minDate: MIN_DATE_2 });

            endInput.simulate("blur");
            startInput.simulate("focus");
            expect(getInputPlaceholderText(startInput)).toBe(DATE_FORMAT.formatDate(MIN_DATE_2));
            startInput.simulate("blur");
            endInput.simulate("focus");
            expect(getInputPlaceholderText(endInput)).toBe(DATE_FORMAT.formatDate(MAX_DATE_2));
        });

        it("should update placeholder text properly when format changes", () => {
            const MIN_DATE = new Date(2022, Months.JANUARY, 1);
            const MAX_DATE = new Date(2022, Months.JANUARY, 31);
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} minDate={MIN_DATE} maxDate={MAX_DATE} />);

            const startInput = getStartInput(root);
            const endInput = getEndInput(root);

            root.setProps({ format: "MM/DD/YYYY" });

            startInput.simulate("focus");
            expect(getInputPlaceholderText(startInput)).toBe("01/01/2022");
            startInput.simulate("blur");
            endInput.simulate("focus");
            expect(getInputPlaceholderText(endInput)).toBe("01/31/2022");
        });
    });

    it("should disable inputs and not open popover if disabled=true", () => {
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} disabled={true} />);
        const startInput = getStartInput(root);
        startInput.simulate("click");
        expect(root.find(Popover).prop("isOpen")).toBe(false);
        expect(startInput.prop("disabled")).toBe(true);
        expect(getEndInput(root).prop("disabled")).toBe(true);
    });

    describe("closeOnSelection", () => {
        it("should keep popover open when full date range is selected if closeOnSelection=false", () => {
            const { root, getDayElement } = wrap(<DateRangeInput {...DATE_FORMAT} closeOnSelection={false} />, true);
            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();
            getDayElement(1).simulate("click");
            getDayElement(10).simulate("click");
            expect(root.state("isOpen")).toBe(true);
            root.unmount();
        });

        it("should close popover when full date range is selected if closeOnSelection=true", () => {
            const { root, getDayElement } = wrap(<DateRangeInput {...DATE_FORMAT} />, true);
            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();
            getDayElement(1).simulate("click");
            getDayElement(10).simulate("click");
            expect(root.state("isOpen")).toBe(false);
            root.unmount();
        });

        it("should close popover when full date range is selected if closeOnSelection=true && timePrecision != null", () => {
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} timePrecision={TimePrecision.MINUTE} />,
                true,
            );
            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();
            getDayElement(1).simulate("click");
            getDayElement(10).simulate("click");
            root.update();
            expect(root.state("isOpen")).toBe(false);
            root.unmount();
        });
    });

    it("should accept contiguousCalendarMonths prop and pass it to the date range picker", () => {
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} contiguousCalendarMonths={false} />);
        act(() => {
            root.setState({ isOpen: true });
        });
        root.update();
        expect(root.find(DateRangePicker).prop("contiguousCalendarMonths")).toBe(false);
    });

    it("should accept singleMonthOnly prop and pass it to the date range picker", () => {
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} singleMonthOnly={false} />);
        act(() => {
            root.setState({ isOpen: true });
        });
        root.update();
        expect(root.find(DateRangePicker).prop("singleMonthOnly")).toBe(false);
    });

    it("should accept shortcuts prop and pass it to the date range picker", () => {
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} shortcuts={false} />);
        act(() => {
            root.setState({ isOpen: true });
        });
        root.update();
        expect(root.find(DateRangePicker).prop("shortcuts")).toBe(false);
    });

    it("should update the selectedShortcutIndex state when clicking on a shortcut", () => {
        const selectedShortcut = 1;
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} />);

        act(() => {
            root.setState({ isOpen: true });
        });
        root.update();
        root.find(DateRangePicker)
            .find(`.${Classes.DATERANGEPICKER_SHORTCUTS}`)
            .find("a")
            .at(selectedShortcut)
            .simulate("click");
        expect(root.state("selectedShortcutIndex")).equals(selectedShortcut);
    });

    it("should blur the start field and close the popover when pressing Shift+Tab", () => {
        const startInputProps = { onKeyDown: vi.fn() };
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} {...{ startInputProps }} />);
        const startInput = getStartInput(root);
        startInput.simulate("keydown", { key: "Tab", shiftKey: true });
        expect(root.state("isStartInputFocused")).toBe(false);
        expect(startInputProps.onKeyDown).toHaveBeenCalledOnce();
        expect(root.state("isOpen")).toBe(false);
    });

    it("should blur the end field and close the popover when pressing Tab", () => {
        const endInputProps = { onKeyDown: vi.fn() };
        const { root } = wrap(<DateRangeInput {...DATE_FORMAT} {...{ endInputProps }} />);
        const endInput = getEndInput(root);
        endInput.simulate("keydown", { key: "Tab" });
        expect(root.state("isEndInputFocused")).toBe(false);
        expect(endInputProps.onKeyDown).toHaveBeenCalledOnce();
        expect(root.state("isOpen")).toBe(false);
    });

    describe("selectAllOnFocus", () => {
        it("should not select any text on focus if false (the default)", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} defaultValue={[START_DATE, null]} />, true);

            const startInput = getStartInput(root);
            startInput.simulate("focus");

            const startInputNode = containerElement!.querySelectorAll("input")[0];
            expect(startInputNode.selectionStart).toBe(startInputNode.selectionEnd);
        });

        it("should select all text on focus if true", () => {
            const { root } = wrap(
                <DateRangeInput {...DATE_FORMAT} defaultValue={[START_DATE, null]} selectAllOnFocus={true} />,
                true,
            );

            const startInput = getStartInput(root);
            startInput.simulate("focus");

            const startInputNode = containerElement!.querySelectorAll("input")[0];
            expect(startInputNode.selectionStart).toBe(0);
            expect(startInputNode.selectionEnd).toBe(START_STR.length);
        });

        it.skip("should select all text on day mouseenter in calendar if true", () => {
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} defaultValue={[START_DATE, null]} selectAllOnFocus={true} />,
                true,
            );

            act(() => {
                root.setState({ isOpen: true });
            });
            // getDay is 0-indexed, but getDayElement is 1-indexed
            getDayElement(START_DATE_2.getDay() + 1).simulate("mouseenter");

            const startInputNode = containerElement!.querySelectorAll("input")[0];
            expect(startInputNode.selectionStart).toBe(0);
            expect(startInputNode.selectionEnd).toBe(START_STR.length);
        });
    });

    describe("allowSingleDayRange", () => {
        it("should allow start and end to be the same day when clicking", () => {
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} allowSingleDayRange={true} defaultValue={[START_DATE, END_DATE]} />,
            );
            getEndInput(root).simulate("focus");
            getDayElement(END_DAY).simulate("click");
            getDayElement(START_DAY).simulate("click");
            assertInputValuesEqual(root, START_STR, START_STR);
        });

        it("should allow start and end to be the same day when typing", () => {
            const { root } = wrap(
                <DateRangeInput {...DATE_FORMAT} allowSingleDayRange={true} defaultValue={[START_DATE, END_DATE]} />,
            );
            changeEndInputText(root, "");
            changeEndInputText(root, START_STR);
            assertInputValuesEqual(root, START_STR, START_STR);
        });
    });

    describe("popoverProps", () => {
        it("should accept custom popoverProps", () => {
            const popoverProps: Partial<PopoverProps> = {
                backdropProps: {},
                placement: "top-start",
                usePortal: false,
            };
            const popover = wrap(<DateRangeInput {...DATE_FORMAT} popoverProps={popoverProps} />).root.find(Popover);
            expect(popover.prop("backdropProps")).toBe(popoverProps.backdropProps);
            expect(popover.prop("placement")).toBe(popoverProps.placement);
        });

        it("should ignore autoFocus, enforceFocus, and content in custom popoverProps", () => {
            const CUSTOM_CONTENT = "Here is some custom content";
            const popoverProps = {
                autoFocus: true,
                content: CUSTOM_CONTENT,
                enforceFocus: true,
                usePortal: false,
            };
            const popover = wrap(<DateRangeInput {...DATE_FORMAT} popoverProps={popoverProps} />).root.find(Popover);
            // this test assumes the following values will be the defaults internally
            expect(popover.prop("autoFocus")).toBe(false);
            expect(popover.prop("enforceFocus")).toBe(false);
            expect(popover.prop("content")).not.toBe(CUSTOM_CONTENT);
        });
    });

    describe("when uncontrolled", () => {
        it("should show empty fields when defaultValue is [null, null]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} defaultValue={[null, null]} />);
            assertInputValuesEqual(root, "", "");
        });

        it("should show empty start field and formatted date in end field when defaultValue is [null, <date>]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} defaultValue={[null, END_DATE]} />);
            assertInputValuesEqual(root, "", END_STR);
        });

        it("should show empty end field and formatted date in start field when defaultValue is [<date>, null]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} defaultValue={[START_DATE, null]} />);
            assertInputValuesEqual(root, START_STR, "");
        });

        it("should show formatted dates in both fields when defaultValue is [<date1>, <date2>]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} defaultValue={[START_DATE, END_DATE]} />);
            assertInputValuesEqual(root, START_STR, END_STR);
        });

        // HACKHACK: https://github.com/palantir/blueprint/issues/6109
        // N.B. this test passes locally
        it.skip("Pressing Enter saves the inputted date and closes the popover", () => {
            const startInputProps = { onKeyDown: vi.fn() };
            const endInputProps = { onKeyDown: vi.fn() };
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} {...{ endInputProps, startInputProps }} />);
            act(() => {
                root.setState({ isOpen: true });
            });

            // Don't save the input elements into variables; they can become
            // stale across React updates.

            getStartInput(root).simulate("focus");
            getStartInput(root).simulate("change", { target: { value: START_STR } });
            getStartInput(root).simulate("keydown", { key: "Enter" });
            expect(startInputProps.onKeyDown).toHaveBeenCalledOnce();
            expect(isStartInputFocused(root)).toBe(false);

            expect(root.state("isOpen")).toBe(true);

            getEndInput(root).simulate("focus");
            getEndInput(root).simulate("change", { target: { value: END_STR } });
            getEndInput(root).simulate("keydown", { key: "Enter" });
            expect(endInputProps.onKeyDown).toHaveBeenCalledOnce();
            expect(isEndInputFocused(root)).toBe(true);

            expect(getStartInput(root).prop("value")).toBe(START_STR);
            expect(getEndInput(root).prop("value")).toBe(END_STR);

            expect(root.state("isOpen")).toBe(false);
        });

        it("should invoke onChange with the new date range and update the input fields when clicking a date", () => {
            const defaultValue = [START_DATE, null] as DateRange;

            const onChange = vi.fn();
            const { root, getDayElement } = wrap(
                <DateRangeInput
                    {...DATE_FORMAT}
                    closeOnSelection={false}
                    defaultValue={defaultValue}
                    onChange={onChange}
                />,
            );
            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();

            getDayElement(END_DAY).simulate("click");
            assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, END_STR]);
            assertInputValuesEqual(root, START_STR, END_STR);

            getDayElement(START_DAY).simulate("click");
            assertDateRangesEqual(onChange.mock.calls[1][0], [null, END_STR]);
            assertInputValuesEqual(root, "", END_STR);

            getDayElement(END_DAY).simulate("click");
            assertDateRangesEqual(onChange.mock.calls[2][0], [null, null]);
            assertInputValuesEqual(root, "", "");

            getDayElement(START_DAY).simulate("click");
            assertDateRangesEqual(onChange.mock.calls[3][0], [START_STR, null]);
            assertInputValuesEqual(root, START_STR, "");

            expect(onChange.mock.calls.length).toBe(4);
        });

        it(`should invoke onChange with the new date range and update the input fields when typing a valid start or end date`, () => {
            const onChange = vi.fn();
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={DATE_RANGE} />);

            changeStartInputText(root, START_STR_2);
            expect(onChange.mock.calls.length).toBe(1);
            assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR_2, END_STR]);
            assertInputValuesEqual(root, START_STR_2, END_STR);

            changeEndInputText(root, END_STR_2);
            expect(onChange.mock.calls.length).toBe(2);
            assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR_2, END_STR_2]);
            assertInputValuesEqual(root, START_STR_2, END_STR_2);
        });

        it(`should show the typed date, not the hovered date, when typing in a field while hovering over a date`, () => {
            const onChange = vi.fn();
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={DATE_RANGE} />,
            );

            getStartInput(root).simulate("focus");
            getDayElement(1).simulate("mouseenter");
            changeStartInputText(root, START_STR_2);
            assertInputValuesEqual(root, START_STR_2, END_STR);
        });

        // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
        describe.skip("Typing an out-of-range date", () => {
            // we run the same four tests for each of several cases. putting
            // setup logic in beforeEach lets us express our it(...) tests as
            // nice one-liners further down this block, and it also gives
            // certain tests easy access to onError/onChange if they need it.

            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                // use defaultValue to specify the calendar months in view
                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        defaultValue={DATE_RANGE}
                        minDate={OUT_OF_RANGE_TEST_MIN}
                        maxDate={OUT_OF_RANGE_TEST_MAX}
                        onError={onError}
                        outOfRangeMessage={OUT_OF_RANGE_MESSAGE}
                    />,
                );
                root = result.root;

                // clear the fields *before* setting up an onChange callback to
                // keep onChange.mock.calls.length at 0 before tests run
                changeStartInputText(root, "");
                changeEndInputText(root, "");
                root.setProps({ onChange });
            });

            describe("shows the error message on blur", () => {
                runTestForEachScenario((inputGetterFn, inputString) => {
                    changeInputText(inputGetterFn(root), inputString);
                    inputGetterFn(root).simulate("blur");
                    assertInputValueEquals(inputGetterFn(root), OUT_OF_RANGE_MESSAGE);
                });
            });

            describe("shows the offending date in the field on focus", () => {
                runTestForEachScenario((inputGetterFn, inputString) => {
                    changeInputText(inputGetterFn(root), inputString);
                    inputGetterFn(root).simulate("blur");
                    inputGetterFn(root).simulate("focus");
                    assertInputValueEquals(inputGetterFn(root), inputString);
                });
            });

            describe("calls onError with invalid date on blur", () => {
                runTestForEachScenario((inputGetterFn, inputString, boundary) => {
                    const expectedRange: DateStringRange =
                        boundary === Boundary.START ? [inputString, null] : [null, inputString];
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), inputString);
                    expect(onError).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();
                    assertDateRangesEqual(onError.mock.calls[0][0], expectedRange);
                });
            });

            describe("does NOT call onChange before OR after blur", () => {
                runTestForEachScenario((inputGetterFn, inputString) => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), inputString);
                    expect(onChange).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });
            });

            describe("removes error message if input is changed to an in-range date again", () => {
                runTestForEachScenario((inputGetterFn, inputString) => {
                    changeInputText(inputGetterFn(root), inputString);
                    inputGetterFn(root).simulate("blur");

                    const IN_RANGE_DATE_STR = START_STR;
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), IN_RANGE_DATE_STR);
                    inputGetterFn(root).simulate("blur");
                    assertInputValueEquals(inputGetterFn(root), IN_RANGE_DATE_STR);
                });
            });

            function runTestForEachScenario(runTestFn: OutOfRangeTestFunction) {
                const { START, END } = Boundary; // deconstruct to keep line lengths under threshold
                it("if start < minDate", () => runTestFn(getStartInput, OUT_OF_RANGE_START_STR, START));
                it("if start > maxDate", () => runTestFn(getStartInput, OUT_OF_RANGE_END_STR, START));
                it("if end < minDate", () => runTestFn(getEndInput, OUT_OF_RANGE_START_STR, END));
                it("if end > maxDate", () => runTestFn(getEndInput, OUT_OF_RANGE_END_STR, END));
            }
        });

        // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
        describe.skip("Typing an invalid date", () => {
            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        defaultValue={DATE_RANGE}
                        invalidDateMessage={INVALID_MESSAGE}
                        onError={onError}
                    />,
                );
                root = result.root;

                // clear the fields *before* setting up an onChange callback to
                // keep onChange.mock.calls.length at 0 before tests run
                changeStartInputText(root, "");
                changeEndInputText(root, "");
                root.setProps({ onChange });
            });

            describe("shows the error message on blur", () => {
                runTestForEachScenario(inputGetterFn => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    inputGetterFn(root).simulate("blur");
                    assertInputValueEquals(inputGetterFn(root), INVALID_MESSAGE);
                });
            });

            describe("keeps showing the error message on next focus", () => {
                runTestForEachScenario(inputGetterFn => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    inputGetterFn(root).simulate("blur");
                    inputGetterFn(root).simulate("focus");
                    assertInputValueEquals(inputGetterFn(root), INVALID_MESSAGE);
                });
            });

            describe.skip("calls onError on blur with Date(undefined) in place of the invalid date", () => {
                runTestForEachScenario((inputGetterFn, boundary) => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    expect(onError).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();

                    const dateRange = onError.mock.calls[0][0];
                    const dateIndex = boundary === Boundary.START ? 0 : 1;
                    expect((dateRange[dateIndex] as Date).valueOf()).toBeNaN();
                });
            });

            describe("does NOT call onChange before OR after blur", () => {
                runTestForEachScenario(inputGetterFn => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    expect(onChange).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });
            });

            describe("removes error message if input is changed to an in-range date again", () => {
                runTestForEachScenario(inputGetterFn => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    inputGetterFn(root).simulate("blur");

                    // just use START_STR for this test, because it will be
                    // valid in either field.
                    const VALID_STR = START_STR;
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), VALID_STR);
                    inputGetterFn(root).simulate("blur");
                    assertInputValueEquals(inputGetterFn(root), VALID_STR);
                });
            });

            describe("calls onChange if last-edited boundary is in range and the other boundary is out of range", () => {
                runTestForEachScenario((inputGetterFn, boundary, otherInputGetterFn) => {
                    otherInputGetterFn(root).simulate("focus");
                    changeInputText(otherInputGetterFn(root), INVALID_STR);
                    otherInputGetterFn(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();

                    const VALID_STR = START_STR;
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), VALID_STR);
                    expect(onChange).toHaveBeenCalledOnce(); // because latest date is valid

                    const actualRange = onChange.mock.calls[0][0];
                    const expectedRange: DateStringRange =
                        boundary === Boundary.START ? [VALID_STR, UNDEFINED_DATE_STR] : [UNDEFINED_DATE_STR, VALID_STR];

                    assertDateRangesEqual(actualRange, expectedRange);
                });
            });

            function runTestForEachScenario(runTestFn: InvalidDateTestFunction) {
                it("in start field", () => runTestFn(getStartInput, Boundary.START, getEndInput));
                it("in end field", () => runTestFn(getEndInput, Boundary.END, getStartInput));
            }
        });

        describe("Typing an overlapping date time", () => {
            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                const result = wrap(
                    <DateRangeInput
                        {...DATETIME_FORMAT}
                        allowSingleDayRange={true}
                        defaultValue={DATE_RANGE_3}
                        overlappingDatesMessage={OVERLAPPING_DATES_MESSAGE}
                        onChange={onChange}
                        onError={onError}
                        timePrecision={TimePrecision.MINUTE}
                    />,
                );
                root = result.root;
            });

            describe("in the end field", () => {
                it("should show an error message when the start time is later than the end time", () => {
                    getStartInput(root).simulate("focus");
                    changeInputText(getStartInput(root), OVERLAPPING_START_DT_STR);
                    getStartInput(root).simulate("blur");
                    assertInputValueEquals(getStartInput(root), OVERLAPPING_START_DT_STR);
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), OVERLAPPING_END_DT_STR);
                    getEndInput(root).simulate("blur");
                    assertInputValueEquals(getEndInput(root), OVERLAPPING_DATES_MESSAGE);
                });
            });
        });

        // this test sub-suite is structured a little differently because of the
        // different semantics of this error case in each field
        // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
        describe.skip("Typing an overlapping date", () => {
            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        defaultValue={DATE_RANGE}
                        overlappingDatesMessage={OVERLAPPING_DATES_MESSAGE}
                        onChange={onChange}
                        onError={onError}
                    />,
                );
                root = result.root;
            });

            describe("in the start field", () => {
                it("should show an error message in the end field right away", () => {
                    getStartInput(root).simulate("focus");
                    changeInputText(getStartInput(root), OVERLAPPING_START_STR);
                    assertInputValueEquals(getEndInput(root), OVERLAPPING_DATES_MESSAGE);
                });

                it("should show the offending date in the end field on focus", () => {
                    getStartInput(root).simulate("focus");
                    changeInputText(getStartInput(root), OVERLAPPING_START_STR);
                    getStartInput(root).simulate("blur");
                    getEndInput(root).simulate("focus");
                    assertInputValueEquals(getEndInput(root), END_STR);
                });

                it("should call onError with [<overlappingDate>, <endDate] on blur", () => {
                    getStartInput(root).simulate("focus");
                    changeInputText(getStartInput(root), OVERLAPPING_START_STR);
                    expect(onError).not.toHaveBeenCalled();
                    getStartInput(root).simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();
                    assertDateRangesEqual(onError.mock.calls[0][0], [OVERLAPPING_START_STR, END_STR]);
                });

                it("should not call onChange before or after blur", () => {
                    getStartInput(root).simulate("focus");
                    changeInputText(getStartInput(root), OVERLAPPING_START_STR);
                    expect(onChange).not.toHaveBeenCalled();
                    getStartInput(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });

                it("should remove error message if input is changed to an in-range date again", () => {
                    getStartInput(root).simulate("focus");
                    changeInputText(getStartInput(root), OVERLAPPING_START_STR);
                    changeInputText(getStartInput(root), START_STR);
                    assertInputValueEquals(getEndInput(root), END_STR);
                });
            });

            describe("in the end field", () => {
                // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
                it.skip("should show an error message in the end field on blur", () => {
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), OVERLAPPING_END_STR);
                    assertInputValueEquals(getEndInput(root), OVERLAPPING_END_STR);
                    getEndInput(root).simulate("blur");
                    assertInputValueEquals(getEndInput(root), OVERLAPPING_DATES_MESSAGE);
                });

                // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
                it.skip("should show the offending date in the end field on re-focus", () => {
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), OVERLAPPING_END_STR);
                    getEndInput(root).simulate("blur");
                    getEndInput(root).simulate("focus");
                    assertInputValueEquals(getEndInput(root), OVERLAPPING_END_STR);
                });

                it("should call onError with [<startDate>, <overlappingDate>] on blur", () => {
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), OVERLAPPING_END_STR);
                    expect(onError).not.toHaveBeenCalled();
                    getEndInput(root).simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();
                    assertDateRangesEqual(onError.mock.calls[0][0], [START_STR, OVERLAPPING_END_STR]);
                });

                it("should not call onChange before or after blur", () => {
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), OVERLAPPING_END_STR);
                    expect(onChange).not.toHaveBeenCalled();
                    getEndInput(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });

                it("should remove error message if input is changed to an in-range date again", () => {
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), OVERLAPPING_END_STR);
                    getEndInput(root).simulate("blur");
                    getEndInput(root).simulate("focus");
                    changeInputText(getEndInput(root), END_STR);
                    getEndInput(root).simulate("blur");
                    assertInputValueEquals(getEndInput(root), END_STR);
                });
            });
        });

        describe("Arrow key navigation", () => {
            it("should have no effect when pressing an arrow key when the input is not fully selected", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={DATE_RANGE} />,
                );

                getStartInput(root).simulate("keydown", { key: "ArrowDown" });
                getEndInput(root).simulate("keydown", { key: "ArrowDown" });
                expect(onChange).not.toHaveBeenCalled();
            });

            it("should move the date back by a day when pressing the left arrow key", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                const expectedStartDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 1));
                const expectedStartDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 2));

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowLeft" });
                assertInputValueEquals(getStartInput(root), expectedStartDate1);
                assertDateRangesEqual(onChange.mock.calls[0][0], [expectedStartDate1, END_STR]);

                getStartInput(root).simulate("keydown", { key: "ArrowLeft" });
                assertInputValueEquals(getStartInput(root), expectedStartDate2);
                assertDateRangesEqual(onChange.mock.calls[1][0], [expectedStartDate2, END_STR]);
            });

            it("should move the date forward by a day when pressing the right arrow key", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                const expectedEndDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY + 1));
                const expectedEndDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY + 2));

                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowRight" });
                assertInputValueEquals(getEndInput(root), expectedEndDate1);
                assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, expectedEndDate1]);

                getEndInput(root).simulate("keydown", { key: "ArrowRight" });
                assertInputValueEquals(getEndInput(root), expectedEndDate2);
                assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, expectedEndDate2]);
            });

            it("should move the date back by a week when pressing the up arrow key", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                const expectedStartDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 7));
                const expectedStartDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 14));

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getStartInput(root), expectedStartDate1);
                assertDateRangesEqual(onChange.mock.calls[0][0], [expectedStartDate1, END_STR]);

                getStartInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getStartInput(root), expectedStartDate2);
                assertDateRangesEqual(onChange.mock.calls[1][0], [expectedStartDate2, END_STR]);
            });

            it("should move the date forward by a week when pressing the down arrow key", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                const expectedEndDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY + 7));
                const expectedEndDate2 = DATE_FORMAT.formatDate(new Date(YEAR, Months.FEBRUARY, 7));

                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getEndInput(root), expectedEndDate1);
                assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, expectedEndDate1]);

                getEndInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getEndInput(root), expectedEndDate2);
                assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, expectedEndDate2]);
            });

            it("should not move past the end boundary", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                const expectedStartDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY - 1));

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getStartInput(root), expectedStartDate);
                assertDateRangesEqual(onChange.mock.calls[0][0], [expectedStartDate, END_STR]);
            });

            it("should not move past the end boundary when allowSingleDayRange={true}", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        allowSingleDayRange={true}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getStartInput(root), END_STR);
                assertDateRangesEqual(onChange.mock.calls[0][0], [END_STR, END_STR]);
            });

            it("should not move past the start boundary", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                const expectedEndDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY + 1));

                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getEndInput(root), expectedEndDate);
                assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, expectedEndDate]);
            });

            it("should not move past the start boundary when allowSingleDayRange={true}", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        allowSingleDayRange={true}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        selectAllOnFocus={true}
                    />,
                );

                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getEndInput(root), START_STR);
                assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, START_STR]);
            });

            it("should not move past the min date", () => {
                const minDate = new Date(YEAR, Months.JANUARY, START_DAY - 3);
                const minDateStr = DATE_FORMAT.formatDate(minDate);

                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        minDate={minDate}
                        selectAllOnFocus={true}
                    />,
                );

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getStartInput(root), minDateStr);
                assertDateRangesEqual(onChange.mock.calls[0][0], [minDateStr, END_STR]);
            });

            it("should not move past the max date", () => {
                const maxDate = new Date(YEAR, Months.JANUARY, END_DAY + 3);
                const maxDateStr = DATE_FORMAT.formatDate(maxDate);

                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        onChange={onChange}
                        defaultValue={DATE_RANGE}
                        maxDate={maxDate}
                        selectAllOnFocus={true}
                    />,
                );

                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getEndInput(root), maxDateStr);
                assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR, maxDateStr]);
            });

            it("should select today's date by default", () => {
                const onChange = vi.fn();
                const { root } = wrap(<DateRangeInput {...DATE_FORMAT} onChange={onChange} />);

                const today = DATE_FORMAT.formatDate(new Date());
                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getStartInput(root), today);
            });

            it("should choose a reasonable end date when only the start is selected", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={[START_DATE, null]} />,
                );

                const expectedEndDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY + 1));
                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowRight" });
                assertInputValueEquals(getEndInput(root), expectedEndDate);
            });

            it("should choose a reasonable start date when only the end is selected", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={[null, END_DATE]} />,
                );

                const expectedEndDate = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, END_DAY - 7));
                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getStartInput(root), expectedEndDate);
            });

            it("should not make a selection when trying to move backward and only the start is selected", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={[START_DATE, null]} />,
                );

                getEndInput(root).simulate("focus");
                getEndInput(root).simulate("keydown", { key: "ArrowLeft" });
                getEndInput(root).simulate("keydown", { key: "ArrowUp" });
                assertInputValueEquals(getEndInput(root), "");
                expect(onChange).not.toHaveBeenCalled();
            });

            it("should not make a selection when trying to move forward and only the end is selected", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={[null, END_DATE]} />,
                );

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowRight" });
                getStartInput(root).simulate("keydown", { key: "ArrowDown" });
                assertInputValueEquals(getStartInput(root), "");
                expect(onChange).not.toHaveBeenCalled();
            });
        });

        // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
        describe.skip("Hovering over dates", () => {
            // define new constants to clarify chronological ordering of dates
            // TODO: rename all date constants in this file to use a similar
            // scheme, then get rid of these extra constants

            const HOVER_TEST_DAY_1 = 5;
            const HOVER_TEST_DAY_2 = 10;
            const HOVER_TEST_DAY_3 = 15;
            const HOVER_TEST_DAY_4 = 20;
            const HOVER_TEST_DAY_5 = 25;

            const HOVER_TEST_DATE_1 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_1);
            const HOVER_TEST_DATE_2 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_2);
            const HOVER_TEST_DATE_3 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_3);
            const HOVER_TEST_DATE_4 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_4);
            const HOVER_TEST_DATE_5 = new Date(2022, Months.JANUARY, HOVER_TEST_DAY_5);

            const HOVER_TEST_STR_1 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_1);
            const HOVER_TEST_STR_2 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_2);
            const HOVER_TEST_STR_3 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_3);
            const HOVER_TEST_STR_4 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_4);
            const HOVER_TEST_STR_5 = DATE_FORMAT.formatDate(HOVER_TEST_DATE_5);

            const HOVER_TEST_DATE_CONFIG_1 = {
                date: HOVER_TEST_DATE_1,
                day: HOVER_TEST_DAY_1,
                str: HOVER_TEST_STR_1,
            };
            const HOVER_TEST_DATE_CONFIG_2 = {
                date: HOVER_TEST_DATE_2,
                day: HOVER_TEST_DAY_2,
                str: HOVER_TEST_STR_2,
            };
            const HOVER_TEST_DATE_CONFIG_3 = {
                date: HOVER_TEST_DATE_3,
                day: HOVER_TEST_DAY_3,
                str: HOVER_TEST_STR_3,
            };
            const HOVER_TEST_DATE_CONFIG_4 = {
                date: HOVER_TEST_DATE_4,
                day: HOVER_TEST_DAY_4,
                str: HOVER_TEST_STR_4,
            };
            const HOVER_TEST_DATE_CONFIG_5 = {
                date: HOVER_TEST_DATE_5,
                day: HOVER_TEST_DAY_5,
                str: HOVER_TEST_STR_5,
            };

            interface HoverTextDateConfig {
                day: number;
                date: Date;
                str: string;
            }

            let root: WrappedComponentRoot;
            let getDayElement: (dayNumber?: number, fromLeftMonth?: boolean) => WrappedComponentDayElement;

            beforeAll(() => {
                // reuse the same mounted component for every test to speed
                // things up (mounting is costly).
                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        closeOnSelection={false}
                        defaultValue={[HOVER_TEST_DATE_2, HOVER_TEST_DATE_4]}
                    />,
                );

                root = result.root;
                getDayElement = result.getDayElement;
            });

            beforeEach(() => {
                // need to set wasLastFocusChangeDueToHover=false to fully reset state between tests.
                act(() => {
                    root.setState({ isOpen: true, wasLastFocusChangeDueToHover: false });
                });
                // clear the inputs to start from a fresh state, but do so
                // *after* opening the popover so that the calendar doesn't
                // move away from the view we expect for these tests.
                changeInputText(getStartInput(root), "");
                changeInputText(getEndInput(root), "");
            });

            function setSelectedRangeForHoverTest(selectedDateConfigs: NullableRange<HoverTextDateConfig>) {
                const [startConfig, endConfig] = selectedDateConfigs;
                changeInputText(getStartInput(root), startConfig == null ? "" : startConfig.str);
                changeInputText(getEndInput(root), endConfig == null ? "" : endConfig.str);
            }

            describe("when selected date range is [null, null]", () => {
                const SELECTED_RANGE: NullableRange<HoverTextDateConfig> = [null, null];
                const HOVER_TEST_DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1;

                beforeEach(() => {
                    setSelectedRangeForHoverTest(SELECTED_RANGE);
                });

                describe("if start field is focused", () => {
                    beforeEach(() => {
                        getStartInput(root).simulate("focus");
                        getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseenter");
                    });

                    it("should show [<hoveredDate>, null] in input fields", () => {
                        assertInputValuesEqual(root, HOVER_TEST_DATE_CONFIG.str, "");
                    });

                    it("should keep focus on start field", () => {
                        assertStartInputFocused(root);
                    });

                    describe("on click", () => {
                        beforeEach(() => {
                            getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("click");
                        });

                        it("should set selection to [<hoveredDate>, null]", () => {
                            assertInputValuesEqual(root, HOVER_TEST_DATE_CONFIG.str, "");
                        });

                        it("should move focus to end field", () => {
                            assertEndInputFocused(root);
                        });
                    });

                    describe("if mouse moves to no longer be over a calendar day", () => {
                        beforeEach(() => {
                            getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseleave");
                        });

                        it("should show [null, null] in input fields", () => {
                            assertInputValuesEqual(root, "", "");
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });
                    });
                });

                describe("if end field is focused", () => {
                    beforeEach(() => {
                        getEndInput(root).simulate("focus");
                        getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseenter");
                    });

                    it("should show [null, <hoveredDate>] in input fields", () => {
                        assertInputValuesEqual(root, "", HOVER_TEST_DATE_CONFIG.str);
                    });

                    it("should keep focus on end field", () => {
                        assertEndInputFocused(root);
                    });

                    describe("on click", () => {
                        beforeEach(() => {
                            getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("click");
                        });

                        it("should set selection to [null, <hoveredDate>]", () => {
                            assertInputValuesEqual(root, "", HOVER_TEST_DATE_CONFIG.str);
                        });

                        it("should move focus to start field", () => {
                            assertStartInputFocused(root);
                        });
                    });

                    describe("if mouse moves to no longer be over a calendar day", () => {
                        beforeEach(() => {
                            getDayElement(HOVER_TEST_DATE_CONFIG.day).simulate("mouseleave");
                        });

                        it("should show [null, null] in input fields", () => {
                            assertInputValuesEqual(root, "", "");
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });
                    });
                });
            });

            describe("when selected date range is [<startDate>, null]", () => {
                const SELECTED_RANGE: NullableRange<HoverTextDateConfig> = [HOVER_TEST_DATE_CONFIG_2, null];

                beforeEach(() => {
                    setSelectedRangeForHoverTest(SELECTED_RANGE);
                });

                describe("if start field is focused", () => {
                    beforeEach(() => {
                        getStartInput(root).simulate("focus");
                    });

                    describe("if <startDate> < <hoveredDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, null] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, "");
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, null]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, "");
                            });

                            it("should move focus to end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, null] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> < <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, null] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, "");
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, null]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, "");
                            });

                            it("should move focus to end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, null] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, null] in input fields", () => {
                            assertInputValuesEqual(root, "", "");
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, null]", () => {
                                assertInputValuesEqual(root, "", "");
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, null] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });
                });

                describe("if end field is focused", () => {
                    beforeEach(() => {
                        getEndInput(root).simulate("focus");
                    });

                    describe("if <startDate> < <hoveredDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<startDate>, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str);
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<startDate>, <hoveredDate>]", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, null] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> < <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, <startDate>] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[0]?.str);
                        });

                        it("should move focus to start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, <startDate>]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[0]?.str);
                            });

                            it("should leave focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, null] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, null] in input fields", () => {
                            assertInputValuesEqual(root, "", "");
                        });

                        it("should move focus to start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, null] on click", () => {
                                assertInputValuesEqual(root, "", "");
                            });

                            it("should leave focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, null] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });
                });
            });

            describe("when selected date range is [null, <endDate>]", () => {
                const SELECTED_RANGE: NullableRange<HoverTextDateConfig> = [null, HOVER_TEST_DATE_CONFIG_4];

                beforeEach(() => {
                    setSelectedRangeForHoverTest(SELECTED_RANGE);
                });

                describe("if start field is focused", () => {
                    beforeEach(() => {
                        getStartInput(root).simulate("focus");
                    });

                    describe("if <hoveredDate> < <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, <endDate>] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str);
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, <endDate>]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [null, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <endDate> < <hoveredDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<endDate>, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, SELECTED_RANGE[1]?.str, DATE_CONFIG.str);
                        });

                        it("should move focus to end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<endDate>, <hoveredDate>] on click", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[1]?.str, DATE_CONFIG.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [null, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should move focus back to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, null] in input fields", () => {
                            assertInputValuesEqual(root, "", "");
                        });

                        it("should move focus to end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, null] on click", () => {
                                assertInputValuesEqual(root, "", "");
                            });

                            it("should move focus to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [null, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });
                });

                describe("if end field is focused", () => {
                    beforeEach(() => {
                        getEndInput(root).simulate("focus");
                    });

                    describe("if <hoveredDate> < <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, "", DATE_CONFIG.str);
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, <hoveredDate>]", () => {
                                assertInputValuesEqual(root, "", DATE_CONFIG.str);
                            });

                            it("should move focus to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [null, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <endDate> < <hoveredDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, "", DATE_CONFIG.str);
                        });

                        it("should keep focus on start field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("sets selection to [null, <hoveredDate>] on click", () => {
                                assertInputValuesEqual(root, "", DATE_CONFIG.str);
                            });

                            it("should move focus to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [null, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, null] in input fields", () => {
                            assertInputValuesEqual(root, "", "");
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, null] on click", () => {
                                assertInputValuesEqual(root, "", "");
                            });

                            it("should move focus to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [null, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });
                });
            });

            describe("when selected date range is [<startDate>, <endDate>]", () => {
                const SELECTED_RANGE: NullableRange<HoverTextDateConfig> = [
                    HOVER_TEST_DATE_CONFIG_2,
                    HOVER_TEST_DATE_CONFIG_4,
                ];

                beforeEach(() => {
                    setSelectedRangeForHoverTest(SELECTED_RANGE);
                });

                describe("if start field is focused", () => {
                    beforeEach(() => {
                        getStartInput(root).simulate("focus");
                    });

                    describe("if <hoveredDate> < <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, <endDate>] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str);
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, <endDate>]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <startDate> < <hoveredDate> < <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, <endDate>] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str);
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, <endDate>]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <endDate> < <hoveredDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<hoveredDate>, null] in input fields", () => {
                            assertInputValuesEqual(root, DATE_CONFIG.str, "");
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<hoveredDate>, null]", () => {
                                assertInputValuesEqual(root, DATE_CONFIG.str, "");
                            });

                            it("should move focus to end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, <endDate>] in input fields", () => {
                            assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                        });

                        it("should keep focus on start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, <endDate>]", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<startDate>, null] in input fields", () => {
                            assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                        });

                        it("should move focus to end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<startDate>, null]", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should move focus back to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });
                    });
                });

                describe("if end field is focused", () => {
                    beforeEach(() => {
                        getEndInput(root).simulate("focus");
                    });

                    describe("if <hoveredDate> < <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_1;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, "", DATE_CONFIG.str);
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, <hoveredDate>]", () => {
                                assertInputValuesEqual(root, "", DATE_CONFIG.str);
                            });

                            it("should move focus to start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <startDate> < <hoveredDate> < <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_3;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<startDate>, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str);
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<startDate>, <hoveredDate>]", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <endDate> < <hoveredDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_5;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<startDate>, <hoveredDate>] in input fields", () => {
                            assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str);
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<startDate>, <hoveredDate>]", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, DATE_CONFIG.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <startDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_2;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [null, <endDate>] in input fields", () => {
                            assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                        });

                        it("should move focus to start field", () => {
                            assertStartInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [null, <endDate>]", () => {
                                assertInputValuesEqual(root, "", SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on start field", () => {
                                assertStartInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should move focus back to end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });

                    describe("if <hoveredDate> == <endDate>", () => {
                        const DATE_CONFIG = HOVER_TEST_DATE_CONFIG_4;

                        beforeEach(() => {
                            getDayElement(DATE_CONFIG.day).simulate("mouseenter");
                        });

                        it("should show [<startDate>, null] in input fields", () => {
                            assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                        });

                        it("should keep focus on end field", () => {
                            assertEndInputFocused(root);
                        });

                        describe("on click", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("click");
                            });

                            it("should set selection to [<startDate>, null]", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, "");
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });

                        describe("if mouse moves to no longer be over a calendar day", () => {
                            beforeEach(() => {
                                getDayElement(DATE_CONFIG.day).simulate("mouseleave");
                            });

                            it("should show [<startDate>, <endDate>] in input fields", () => {
                                assertInputValuesEqual(root, SELECTED_RANGE[0]?.str, SELECTED_RANGE[1]?.str);
                            });

                            it("should keep focus on end field", () => {
                                assertEndInputFocused(root);
                            });
                        });
                    });
                });
            });
        });

        it("should invoke onChange with [null, null] and clear the inputs when clearing the date range in the picker", () => {
            const onChange = vi.fn();
            const defaultValue = [START_DATE, null] as DateRange;

            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} defaultValue={defaultValue} onChange={onChange} />,
            );

            getStartInput(root).simulate("focus");
            getDayElement(START_DAY).simulate("click");
            assertInputValuesEqual(root, "", "");
            expect(onChange).toHaveBeenCalled();
            expect(onChange).toHaveBeenCalledWith([null, null]);
        });

        it("should invoke onChange with [null, <endDate>] when clearing only the start input", () => {
            const onChange = vi.fn();
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={DATE_RANGE} />);

            const startInput = getStartInput(root);
            startInput.simulate("focus");
            changeInputText(startInput, "");
            expect(onChange).toHaveBeenCalled();
            assertDateRangesEqual(onChange.mock.calls[0][0], [null, END_STR]);
            assertInputValuesEqual(root, "", END_STR);
        });

        it("should invoke onChange with [null, null] and leave inputs empty when clearing the dates in both inputs", () => {
            const onChange = vi.fn();
            const { root } = wrap(
                <DateRangeInput {...DATE_FORMAT} onChange={onChange} defaultValue={[START_DATE, null]} />,
            );
            getStartInput(root).simulate("focus");
            changeStartInputText(root, "");
            expect(onChange).toHaveBeenCalled();
            assertDateRangesEqual(onChange.mock.calls[0][0], [null, null]);
            assertInputValuesEqual(root, "", "");
        });
    });

    describe("when controlled", () => {
        it("should ignore defaultValue when value is set", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} defaultValue={DATE_RANGE_2} value={DATE_RANGE} />);
            assertInputValuesEqual(root, START_STR, END_STR);
        });

        it("should show empty fields when value is [undefined, undefined]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={[null, null]} />);
            assertInputValuesEqual(root, "", "");
        });

        it("should show empty fields when value is [null, null]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={[null, null]} />);
            assertInputValuesEqual(root, "", "");
        });

        it("should show empty start field and formatted date in end field when value is [null, <date>]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={[null, END_DATE]} />);
            assertInputValuesEqual(root, "", END_STR);
        });

        it("should show empty end field and formatted date in start field when value is [<date>, null]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={[START_DATE, null]} />);
            assertInputValuesEqual(root, START_STR, "");
        });

        it("should show formatted dates in both fields when value is [<date1>, <date2>]", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={[START_DATE, END_DATE]} />);
            assertInputValuesEqual(root, START_STR, END_STR);
        });

        it("should change the text accordingly in both fields when updating value", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={DATE_RANGE} />);
            act(() => {
                root.setState({ isOpen: true });
            });
            root.update();
            root.setProps({ value: DATE_RANGE_2 }).update();
            assertInputValuesEqual(root, START_STR_2, END_STR_2);
        });

        // HACKHACK: https://github.com/palantir/blueprint/issues/6109
        // N.B. this test passes locally
        it.skip("Pressing Enter saves the inputted date and closes the popover", () => {
            const onChange = vi.fn();
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} onChange={onChange} value={[null, null]} />);
            act(() => {
                root.setState({ isOpen: true });
            });

            const startInput = getStartInput(root);
            startInput.simulate("focus");
            startInput.simulate("change", { target: { value: START_STR } });
            startInput.simulate("keydown", { key: "Enter" });
            expect(isStartInputFocused(root)).toBe(false);

            expect(root.state("isOpen")).toBe(true);

            const endInput = getEndInput(root);
            expect(isEndInputFocused(root)).toBe(true);
            endInput.simulate("change", { target: { value: END_STR } });
            endInput.simulate("keydown", { key: "Enter" });

            expect(isStartInputFocused(root)).toBe(false);
            expect(isEndInputFocused(root)).toBe(true);

            // onChange is called once on change, once on Enter
            expect(onChange.mock.calls.length).toBe(4);
            // check one of the invocations
            assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, null]);
        });

        it("should close the popover when pressing Escape", () => {
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} value={[null, null]} />);
            act(() => {
                root.setState({ isOpen: true });
            });

            const startInput = getStartInput(root);
            startInput.simulate("focus");

            expect(root.state("isOpen")).toBe(true);

            startInput.simulate("keydown", { key: "Escape" });

            expect(root.state("isOpen")).toBe(false);
            expect(isStartInputFocused(root)).toBe(false);
        });

        it("should invoke onChange with the new date range and update the input field text when clicking a date", () => {
            const onChange = vi.fn();
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} value={DATE_RANGE} onChange={onChange} />,
            );
            getStartInput(root).simulate("focus"); // to open popover
            getDayElement(START_DAY).simulate("click");
            assertDateRangesEqual(onChange.mock.calls[0][0], [null, END_STR]);
            assertInputValuesEqual(root, "", END_STR);
            expect(onChange.mock.calls.length).toBe(1);
        });

        it("should invoke onChange with the new date range but not change UI when typing a valid start or end date", () => {
            const onChange = vi.fn();
            const { root } = wrap(<DateRangeInput {...DATE_FORMAT} onChange={onChange} value={DATE_RANGE} />);

            changeStartInputText(root, START_STR_2);
            expect(onChange.mock.calls.length).toBe(1);
            assertDateRangesEqual(onChange.mock.calls[0][0], [START_STR_2, END_STR]);
            assertInputValuesEqual(root, START_STR, END_STR);

            // since the component is controlled, value changes don't persist across onChanges
            changeEndInputText(root, END_STR_2);
            expect(onChange.mock.calls.length).toBe(2);
            assertDateRangesEqual(onChange.mock.calls[1][0], [START_STR, END_STR_2]);
            assertInputValuesEqual(root, START_STR, END_STR);
        });

        it("should move focus to end field when clicking a start date", () => {
            // eslint-disable-next-line prefer-const
            let controlledRoot: WrappedComponentRoot;

            const onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue });
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} onChange={onChange} value={[null, null]} />,
            );
            controlledRoot = root;

            getStartInput(controlledRoot).simulate("focus");
            getDayElement(1).simulate("click"); // triggers a controlled value change
            assertEndInputFocused(controlledRoot);
        });

        it("should show the typed date, not the hovered date, when typing in a field while hovering over a date", () => {
            // eslint-disable-next-line prefer-const
            let controlledRoot: WrappedComponentRoot;

            const onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue });
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} onChange={onChange} value={[null, null]} />,
            );
            controlledRoot = root;

            getStartInput(root).simulate("focus");
            getDayElement(1).simulate("mouseenter");
            changeStartInputText(root, START_STR_2);
            assertInputValuesEqual(root, START_STR_2, "");
        });

        // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
        describe.skip("Typing an out-of-range date", () => {
            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        minDate={OUT_OF_RANGE_TEST_MIN}
                        maxDate={OUT_OF_RANGE_TEST_MAX}
                        onChange={onChange}
                        onError={onError}
                        outOfRangeMessage={OUT_OF_RANGE_MESSAGE}
                        value={[null, null]}
                    />,
                );
                root = result.root;
            });

            describe("calls onError with invalid date on blur", () => {
                runTestForEachScenario((inputGetterFn, inputString, boundary) => {
                    const expectedRange: DateStringRange =
                        boundary === Boundary.START ? [inputString, null] : [null, inputString];
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), inputString);
                    expect(onError).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();
                    assertDateRangesEqual(onError.mock.calls[0][0], expectedRange);
                });
            });

            describe("does NOT call onChange before OR after blur", () => {
                runTestForEachScenario((inputGetterFn, inputString) => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), inputString);
                    expect(onChange).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });
            });

            function runTestForEachScenario(runTestFn: OutOfRangeTestFunction) {
                const { START, END } = Boundary;
                it("if start < minDate", () => runTestFn(getStartInput, OUT_OF_RANGE_START_STR, START));
                it("if start > maxDate", () => runTestFn(getStartInput, OUT_OF_RANGE_END_STR, START));
                it("if end < minDate", () => runTestFn(getEndInput, OUT_OF_RANGE_START_STR, END));
                it("if end > maxDate", () => runTestFn(getEndInput, OUT_OF_RANGE_END_STR, END));
            }
        });

        describe("Typing an invalid date", () => {
            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        invalidDateMessage={INVALID_MESSAGE}
                        onError={onError}
                        value={DATE_RANGE}
                    />,
                );
                root = result.root;

                changeStartInputText(root, "");
                changeEndInputText(root, "");
                root.setProps({ onChange });
            });

            // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
            describe.skip("calls onError on blur with Date(undefined) in place of the invalid date", () => {
                runTestForEachScenario((inputGetterFn, boundary) => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    expect(onError).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();

                    const dateRange = onError.mock.calls[0][0];
                    const dateIndex = boundary === Boundary.START ? 0 : 1;
                    expect((dateRange[dateIndex] as Date).valueOf()).toBeNaN();
                });
            });

            describe("does NOT call onChange before OR after blur", () => {
                runTestForEachScenario(inputGetterFn => {
                    inputGetterFn(root).simulate("focus");
                    changeInputText(inputGetterFn(root), INVALID_STR);
                    expect(onChange).not.toHaveBeenCalled();
                    inputGetterFn(root).simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });
            });

            function runTestForEachScenario(runTestFn: InvalidDateTestFunction) {
                it("in start field", () => runTestFn(getStartInput, Boundary.START, getEndInput));
                it("in end field", () => runTestFn(getEndInput, Boundary.END, getStartInput));
            }
        });

        // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
        describe.skip("Typing an overlapping date", () => {
            let onChange: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let onError: ReturnType<typeof vi.fn<(range: DateRange) => void>>;
            let root: WrappedComponentRoot;
            let startInput: WrappedComponentInput;
            let endInput: WrappedComponentInput;

            beforeEach(() => {
                onChange = vi.fn();
                onError = vi.fn();

                const result = wrap(
                    <DateRangeInput
                        {...DATE_FORMAT}
                        overlappingDatesMessage={OVERLAPPING_DATES_MESSAGE}
                        onChange={onChange}
                        onError={onError}
                        value={DATE_RANGE}
                    />,
                );
                root = result.root;

                startInput = getStartInput(root);
                endInput = getEndInput(root);
            });

            describe("in the start field", () => {
                it("should call onError with [<overlappingDate>, <endDate] on blur", () => {
                    startInput.simulate("focus");
                    changeInputText(startInput, OVERLAPPING_START_STR);
                    expect(onError).not.toHaveBeenCalled();
                    startInput.simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();
                    assertDateRangesEqual(onError.mock.calls[0][0], [OVERLAPPING_START_STR, END_STR]);
                });

                it("should not call onChange before or after blur", () => {
                    startInput.simulate("focus");
                    changeInputText(startInput, OVERLAPPING_START_STR);
                    expect(onChange).not.toHaveBeenCalled();
                    startInput.simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });
            });

            describe("in the end field", () => {
                it("should call onError with [<startDate>, <overlappingDate>] on blur", () => {
                    endInput.simulate("focus");
                    changeInputText(endInput, OVERLAPPING_END_STR);
                    expect(onError).not.toHaveBeenCalled();
                    endInput.simulate("blur");
                    expect(onError).toHaveBeenCalledOnce();
                    assertDateRangesEqual(onError.mock.calls[0][0], [START_STR, OVERLAPPING_END_STR]);
                });

                it("should not call onChange before or after blur", () => {
                    endInput.simulate("focus");
                    changeInputText(endInput, OVERLAPPING_END_STR);
                    expect(onChange).not.toHaveBeenCalled();
                    endInput.simulate("blur");
                    expect(onChange).not.toHaveBeenCalled();
                });
            });
        });

        describe("Arrow key navigation", () => {
            it("should move the date back by a day when pressing the left arrow key", () => {
                const onChange = vi.fn();
                const { root } = wrap(
                    <DateRangeInput {...DATE_FORMAT} onChange={onChange} value={DATE_RANGE} selectAllOnFocus={true} />,
                );

                const expectedStartDate1 = DATE_FORMAT.formatDate(new Date(YEAR, Months.JANUARY, START_DAY - 1));

                getStartInput(root).simulate("focus");
                getStartInput(root).simulate("keydown", { key: "ArrowLeft" });
                assertInputValueEquals(getStartInput(root), expectedStartDate1);
                assertDateRangesEqual(onChange.mock.calls[0][0], [expectedStartDate1, END_STR]);
            });
        });

        it("should invoke onChange with [null, null] and update input fields when clearing the dates in the picker", () => {
            const onChange = vi.fn();
            const value = [START_DATE, null] as DateRange;

            const { root, getDayElement } = wrap(<DateRangeInput {...DATE_FORMAT} value={value} onChange={onChange} />);

            // popover opens on focus
            getStartInput(root).simulate("focus");
            getDayElement(START_DAY).simulate("click");

            assertDateRangesEqual(onChange.mock.calls[0][0], [null, null]);
            assertInputValuesEqual(root, "", "");
        });

        it(`should invoke onChange with [null, <endDate>], not clear the selected dates, and repopulate the controlled values in the inputs on blur when clearing only the start input`, () => {
            const onChange = vi.fn();
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} onChange={onChange} value={DATE_RANGE} />,
            );

            const startInput = getStartInput(root);

            startInput.simulate("focus");
            changeInputText(startInput, "");
            expect(onChange).toHaveBeenCalledOnce();
            assertDateRangesEqual(onChange.mock.calls[0][0], [null, END_STR]);
            assertInputValuesEqual(root, "", END_STR);

            // start day should still be selected in the calendar, ignoring user's typing
            expect(getDayElement(START_DAY).hasClass(Classes.DATEPICKER3_DAY_SELECTED)).toBe(true);

            // blurring should put the controlled start date back in the start input, overriding user's typing
            startInput.simulate("blur");
            assertInputValuesEqual(root, START_STR, END_STR);
        });

        it(`should invoke onChange with [null, null], not clear the selected dates, and repopulate the controlled values in the inputs on blur when clearing the inputs`, () => {
            const onChange = vi.fn();
            const { root, getDayElement } = wrap(
                <DateRangeInput {...DATE_FORMAT} onChange={onChange} value={[START_DATE, null]} />,
            );

            const startInput = getStartInput(root);

            startInput.simulate("focus");
            changeInputText(startInput, "");
            expect(onChange).toHaveBeenCalledOnce();
            assertDateRangesEqual(onChange.mock.calls[0][0], [null, null]);
            assertInputValuesEqual(root, "", "");

            expect(getDayElement(START_DAY).hasClass(Classes.DATEPICKER3_DAY_SELECTED)).toBe(true);

            startInput.simulate("blur");
            assertInputValuesEqual(root, START_STR, "");
        });

        // Regression test for https://github.com/palantir/blueprint/issues/5791
        it("should show the new date in input, not a previously selected date, when hovering and clicking on end date", () => {
            const DEC_1_DATE = new Date(2022, 11, 1);
            const DEC_1_STR = DATE_FORMAT.formatDate(DEC_1_DATE);
            const DEC_2_DATE = new Date(2022, 11, 2);
            const DEC_2_STR = DATE_FORMAT.formatDate(DEC_2_DATE);
            const DEC_6_DATE = new Date(2022, 11, 6);
            const DEC_6_STR = DATE_FORMAT.formatDate(DEC_6_DATE);
            const DEC_8_DATE = new Date(2022, 11, 8);
            const DEC_8_STR = DATE_FORMAT.formatDate(DEC_8_DATE);

            // eslint-disable-next-line prefer-const
            let controlledRoot: WrappedComponentRoot;

            const onChange = (nextValue: DateRange) => controlledRoot.setProps({ value: nextValue });
            const { root, getDayElement } = wrap(
                <DateRangeInput
                    {...DATE_FORMAT}
                    closeOnSelection={false}
                    popoverProps={{ isOpen: true }}
                    onChange={onChange}
                    value={[DEC_6_DATE, DEC_8_DATE]}
                />,
                true,
            );
            controlledRoot = root;

            // initial state
            getStartInput(root).simulate("focus");
            assertInputValuesEqual(root, DEC_6_STR, DEC_8_STR);

            // hover over Dec 1
            getDayElement(1).simulate("mouseenter");
            assertInputValuesEqual(root, DEC_1_STR, DEC_8_STR);

            // click to select Dec 1
            getDayElement(1).simulate("click");
            getDayElement(1).simulate("mouseleave");
            assertInputValuesEqual(root, DEC_1_STR, DEC_8_STR);

            // re-focus on start input to ensure the component doesn't think we're changing the end boundary
            // (this mimics real UX, where the component-refocuses the start input after selecting a start date)
            getStartInput(root).simulate("focus");

            // hover over Dec 2
            getDayElement(2).simulate("mouseenter");
            assertInputValuesEqual(root, DEC_2_STR, DEC_8_STR);

            // click to select Dec 2
            getDayElement(2).simulate("click");
            getDayElement(2).simulate("mouseleave");
            assertInputValuesEqual(root, DEC_2_STR, DEC_8_STR);
        });

        describe("localization", () => {
            describe("with formatDate & parseDate undefined", () => {
                it("should format date strings with provided Locale object", () => {
                    const { root } = wrap(
                        <DateRangeInput dateFnsFormat="PPP" locale={esLocale} value={DATE_RANGE_2} />,
                        true,
                    );
                    assertInputValuesEqual(root, START_STR_2_ES_LOCALE, END_STR_2_ES_LOCALE);
                });

                // HACKHACK: skipped test resulting from React 18 upgrade. See: https://github.com/palantir/blueprint/issues/7168
                it.skip("formats date strings with async-loaded locale corresponding to provided locale code", () =>
                    new Promise<void>(resolve => {
                        const { root } = wrap(
                            <DateRangeInput dateFnsFormat="PPP" locale="es" value={DATE_RANGE_2} />,
                            true,
                        );
                        // give the component one animation frame to load the locale upon mount
                        setTimeout(() => {
                            root.update();
                            assertInputValuesEqual(root, START_STR_2_ES_LOCALE, END_STR_2_ES_LOCALE);
                            resolve();
                        });
                    }));
            });
        });
    });

    function getStartInput(root: WrappedComponentRoot): WrappedComponentInput {
        return root.find(InputGroup).first().find("input") as WrappedComponentInput;
    }

    function getEndInput(root: WrappedComponentRoot): WrappedComponentInput {
        return root.find(InputGroup).last().find("input") as WrappedComponentInput;
    }

    function getInputPlaceholderText(input: WrappedComponentInput) {
        return input.prop("placeholder");
    }

    function isStartInputFocused(root: WrappedComponentRoot) {
        // TODO: find a more elegant way to do this; reaching into component state is gross.
        return root.state("isStartInputFocused");
    }

    function isEndInputFocused(root: WrappedComponentRoot) {
        // TODO: find a more elegant way to do this; reaching into component state is gross.
        return root.state("isEndInputFocused");
    }

    function changeStartInputText(root: WrappedComponentRoot, value: string) {
        changeInputText(getStartInput(root), value);
    }

    function changeEndInputText(root: WrappedComponentRoot, value: string) {
        changeInputText(getEndInput(root), value);
    }

    function changeInputText(input: WrappedComponentInput, value: string) {
        input.simulate("change", { target: { value } });
    }

    function assertStartInputFocused(root: WrappedComponentRoot) {
        expect(isStartInputFocused(root)).toBe(true);
    }

    function assertEndInputFocused(root: WrappedComponentRoot) {
        expect(isEndInputFocused(root)).toBe(true);
    }

    function assertInputValuesEqual(
        root: WrappedComponentRoot,
        startInputValue: string | undefined,
        endInputValue: string | undefined,
    ) {
        assertInputValueEquals(getStartInput(root), startInputValue);
        assertInputValueEquals(getEndInput(root), endInputValue);
    }

    function assertInputValueEquals(input: WrappedComponentInput, inputValue: string | undefined) {
        expect(input.closest(InputGroup).prop("value")).toBe(inputValue);
    }

    function assertDateRangesEqual(actual: DateRange, expected: DateStringRange) {
        const [expectedStart, expectedEnd] = expected;
        const [actualStart, actualEnd] = actual.map((date: Date | null) => {
            if (date == null) {
                return null;
            } else if (isNaN(date.valueOf())) {
                return UNDEFINED_DATE_STR;
            } else {
                return DATE_FORMAT.formatDate(date);
            }
        });
        expect(actualStart).toBe(expectedStart);
        expect(actualEnd).toBe(expectedEnd);
    }

    function wrap(dateRangeInput: React.JSX.Element, attachToDOM = false) {
        const mountOptions = attachToDOM ? { attachTo: containerElement } : undefined;
        const wrapper = mount(dateRangeInput, mountOptions);
        mountedWrappers.push(wrapper);
        return {
            getDayElement: (dayNumber = 1, fromLeftMonth = true) => {
                const monthElement = wrapper.find(`.${ReactDayPickerClasses.RDP_MONTH}`).at(fromLeftMonth ? 0 : 1);
                const dayElements = monthElement.find(`.${Classes.DATEPICKER3_DAY}`);
                return dayElements
                    .filterWhere(d => d.text() === dayNumber.toString() && !d.hasClass(Classes.DATEPICKER3_DAY_OUTSIDE))
                    .hostNodes();
            },
            root: wrapper,
        };
    }
});

function getDateFnsFormatter(formatStr: string): DateFormatProps {
    return {
        formatDate: (date, localeCode) => format(date, formatStr, maybeGetDateFnsLocaleOptions(localeCode)),
        parseDate: (str, localeCode) => parse(str, formatStr, new Date(), maybeGetDateFnsLocaleOptions(localeCode)),
        placeholder: `${formatStr}`,
    };
}

const AllLocales: Record<string, Locale> = Locales;

function maybeGetDateFnsLocaleOptions(localeCode: string | undefined): { locale: Locale } | undefined {
    if (localeCode !== undefined && AllLocales[localeCode] !== undefined) {
        return { locale: AllLocales[localeCode] };
    }
    return undefined;
}
