def construct_X_block(
        series: TimeSeries, feature_times: pd.Index, lags: Optional[Sequence[int]]
    ) -> np.array:
        """
        Helper function that creates the lagged features 'block' of a specific
        `series` (i.e. either `target_series`, `past_covariates`, or `future_covariates`);
        the feature matrix `X` is formed by concatenating the blocks of all of the specified
        series along the components axis. If `lags` is `None`, then `None` will be returned in
        lieu of an array. Please refer to the `create_lagged_features` docstring for further
        details about the structure of the `X` feature matrix.

        The returned `X_block` is constructed by looping over each time in `feature_times`,
        finding the index position of that time in the series, and then for each lag value in
        `lags`, offset this index position by a particular lag value; this offset index is then
        used to extract all of the components at a single lagged time.

        Unlike the implementation found in `darts.utils.data.tabularization`, this function doesn't
        use any 'vectorisation' tricks, which makes it slower to run, but more easily interpretable.

        Some of the times in `feature_times` may occur before the start *or* after the end of `series`;
        see the docstrings of `get_feature_times_past` and `get_feature_times_future` for why this is the
        case. Because of this, we need to prepend or append these 'extended times' to `series.time_index`
        before searching for the index of each time in the series. Even though the integer indices of the
        'extended times' won't be contained within the original `series`, offsetting these found indices
        by the requested lag value should 'bring us back' to a time within the original, unextended `series`.
        However, if we've prepended times to `series.time_index`, we have to note that all of the indices will
        be 'bumped up' by the number of values we've prepended, even after offsetting by a lag value. For example,
        if we extended `series.time_index` by prepending two values to the start, the integer index of the first
        actual value in `series` will occur at an index of `2` instead of `0`. To 'undo' this, we must subtract off
        the number of prepended value from the lag-offseted indices before retrieving values from `series`.
        """
        if lags is None:
            X_block = None
        else:
            series_times = series.time_index
            is_range_idx = isinstance(series_times[0], int)
            # If series ends before last time in `feature_times`:
            add_to_end = series_times[-1] < feature_times[-1]
            # If series starts after first time in `feature_times`:
            add_to_start = series_times[0] > feature_times[0]
            # Either add to start *OR* add to end, we can't have both (see `get_feature_times_future`):
            if add_to_end:
                num_prepended = 0
                if is_range_idx:
                    # `+ 1` since `stop` index is exclusive:
                    series_times = pd.RangeIndex(
                        start=series_times[0],
                        stop=feature_times[-1] + 1,
                        step=series.freq,
                    )
                else:
                    series_times = pd.date_range(
                        start=series_times[0], end=feature_times[-1], freq=series.freq
                    )
            elif add_to_start:
                num_prepended = (series_times[0] - feature_times[0]) // series.freq
                if is_range_idx:
                    # `+ 1` since `stop` index is exclusive:
                    series_times = pd.RangeIndex(
                        start=feature_times[0],
                        stop=series_times[-1] + 1,
                        step=series.freq,
                    )
                else:
                    series_times = pd.date_range(
                        start=feature_times[0], end=series_times[-1], freq=series.freq
                    )
            else:
                num_prepended = 0
            array_vals = series.all_values(copy=False)[:, :, 0]
            X_block = []
            for time in feature_times:
                # Find position of time within series:
                time_idx = np.searchsorted(series_times, time)
                X_row = []
                for lag in lags:
                    # Offset by particular lag value:
                    idx_to_get = time_idx + lag
                    # Account for prepended values:
                    idx_to_get -= num_prepended
                    raise_if_not(
                        idx_to_get >= 0,
                        f"Unexpected case encountered: `time_idx + lag - num_prepended = {idx_to_get} < 0`.",
                    )
                    # Extract all components at this lagged time:
                    X_row.append(array_vals[idx_to_get, :].reshape(-1))
                # Concatenate together all lagged values into a single row:
                X_row = np.concatenate(X_row, axis=0)
                X_block.append(X_row)
            # Concatenate all rows (i.e. observations) together into a block:
            X_block = np.stack(X_block, axis=0)
        return X_block

        Helper function that creates the lagged features 'block' of a specific
        `series` (i.e. either `target_series`, `past_covariates`, or `future_covariates`);
        the feature matrix `X` is formed by concatenating the blocks of all of the specified
        series along the components axis. If `lags` is `None`, then `None` will be returned in
        lieu of an array. Please refer to the `create_lagged_features` docstring for further
        details about the structure of the `X` feature matrix.

        The returned `X_block` is constructed by looping over each time in `feature_times`,
        finding the index position of that time in the series, and then for each lag value in
        `lags`, offset this index position by a particular lag value; this offset index is then
        used to extract all of the components at a single lagged time.

        Unlike the implementation found in `darts.utils.data.tabularization`, this function doesn't
        use any 'vectorisation' tricks, which makes it slower to run, but more easily interpretable.

        Some of the times in `feature_times` may occur before the start *or* after the end of `series`;
        see the docstrings of `get_feature_times_past` and `get_feature_times_future` for why this is the
        case. Because of this, we need to prepend or append these 'extended times' to `series.time_index`
        before searching for the index of each time in the series. Even though the integer indices of the
        'extended times' won't be contained within the original `series`, offsetting these found indices
        by the requested lag value should 'bring us back' to a time within the original, unextended `series`.
        However, if we've prepended times to `series.time_index`, we have to note that all of the indices will
        be 'bumped up' by the number of values we've prepended, even after offsetting by a lag value. For example,
        if we extended `series.time_index` by prepending two values to the start, the integer index of the first
        actual value in `series` will occur at an index of `2` instead of `0`. To 'undo' this, we must subtract off
        the number of prepended value from the lag-offseted indices before retrieving values from `series`.
        
darts/tests/utils/tabularization/test_create_lagged_prediction_data.py