Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class WorkPackages::SetScheduleService
  attr_accessor :user, :work_packages, :initiated_by

  def initialize(user:, work_package:, initiated_by: nil)
    self.user = user
    self.work_packages = Array(work_package)
    self.initiated_by = initiated_by
  end

  def call(changed_attributes = %i(start_date due_date))
    altered = []

    if %i(parent parent_id).intersect?(changed_attributes)
      altered += schedule_by_parent
    end

    if %i(start_date due_date parent parent_id).intersect?(changed_attributes)
      altered += schedule_following
    end

    result = ServiceResult.success(result: work_packages.first)

    altered.each do |wp|
      result.add_dependent!(ServiceResult.success(result: wp))
    end

    result
  end

  private

  # rubocop:disable Metrics/AbcSize
  def schedule_by_parent
    work_packages
      .select { |wp| wp.start_date.nil? && wp.parent }
      .each do |wp|
        days = WorkPackages::Shared::Days.for(wp)
        wp.start_date = days.soonest_working_day(wp.parent.soonest_start)
        if wp.due_date || wp.duration
          wp.due_date = [
            wp.start_date,
            days.due_date(wp.start_date, wp.duration),
            wp.due_date
          ].compact.max
          assign_cause_for_journaling(wp, :parent)
        end
      end
  end
  # rubocop:enable Metrics/AbcSize

  # Finds all work packages that need to be rescheduled because of a
  # rescheduling of the service's work package and reschedules them.
  #
  # The order of the rescheduling is important as successors' dates are
  # calculated based on their predecessors' dates and ancestors' dates based on
  # their children's dates.
  #
  # Thus, the work packages following (having a follows relation, direct or
  # transitively) the service's work package are first all loaded, and then
  # sorted by their need to be scheduled before one another:
  #
  # - predecessors are scheduled before their successors
  # - children/descendants are scheduled before their parents/ancestors
  #
  # Manually scheduled work packages are not encountered at this point as they
  # are filtered out when fetching the work packages eligible for rescheduling.
  def schedule_following
    altered = []

    WorkPackages::ScheduleDependency.new(work_packages).in_schedule_order do |scheduled, dependency|
      reschedule(scheduled, dependency)

      altered << scheduled if scheduled.changed?
    end

    altered
  end

  # Schedules work packages based on either
  #  - their descendants if they are parents
  #  - their predecessors (or predecessors of their ancestors) if they are
  #    leaves
  def reschedule(scheduled, dependency)
    if dependency.has_descendants?
      reschedule_by_descendants(scheduled, dependency)
    else
      reschedule_by_predecessors(scheduled, dependency)
    end
  end

  # Inherits the start/due_date from the descendants of this work package.
  #
  # Only parent work packages are scheduled like this. start_date receives the
  # minimum of the dates (start_date and due_date) of the descendants due_date
  # receives the maximum of the dates (start_date and due_date) of the
  # descendants
  def reschedule_by_descendants(scheduled, dependency)
    set_dates(scheduled, dependency.start_date, dependency.due_date)
    assign_cause_for_journaling(scheduled, :children)
  end

  # Calculates the dates of a work package based on its follows relations.
  #
  # The start date of a work package is constrained by its direct and indirect
  # predecessors, as it must start strictly after all predecessors finish.
  #
  # The follows relations of ancestors are considered to be equal to own follows
  # relations as they inhibit moving a work package just the same. Only leaf
  # work packages are calculated like this.
  #
  # work package is moved to a later date:
  #   - following work packages are moved forward only to ensure they start
  #     after their predecessor's finish date. They may not need to move at all
  #     when there a time buffer between a follower and its predecessors
  #     (predecessors can also be acquired transitively by ancestors)
  #
  # work package moved to an earlier date:
  #   - following work packages do not move at all.
  def reschedule_by_predecessors(scheduled, dependency)
    return unless dependency.soonest_start_date

    new_start_date = [scheduled.start_date, dependency.soonest_start_date].compact.max
    new_due_date = determine_due_date(scheduled, new_start_date)
    set_dates(scheduled, new_start_date, new_due_date)
    assign_cause_for_journaling(scheduled, :predecessor)
  end

  def determine_due_date(work_package, start_date)
    # due date is set only if the moving work package already has one or has a
    # duration. If not, due date is nil (and duration will be nil too).
    return unless work_package.due_date || work_package.duration

    due_date =
      if work_package.duration
        days(work_package).due_date(start_date, work_package.duration)
      else
        work_package.due_date
      end

    # if due date is before start date, then start is used as due date.
    [start_date, due_date].max
  end

  def set_dates(work_package, start_date, due_date)
    work_package.start_date = start_date
    work_package.due_date = due_date
    work_package.duration = days(work_package).duration(start_date, due_date)
  end

  def days(work_package)
    WorkPackages::Shared::Days.for(work_package)
  end

  def assign_cause_for_journaling(work_package, relation)
    return {} if initiated_by.nil?
    return {} unless work_package.changes.keys.intersect?(%w(start_date due_date duration))

    if initiated_by.is_a?(WorkPackage)
      assign_cause_initiated_by_work_package(work_package, relation)
    elsif initiated_by.is_a?(Journal::WorkingDayUpdate)
      assign_cause_initiated_by_changed_working_days(work_package)
    end
  end

  def assign_cause_initiated_by_work_package(work_package, _relation)
    # For now we only track a generic cause, and not a specialized reason depending on the relation
    #
    # type_mapping = {
    #   parent: 'work_package_parent_changed_times',
    #   children: 'work_package_children_changed_times',
    #   predecessor: 'work_package_predecessor_changed_times',
    #   related: 'work_package_related_changed_times'
    # }
    # work_package.journal_cause = { "type" => type_mapping[relation], "work_package_id" => initiated_by.id }

    work_package.journal_cause = {
      "type" => "work_package_related_changed_times",
      "work_package_id" => initiated_by.id
    }
  end

  def assign_cause_initiated_by_changed_working_days(work_package)
    work_package.journal_cause = {
      "type" => 'working_days_changed',
      "changed_days" => initiated_by.to_h
    }
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe WorkPackages::SetScheduleService do
  create_shared_association_defaults_for_work_package_factory

  let(:work_package) do
    create(:work_package,
           subject: 'subject',
           start_date: work_package_start_date,
           due_date: work_package_due_date)
  end
  let(:work_package_due_date) { Time.zone.today }
  let(:work_package_start_date) { nil }
  let(:initiating_work_package) { work_package }
  let(:instance) do
    described_class.new(user:, work_package:, initiated_by: initiating_work_package)
  end
  let!(:following) { [] }

  let(:follower1_start_date) { Time.zone.today + 1.day }
  let(:follower1_due_date) { Time.zone.today + 3.days }
  let(:follower1_delay) { 0 }
  let(:following_work_package1) do
    create_follower(follower1_start_date,
                    follower1_due_date,
                    { work_package => follower1_delay })
  end
  let(:follower2_start_date) { Time.zone.today + 4.days }
  let(:follower2_due_date) { Time.zone.today + 8.days }
  let(:follower2_delay) { 0 }
  let(:following_work_package2) do
    create_follower(follower2_start_date,
                    follower2_due_date,
                    { following_work_package1 => follower2_delay })
  end
  let(:follower3_start_date) { Time.zone.today + 9.days }
  let(:follower3_due_date) { Time.zone.today + 10.days }
  let(:follower3_delay) { 0 }
  let(:following_work_package3) do
    create_follower(follower3_start_date,
                    follower3_due_date,
                    { following_work_package2 => follower3_delay })
  end

  let(:parent_follower1_start_date) { follower1_start_date }
  let(:parent_follower1_due_date) { follower1_due_date }

  let(:parent_following_work_package1) do
    create_parent(following_work_package1)
  end

  let(:follower_sibling_work_package) do
    create_follower(follower1_due_date + 2.days,
                    follower1_due_date + 4.days,
                    {},
                    parent: parent_following_work_package1)
  end

  let(:attributes) { [:start_date] }

  def create_follower(start_date, due_date, predecessors, parent: nil)
    work_package = create(:work_package,
                          subject: "follower of #{predecessors.keys.map(&:subject).to_sentence}",
                          start_date:,
                          due_date:,
                          parent:)

    predecessors.map do |predecessor, delay|
      create(:follows_relation,
             delay:,
             from: work_package,
             to: predecessor)
    end

    work_package
  end

  def create_parent(child, start_date: child.start_date, due_date: child.due_date)
    create(:work_package,
           subject: "parent of #{child.subject}",
           start_date:,
           due_date:).tap do |parent|
             child.parent = parent
             child.save
           end
  end

  def create_child(parent, start_date, due_date)
    create(:work_package,
           subject: "child of #{parent.subject}",
           start_date:,
           due_date:,
           parent:)
  end

  subject { instance.call(attributes) }

  shared_examples_for 'reschedules' do
    before do
      subject
    end

    it 'is success' do
      expect(subject)
        .to be_success
    end

    it 'updates the following work packages' do
      expected.each do |wp, (start_date, due_date)|
        expected_cause_type = "work_package_related_changed_times"
        result = subject.all_results.find { |result_wp| result_wp.id == wp.id }
        expect(result)
          .to be_present,
              "Expected work package ##{wp.id} '#{wp.subject}' to be rescheduled"

        expect(result.journal_cause['work_package_id'])
          .to eql(initiating_work_package.id),
              "Expected work package change to ##{wp.id} to have been caused by ##{initiating_work_package.id}."

        expect(result.journal_cause['type'])
        .to eql("work_package_related_changed_times"),
            "Expected work package change to ##{wp.id} to have been caused because ##{expected_cause_type}."

        expect(result.start_date)
          .to eql(start_date),
              "Expected work package ##{wp.id} '#{wp.subject}' " \
              "to have start date #{start_date.inspect}, got #{result.start_date.inspect}"
        expect(result.due_date)
          .to eql(due_date),
              "Expected work package ##{wp.id} '#{wp.subject}' " \
              "to have due date #{due_date.inspect}, got #{result.due_date.inspect}"

        duration = WorkPackages::Shared::AllDays.new.duration(start_date, due_date)

        expect(result.duration)
          .to eql(duration),
              "Expected work package ##{wp.id} '#{wp.subject}' " \
              "to have duration #{duration.inspect}, got #{result.duration.inspect}"
      end
    end

    it 'returns only the original and the changed work packages' do
      expect(subject.all_results)
        .to match_array expected.keys + [work_package]
    end
  end

  shared_examples_for 'does not reschedule' do
    before do
      subject
    end

    it 'is success' do
      expect(subject)
        .to be_success
    end

    it 'does not change any other work packages' do
      expect(subject.all_results)
        .to contain_exactly(work_package)
    end

    it 'does not assign a journal cause' do
      subject.all_results.each do |work_package|
        expect(work_package.journal_cause).to be_blank
      end
    end
  end

  context 'without relation' do
    it 'is success' do
      expect(subject)
        .to be_success
    end
  end

  context 'with a single successor' do
    let!(:following) do
      [following_work_package1]
    end

    context 'when moving forward' do
      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] }
        end
      end
    end

    context 'when moving forward with the follower having no due date' do
      let(:follower1_due_date) { nil }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, nil] }
        end
      end
    end

    context 'when moving forward with the follower having no start date' do
      let(:follower1_start_date) { nil }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 6.days] }
        end
      end
    end

    context 'when moving forward with the follower having some space left' do
      let(:follower1_start_date) { Time.zone.today + 3.days }
      let(:follower1_due_date) { Time.zone.today + 5.days }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] }
        end
      end
    end

    context 'when moving forward with the follower having enough space left to not be moved at all' do
      let(:follower1_start_date) { Time.zone.today + 10.days }
      let(:follower1_due_date) { Time.zone.today + 12.days }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'does not reschedule'
    end

    context 'when moving forward with the follower having some space left and a delay' do
      let(:follower1_start_date) { Time.zone.today + 5.days }
      let(:follower1_due_date) { Time.zone.today + 7.days }
      let(:follower1_delay) { 3 }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 9.days, Time.zone.today + 11.days] }
        end
      end
    end

    context 'when moving forward with the follower not needing to be moved' do
      let(:follower1_start_date) { Time.zone.today + 6.days }
      let(:follower1_due_date) { Time.zone.today + 8.days }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'does not reschedule'
    end

    context 'when moving backwards' do
      before do
        work_package.due_date = Time.zone.today - 5.days
      end

      it_behaves_like 'does not reschedule'
    end

    context 'when moving backwards with space between' do
      let(:follower1_start_date) { Time.zone.today + 3.days }
      let(:follower1_due_date) { Time.zone.today + 5.days }

      before do
        work_package.due_date = Time.zone.today - 5.days
      end

      it_behaves_like 'does not reschedule'
    end

    context 'when moving backwards with the follower having no start date (which should not happen) \
             and the due date after the scheduled to date' do
      let(:follower1_start_date) { nil }

      before do
        work_package.due_date = Time.zone.today - 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today - 4.days, follower1_due_date] }
        end
      end
    end

    context 'when moving forward with the follower having no start date (which should not happen) \
             and the due date before the scheduled to date' do
      let(:follower1_start_date) { nil }

      before do
        work_package.due_date = follower1_due_date + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [follower1_due_date + 6.days, follower1_due_date + 6.days] }
        end
      end
    end

    context 'when removing the dates on the predecessor' do
      before do
        work_package.start_date = work_package.due_date = nil
      end

      # The follower will keep its dates
      it_behaves_like 'does not reschedule'

      context 'when the follower has no start date but a due date' do
        let(:follower1_start_date) { nil }
        let(:follower1_due_date) { Time.zone.today + 15.days }

        it_behaves_like 'does not reschedule'
      end
    end

    context 'when not moving and the successor not having start & due date (e.g. creating relation)' do
      let(:follower1_start_date) { nil }
      let(:follower1_due_date) { nil }

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [work_package.due_date + 1.day, nil] }
        end
      end
    end

    context 'when not moving and the successor having due before predecessor due date (e.g. creating relation)' do
      let(:follower1_start_date) { nil }
      let(:follower1_due_date) { work_package_due_date - 5.days }

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [work_package.due_date + 1.day, work_package.due_date + 1.day] }
        end
      end
    end

    context 'when not moving and the successor having start before predecessor due date (e.g. creating relation)' do
      let(:follower1_start_date) { work_package_due_date - 5.days }
      let(:follower1_due_date) { nil }

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [work_package.due_date + 1.day, nil] }
        end
      end
    end

    context 'when not moving and the successor having start and due before predecessor due date (e.g. creating relation)' do
      let(:follower1_start_date) { work_package_due_date - 5.days }
      let(:follower1_due_date) { work_package_due_date - 2.days }

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [work_package.due_date + 1.day, work_package.due_date + 4.days] }
        end
      end
    end

    context 'when not having dates and the successor not having start & due date (e.g. creating relation)' do
      let(:work_package_due_date) { nil }
      let(:follower1_start_date) { nil }
      let(:follower1_due_date) { nil }

      it_behaves_like 'does not reschedule'
    end

    context 'with the successor having another predecessor which has no dates' do
      let(:following_work_package1) do
        create_follower(follower1_start_date,
                        follower1_due_date,
                        { work_package => follower1_delay,
                          another_successor => 0 })
      end
      let(:another_successor) do
        create(:work_package,
               start_date: nil,
               due_date: nil)
      end

      context 'when moving forward' do
        before do
          work_package.due_date = Time.zone.today + 5.days
        end

        it_behaves_like 'reschedules' do
          let(:expected) do
            { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] }
          end
        end
      end

      context 'when moving backwards' do
        before do
          work_package.due_date = Time.zone.today - 5.days
        end

        it_behaves_like 'does not reschedule'
      end
    end
  end

  context 'with only a parent' do
    let!(:parent_work_package) do
      create(:work_package).tap do |parent|
        work_package.parent = parent
        work_package.save
      end
    end
    let(:work_package_start_date) { Time.zone.today - 5.days }

    it_behaves_like 'reschedules' do
      let(:expected) do
        { parent_work_package => [work_package_start_date, work_package_due_date] }
      end
    end
  end

  context 'with a parent having a follower' do
    let(:work_package_start_date) { Time.zone.today }
    let(:work_package_due_date) { Time.zone.today + 5.days }
    let!(:parent_work_package) do
      create(:work_package,
             subject: "parent of #{work_package.subject}",
             start_date: Time.zone.today,
             due_date: Time.zone.today + 1.day).tap do |parent|
        work_package.parent = parent
        work_package.save
      end
    end
    let!(:follower_of_parent_work_package) do
      create_follower(Time.zone.today + 4.days,
                      Time.zone.today + 6.days,
                      { parent_work_package => 0 })
    end

    it_behaves_like 'reschedules' do
      let(:expected) do
        { parent_work_package => [work_package_start_date, work_package_due_date],
          follower_of_parent_work_package => [work_package_due_date + 1.day, work_package_due_date + 3.days] }
      end
    end

    # There is a bug in the scheduling that happens if the dependencies
    # array order is: [sibling child, follower of parent, parent]
    #
    # In this case, as the follower of parent only knows about direct
    # dependencies (and not about the transitive dependencies of children of
    # predecessor), it will be made the first in the order, based on the
    # current algorithm. And as the parent depends on its child, it will
    # come after it.
    #
    # Based on the algorithm when this test was written, the resulting
    # scheduling order will be [follower of parent, sibling child, parent],
    # which is wrong: if follower of parent is rescheduled first, then it
    # will not change because its predecessor, the parent, has not been
    # scheduled yet.
    #
    # The expected and right order is [sibling child, parent, follower of
    # parent].
    #
    # That's why the WorkPackage.for_scheduling call is mocked to customize
    # the order of the returned work_packages to reproduce this bug.
    context 'with also a sibling follower with same parent' do
      let!(:sibling_follower_of_work_package) do
        create_follower(Time.zone.today + 2.days,
                        Time.zone.today + 3.days,
                        { work_package => 0 },
                        parent: parent_work_package)
      end

      before do
        allow(WorkPackage)
          .to receive(:for_scheduling)
          .and_wrap_original do |method, *args|
            wanted_order = [sibling_follower_of_work_package, follower_of_parent_work_package, parent_work_package]
            method.call(*args).in_order_of(:id, wanted_order.map(&:id))
          end
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { sibling_follower_of_work_package => [work_package_due_date + 1.day, work_package_due_date + 2.days],
            parent_work_package => [work_package_start_date, work_package_due_date + 2.days],
            follower_of_parent_work_package => [work_package_due_date + 3.days, work_package_due_date + 5.days] }
        end
      end
    end
  end

  context 'with a single successor having a parent' do
    let!(:following) do
      [following_work_package1,
       parent_following_work_package1]
    end

    context 'when moving forward' do
      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days],
            parent_following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days] }
        end
      end
    end

    context 'when moving forward with the parent having another child not being moved' do
      let(:parent_follower1_start_date) { follower1_start_date }
      let(:parent_follower1_due_date) { follower1_due_date + 4.days }

      let!(:following) do
        [following_work_package1,
         parent_following_work_package1,
         follower_sibling_work_package]
      end

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days],
            parent_following_work_package1 => [Time.zone.today + 5.days, Time.zone.today + 8.days] }
        end
      end
    end

    context 'when moving backwards' do
      before do
        work_package.due_date = Time.zone.today - 5.days
      end

      it_behaves_like 'does not reschedule'
    end
  end

  context 'with a single successor having a child' do
    let(:child_start_date) { follower1_start_date }
    let(:child_due_date) { follower1_due_date }

    let(:child_work_package) { create_child(following_work_package1, child_start_date, child_due_date) }

    let!(:following) do
      [following_work_package1,
       child_work_package]
    end

    context 'when moving forward' do
      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days],
            child_work_package => [Time.zone.today + 6.days, Time.zone.today + 8.days] }
        end
      end
    end
  end

  context 'with a single successor having two children' do
    let(:follower1_start_date) { work_package_due_date + 1.day }
    let(:follower1_due_date) { work_package_due_date + 10.days }
    let(:child1_start_date) { follower1_start_date }
    let(:child1_due_date) { follower1_start_date + 3.days }
    let(:child2_start_date) { follower1_start_date + 8.days }
    let(:child2_due_date) { follower1_due_date }

    let(:child1_work_package) { create_child(following_work_package1, child1_start_date, child1_due_date) }
    let(:child2_work_package) { create_child(following_work_package1, child2_start_date, child2_due_date) }

    let!(:following) do
      [following_work_package1,
       child1_work_package,
       child2_work_package]
    end

    context 'with unchanged dates (e.g. when creating a follows relation) and successor starting 1 day after scheduled' do
      it_behaves_like 'does not reschedule'
    end

    context 'with unchanged dates (e.g. when creating a follows relation) and successor starting 3 days after scheduled' do
      let(:follower1_start_date) { work_package_due_date + 3.days }
      let(:follower1_due_date) { follower1_start_date + 10.days }
      let(:child1_start_date) { follower1_start_date }
      let(:child1_due_date) { follower1_start_date + 6.days }
      let(:child2_start_date) { follower1_start_date + 8.days }
      let(:child2_due_date) { follower1_due_date }

      it_behaves_like 'does not reschedule'
    end

    context 'with unchanged dates (e.g. when creating a follows relation) and successor\'s first child needs to be rescheduled' do
      let(:follower1_start_date) { work_package_due_date - 3.days }
      let(:follower1_due_date) { work_package_due_date + 10.days }
      let(:child1_start_date) { follower1_start_date }
      let(:child1_due_date) { follower1_start_date + 6.days }
      let(:child2_start_date) { follower1_start_date + 8.days }
      let(:child2_due_date) { follower1_due_date }

      # following parent is reduced in length as the children allow to be executed at the same time
      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [work_package_due_date + 1.day, follower1_due_date],
            child1_work_package => [work_package_due_date + 1.day, follower1_start_date + 10.days] }
        end
      end
    end

    context 'with unchanged dates (e.g. when creating a follows relation) and successor\s children need to be rescheduled' do
      let(:follower1_start_date) { work_package_due_date - 8.days }
      let(:follower1_due_date) { work_package_due_date + 10.days }
      let(:child1_start_date) { follower1_start_date }
      let(:child1_due_date) { follower1_start_date + 4.days }
      let(:child2_start_date) { follower1_start_date + 6.days }
      let(:child2_due_date) { follower1_due_date }

      # following parent is reduced in length and children are rescheduled
      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [work_package_due_date + 1.day, follower1_start_date + 21.days],
            child1_work_package => [work_package_due_date + 1.day, child1_due_date + 9.days],
            child2_work_package => [work_package_due_date + 1.day, follower1_start_date + 21.days] }
        end
      end
    end
  end

  context 'with a chain of successors' do
    let(:follower1_start_date) { Time.zone.today + 1.day }
    let(:follower1_due_date) { Time.zone.today + 3.days }
    let(:follower2_start_date) { Time.zone.today + 4.days }
    let(:follower2_due_date) { Time.zone.today + 8.days }
    let(:follower3_start_date) { Time.zone.today + 9.days }
    let(:follower3_due_date) { Time.zone.today + 10.days }

    let!(:following) do
      [following_work_package1,
       following_work_package2,
       following_work_package3]
    end

    context 'when moving forward' do
      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days],
            following_work_package2 => [Time.zone.today + 9.days, Time.zone.today + 13.days],
            following_work_package3 => [Time.zone.today + 14.days, Time.zone.today + 15.days] }
        end
      end
    end

    context 'when moving forward with some space between the followers' do
      let(:follower1_start_date) { Time.zone.today + 1.day }
      let(:follower1_due_date) { Time.zone.today + 3.days }
      let(:follower2_start_date) { Time.zone.today + 7.days }
      let(:follower2_due_date) { Time.zone.today + 10.days }
      let(:follower3_start_date) { Time.zone.today + 17.days }
      let(:follower3_due_date) { Time.zone.today + 18.days }

      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days],
            following_work_package2 => [Time.zone.today + 9.days, Time.zone.today + 12.days] }
        end
      end
    end

    context 'when moving backwards' do
      before do
        work_package.due_date = Time.zone.today - 5.days
      end

      it_behaves_like 'does not reschedule'
    end
  end

  context 'with a chain of successors with two paths leading to the same work package in the end' do
    let(:follower3_start_date) { Time.zone.today + 4.days }
    let(:follower3_due_date) { Time.zone.today + 7.days }
    let(:follower3_delay) { 0 }
    let(:following_work_package3) do
      create_follower(follower3_start_date,
                      follower3_due_date,
                      { work_package => follower3_delay })
    end
    let(:follower4_start_date) { Time.zone.today + 9.days }
    let(:follower4_due_date) { Time.zone.today + 10.days }
    let(:follower4_delay2) { 0 }
    let(:follower4_delay3) { 0 }
    let(:following_work_package4) do
      create_follower(follower4_start_date,
                      follower4_due_date,
                      { following_work_package2 => follower4_delay2, following_work_package3 => follower4_delay3 })
    end
    let!(:following) do
      [following_work_package1,
       following_work_package2,
       following_work_package3,
       following_work_package4]
    end

    context 'when moving forward' do
      before do
        work_package.due_date = Time.zone.today + 5.days
      end

      it_behaves_like 'reschedules' do
        let(:expected) do
          { following_work_package1 => [Time.zone.today + 6.days, Time.zone.today + 8.days],
            following_work_package2 => [Time.zone.today + 9.days, Time.zone.today + 13.days],
            following_work_package3 => [Time.zone.today + 6.days, Time.zone.today + 9.days],
            following_work_package4 => [Time.zone.today + 14.days, Time.zone.today + 15.days] }
        end
      end
    end

    context 'when moving backwards' do
      before do
        work_package.due_date = Time.zone.today - 5.days
      end

      it_behaves_like 'does not reschedule'
    end
  end

  context 'when setting the parent' do
    let(:new_parent_work_package) { create(:work_package) }
    let(:attributes) { [:parent] }

    before do
      allow(new_parent_work_package)
        .to receive(:soonest_start)
              .and_return(soonest_date)
      allow(work_package)
        .to receive(:parent)
              .and_return(new_parent_work_package)
    end

    context "with the parent being restricted in its ability to be moved" do
      let(:soonest_date) { Time.zone.today + 3.days }

      it 'sets the start date and due date to the earliest possible date' do
        subject

        expect(work_package.start_date).to eql(Time.zone.today + 3.days)
        expect(work_package.due_date).to eql(Time.zone.today + 3.days)
      end

      it 'does not change the due date if after the newly set start date' do
        work_package.due_date = Time.zone.today + 5.days
        subject

        expect(work_package.start_date).to eql(Time.zone.today + 3.days)
        expect(work_package.due_date).to eql(Time.zone.today + 5.days)
      end
    end

    context 'with the parent being restricted but work package already having dates set' do
      let(:soonest_date) { Time.zone.today + 3.days }

      before do
        work_package.start_date = Time.zone.today + 4.days
        work_package.due_date = Time.zone.today + 5.days
      end

      it 'sets the dates to provided dates' do
        subject

        expect(work_package.start_date).to eql(Time.zone.today + 4.days)
        expect(work_package.due_date).to eql(Time.zone.today + 5.days)
      end
    end

    context 'with the parent being restricted but the attributes define an earlier date' do
      let(:soonest_date) { Time.zone.today + 3.days }

      before do
        work_package.start_date = Time.zone.today + 1.day
        work_package.due_date = Time.zone.today + 2.days
      end

      # This would be invalid but the dates should be set nevertheless
      # so we can have a correct error handling.
      it 'sets the dates to provided dates' do
        subject

        expect(work_package.start_date).to eql(Time.zone.today + 1.day)
        expect(work_package.due_date).to eql(Time.zone.today + 2.days)
      end
    end
  end

  context 'with deep hierarchy of work packages' do
    before do
      work_package.due_date = Time.zone.today - 5.days
    end

    def create_hierarchy(parent, nb_children_by_levels)
      nb_children, *remaining_levels = nb_children_by_levels
      children = create_list(:work_package, nb_children, parent:)
      if remaining_levels.any?
        children.each do |child|
          create_hierarchy(child, remaining_levels)
        end
      end
    end

    it 'does not fail with a SystemStackError (regression #43894)' do
      parent = create(:work_package, start_date: Date.current, due_date: Date.current)
      hierarchy = [1, 1, 1, 1, 2, 4, 4, 4]
      create_hierarchy(parent, hierarchy)

      # The bug triggers when moving work package is in the middle of the
      # hierarchy
      work_package.parent = parent.children.first.children.first.children.first
      work_package.save

      expect { instance.call(attributes) }
        .not_to raise_error
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class WorkPackages::UpdateAncestorsService
  attr_accessor :user,
                :work_package

  def initialize(user:, work_package:)
    self.user = user
    self.work_package = work_package
  end

  def call(attributes)
    modified = update_current_and_former_ancestors(attributes)

    set_journal_note(modified)

    # Do not send notification for parent updates
    success = Journal::NotificationConfiguration.with(false) do
      modified.all? { |wp| wp.save(validate: false) }
    end

    result = ServiceResult.new(success:, result: work_package)

    modified.each do |wp|
      result.add_dependent!(ServiceResult.new(success: !wp.changed?, result: wp))
    end

    result
  end

  private

  def update_current_and_former_ancestors(attributes)
    WorkPackages::UpdateAncestors::Loader
      .new(work_package, (%i(parent_id parent) & attributes).any?)
      .select do |ancestor, loader|
        inherit_attributes(ancestor, loader, attributes)

        ancestor.changed?
      end
  end

  def inherit_attributes(ancestor, loader, attributes)
    return unless attributes_justify_inheritance?(attributes)

    # Estimated hours need to be calculated before the done_ratio below.
    # The aggregation only depends on estimated hours.
    derive_estimated_hours(ancestor, loader) if inherit?(attributes, :estimated_hours)

    # Progress (done_ratio or also: percentDone) depends on both
    # the completion of sub-WPs, as well as the estimated hours
    # as a weight factor. So changes in estimated hours also have
    # to trigger a recalculation of done_ratio.
    inherit_done_ratio(ancestor, loader) if inherit?(attributes, :done_ratio) || inherit?(attributes, :estimated_hours)

    inherit_ignore_non_working_days(ancestor, loader) if inherit?(attributes, :ignore_non_working_days)
  end

  def inherit?(attributes, attribute)
    ([attribute, :parent, :parent_id] & attributes).any?
  end

  def set_journal_note(work_packages)
    work_packages.each do |wp|
      wp.journal_notes = I18n.t('work_package.updated_automatically_by_child_changes', child: "##{work_package.id}")
    end
  end

  def inherit_done_ratio(ancestor, loader)
    return if WorkPackage.done_ratio_disabled?

    return if WorkPackage.use_status_for_done_ratio? && ancestor.status && ancestor.status.default_done_ratio

    # done ratio = weighted average ratio of leaves
    ancestor.done_ratio = (aggregate_done_ratio(ancestor, loader) || 0).round
  end

  # Sets the ignore_non_working_days to true if any ancestor has its value set to true.
  # If there is no value returned from the descendants, that means that the work package in
  # question no longer has a descendant. But since we are in the service going up the ancestor chain,
  # such a work package is the former parent. The property of such a work package is reset to `false`.
  def inherit_ignore_non_working_days(work_package, loader)
    return if work_package.schedule_manually

    descendant_value = ignore_non_working_days_of_descendants(work_package, loader)

    if descendant_value.nil?
      descendant_value = work_package.ignore_non_working_days
    end

    work_package.ignore_non_working_days = descendant_value
  end

  ##
  # done ratio = weighted average ratio of leaves
  def aggregate_done_ratio(work_package, loader)
    leaves = loader.leaves_of(work_package)

    leaves_count = leaves.size

    if leaves_count.positive?
      average = average_estimated_hours(leaves)
      progress = done_ratio_sum(leaves, average) / (average * leaves_count)

      progress.round(2)
    end
  end

  def average_estimated_hours(leaves)
    # 0 and nil shall be considered the same for estimated hours
    sum = all_estimated_hours(leaves).sum.to_f
    count = all_estimated_hours(leaves).count

    count = 1 if count.zero?

    average = sum / count

    average.zero? ? 1 : average
  end

  def done_ratio_sum(leaves, average_estimated_hours)
    # Do not take into account estimated_hours when it is either nil or set to 0.0
    summands = leaves.map do |leaf|
      estimated_hours = if leaf.estimated_hours.to_f.positive?
                          leaf.estimated_hours
                        else
                          average_estimated_hours
                        end

      done_ratio = if leaf.closed?
                     100
                   else
                     leaf.done_ratio || 0
                   end

      estimated_hours * done_ratio
    end

    summands.sum
  end

  def derive_estimated_hours(work_package, loader)
    descendants = loader.descendants_of(work_package)

    work_package.derived_estimated_hours = not_zero(all_estimated_hours(descendants).sum.to_f)
  end

  def not_zero(value)
    value unless value.zero?
  end

  def all_estimated_hours(work_packages)
    work_packages
      .map(&:estimated_hours)
      .reject { |hours| hours.to_f.zero? }
  end

  def attributes_justify_inheritance?(attributes)
    (%i(estimated_hours done_ratio parent parent_id status status_id ignore_non_working_days) & attributes).any?
  end

  def ignore_non_working_days_of_descendants(ancestor, loader)
    children = loader
                 .children_of(ancestor)
                 .reject(&:schedule_manually)

    if children.any?
      children.any?(&:ignore_non_working_days)
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe WorkPackages::UpdateAncestorsService, type: :model do
  shared_association_default(:author, factory_name: :user) { create(:user) }
  shared_association_default(:project_with_types) { create(:project_with_types) }
  shared_association_default(:priority) { create(:priority) }
  shared_association_default(:open_status, factory_name: :status) { create(:status) }
  shared_let(:closed_status) { create(:closed_status) }
  shared_let(:user) { create(:user) }

  let(:estimated_hours) { [nil, nil, nil] }
  let(:done_ratios) { [0, 0, 0] }
  let(:statuses) { %i(open open open) }
  let(:aggregate_done_ratio) { 0.0 }
  let(:ignore_non_working_days) { [false, false, false] }

  describe 'done_ratio/estimated_hours propagation' do
    context 'for the new ancestor chain' do
      shared_examples 'attributes of parent having children' do
        before do
          children
        end

        it 'updated one work package - the parent' do
          expect(subject.dependent_results.map(&:result))
          .to contain_exactly(parent)
        end

        it 'has the expected aggregate done ratio' do
          expect(subject.dependent_results.first.result.done_ratio)
          .to eq aggregate_done_ratio
        end

        it 'has the expected derived estimated_hours' do
          expect(subject.dependent_results.first.result.derived_estimated_hours)
          .to eq aggregate_estimated_hours
        end

        it 'is a success' do
          expect(subject)
          .to be_success
        end
      end

      let(:children) do
        (statuses.size - 1).downto(0).map do |i|
          create(:work_package,
                 parent:,
                 status: statuses[i] == :open ? open_status : closed_status,
                 estimated_hours: estimated_hours[i],
                 done_ratio: done_ratios[i],
                 ignore_non_working_days:)
        end
      end

      shared_let(:parent) { create(:work_package, status: open_status) }

      subject do
        # In the call we only use estimated_hours (instead of also adding
        # done_ratio) in order to test that changes in estimated hours
        # trigger a recalculation of done_ration, because estimated hours
        # act as weights in this calculation.
        described_class
          .new(user:,
               work_package: children.first)
          .call(%i(estimated_hours))
      end

      context 'with no estimated hours and no progress' do
        let(:statuses) { %i(open open open) }

        it 'is a success' do
          expect(subject)
          .to be_success
        end

        it 'does not update the parent' do
          expect(subject.dependent_results)
          .to be_empty
        end
      end

      context 'with 1 out of 3 tasks having estimated hours and 2 out of 3 tasks done' do
        let(:statuses) do
          %i(open closed closed)
        end

        it_behaves_like 'attributes of parent having children' do
          let(:estimated_hours) do
            [0.0, 2.0, 0.0]
          end

          # 66.67 rounded - previous wrong result: 133
          let(:aggregate_done_ratio) do
            67
          end
          let(:aggregate_estimated_hours) do
            2.0
          end
        end

        context 'with mixed nil and 0 values for estimated hours' do
          it_behaves_like 'attributes of parent having children' do
            let(:estimated_hours) do
              [nil, 2.0, 0.0]
            end

            # 66.67 rounded - previous wrong result: 100
            let(:aggregate_done_ratio) do
              67
            end
            let(:aggregate_estimated_hours) do
              2.0
            end
          end
        end
      end

      context 'with some values same for done ratio' do
        it_behaves_like 'attributes of parent having children' do
          let(:done_ratios) { [20, 20, 50] }
          let(:estimated_hours) { [nil, nil, nil] }

          let(:aggregate_done_ratio) { 30 }
          let(:aggregate_estimated_hours) { nil }
        end
      end

      context 'with no estimated hours and 1.5 of the tasks done' do
        it_behaves_like 'attributes of parent having children' do
          let(:done_ratios) { [0, 50, 100] }

          let(:aggregate_done_ratio) { 50 }
          let(:aggregate_estimated_hours) { nil }
        end
      end

      context 'with estimated hours being 1, 2 and 5' do
        let(:estimated_hours) { [1, 2, 5] }

        context 'with the last 2 tasks at 100% progress' do
          it_behaves_like 'attributes of parent having children' do
            let(:done_ratios) { [0, 100, 100] }

            # (2 + 5 = 7) / 8 estimated hours done
            let(:aggregate_done_ratio) { 88 } # 87.5 rounded
            let(:aggregate_estimated_hours) { estimated_hours.sum }
          end
        end

        context 'with the last 2 tasks closed (therefore at 100%)' do
          it_behaves_like 'attributes of parent having children' do
            let(:statuses) { %i(open closed closed) }

            # (2 + 5 = 7) / 8 estimated hours done
            let(:aggregate_done_ratio) { 88 } # 87.5 rounded
            let(:aggregate_estimated_hours) { estimated_hours.sum }
          end
        end

        context 'with mixed done ratios, statuses' do
          it_behaves_like 'attributes of parent having children' do
            let(:done_ratios) { [50, 75, 42] }
            let(:statuses) { %i(open open closed) }

            #  50%       75%        100% (42 ignored)
            # (0.5 * 1 + 0.75 * 2 + 1 * 5 [since closed] = 7)
            # (0.5 + 1.5 + 5 = 7) / 8 estimated hours done
            let(:aggregate_done_ratio) { 88 } # 87.5 rounded
            let(:aggregate_estimated_hours) { estimated_hours.sum }
          end
        end
      end

      context 'with everything playing together' do
        it_behaves_like 'attributes of parent having children' do
          let(:statuses) { %i(open open closed open) }
          let(:done_ratios) { [0, 0, 0, 50] }
          let(:estimated_hours) { [0.0, 3.0, nil, 7.0] }

          # (0 * 5 + 0 * 3 + 1 * 5 + 0.5 * 7 = 8.5) / 20 est. hours done
          let(:aggregate_done_ratio) { 43 } # 42.5 rounded
          let(:aggregate_estimated_hours) { 10.0 }
        end
      end
    end

    context 'for the previous ancestors' do
      shared_let(:sibling_status) { open_status }
      shared_let(:sibling_done_ratio) { 50 }
      shared_let(:sibling_estimated_hours) { 7.0 }

      shared_let(:grandparent) do
        create(:work_package)
      end
      shared_let(:parent) do
        create(:work_package,
               parent: grandparent)
      end
      shared_let(:sibling) do
        create(:work_package,
               parent:,
               status: sibling_status,
               estimated_hours: sibling_estimated_hours,
               done_ratio: sibling_done_ratio)
      end

      shared_let(:work_package) do
        create(:work_package,
               parent:)
      end

      subject do
        work_package.parent = nil
        work_package.save!

        described_class
          .new(user:,
               work_package:)
          .call(%i(parent))
      end

      before do
        subject
      end

      it 'is successful' do
        expect(subject)
          .to be_success
      end

      it 'returns the former ancestors in the dependent results' do
        expect(subject.dependent_results.map(&:result))
          .to contain_exactly(parent, grandparent)
      end

      it 'updates the done_ratio of the former parent' do
        expect(parent.reload(select: :done_ratio).done_ratio)
          .to eql sibling_done_ratio
      end

      it 'updates the estimated_hours of the former parent' do
        expect(parent.reload(select: :derived_estimated_hours).derived_estimated_hours)
          .to eql sibling_estimated_hours
      end

      it 'updates the done_ratio of the former grandparent' do
        expect(grandparent.reload(select: :done_ratio).done_ratio)
          .to eql sibling_done_ratio
      end

      it 'updates the estimated_hours of the former grandparent' do
        expect(grandparent.reload(select: :derived_estimated_hours).derived_estimated_hours)
          .to eql sibling_estimated_hours
      end
    end

    context 'for new ancestors' do
      shared_let(:status) { open_status }
      shared_let(:done_ratio) { 50 }
      shared_let(:estimated_hours) { 7.0 }

      shared_let(:grandparent) do
        create(:work_package)
      end
      shared_let(:parent) do
        create(:work_package,
               parent: grandparent)
      end
      shared_let(:work_package) do
        create(:work_package,
               status:,
               estimated_hours:,
               done_ratio:)
      end

      shared_examples_for 'updates the attributes within the new hierarchy' do
        before do
          subject
        end

        it 'is successful' do
          expect(subject)
            .to be_success
        end

        it 'returns the new ancestors in the dependent results' do
          expect(subject.dependent_results.map(&:result))
            .to contain_exactly(parent, grandparent)
        end

        it 'updates the done_ratio of the new parent' do
          expect(parent.reload(select: :done_ratio).done_ratio)
            .to eql done_ratio
        end

        it 'updates the estimated_hours of the new parent' do
          expect(parent.reload(select: :derived_estimated_hours).derived_estimated_hours)
            .to eql estimated_hours
        end

        it 'updates the done_ratio of the new grandparent' do
          expect(grandparent.reload(select: :done_ratio).done_ratio)
            .to eql done_ratio
        end

        it 'updates the estimated_hours of the new grandparent' do
          expect(grandparent.reload(select: :derived_estimated_hours).derived_estimated_hours)
            .to eql estimated_hours
        end
      end

      context 'if setting the parent' do
        subject do
          work_package.parent = parent
          work_package.save!
          work_package.parent_id_was

          described_class
            .new(user:,
                 work_package:)
            .call(%i(parent))
        end

        it_behaves_like 'updates the attributes within the new hierarchy'
      end

      context 'if setting the parent_id' do
        subject do
          work_package.parent_id = parent.id
          work_package.save!
          work_package.parent_id_was

          described_class
            .new(user:,
                 work_package:)
            .call(%i(parent_id))
        end

        it_behaves_like 'updates the attributes within the new hierarchy'
      end
    end

    context 'with old and new parent having a common ancestor' do
      shared_let(:status) { open_status }
      shared_let(:done_ratio) { 50 }
      shared_let(:estimated_hours) { 7.0 }

      shared_let(:grandparent) do
        create(:work_package,
               derived_estimated_hours: estimated_hours,
               done_ratio:)
      end
      shared_let(:old_parent) do
        create(:work_package,
               parent: grandparent,
               derived_estimated_hours: estimated_hours,
               done_ratio:)
      end
      shared_let(:new_parent) do
        create(:work_package,
               parent: grandparent)
      end
      shared_let(:work_package) do
        create(:work_package,
               parent: old_parent,
               status:,
               estimated_hours:,
               done_ratio:)
      end

      subject do
        work_package.parent = new_parent
        # In this test case, derived_estimated_hours and done_ratio will not
        # inherently change on grandparent. However, if work_package has siblings
        # then changing its parent could cause derived_estimated_hours and/or
        # done_ratio on grandparent to inherently change. To verify that
        # grandparent can be properly updated in that case without making this
        # test dependent on the implementation details of the
        # derived_estimated_hours and done_ratio calculations, force
        # derived_estimated_hours and done_ratio to change at the same time as the
        # parent.
        work_package.estimated_hours = (estimated_hours + 1)
        work_package.done_ratio = (done_ratio + 1)
        work_package.save!

        described_class
          .new(user:,
               work_package:)
          .call(%i(parent))
      end

      before do
        subject
      end

      it 'is successful' do
        expect(subject)
          .to be_success
      end

      it 'returns both the former and new ancestors in the dependent results without duplicates' do
        expect(subject.dependent_results.map(&:result))
          .to contain_exactly(new_parent, grandparent, old_parent)
      end

      it 'updates the done_ratio of the former parent' do
        expect(old_parent.reload(select: :done_ratio).done_ratio)
          .to be 0
      end

      it 'updates the estimated_hours of the former parent' do
        expect(old_parent.reload(select: :derived_estimated_hours).derived_estimated_hours)
          .to be_nil
      end
    end
  end

  describe 'ignore_non_working_days propagation' do
    shared_let(:grandgrandparent) do
      create(:work_package,
             subject: 'grandgrandparent')
    end
    shared_let(:grandparent) do
      create(:work_package,
             subject: 'grandparent',
             parent: grandgrandparent)
    end
    shared_let(:parent) do
      create(:work_package,
             subject: 'parent',
             parent: grandparent)
    end
    shared_let(:sibling) do
      create(:work_package,
             subject: 'sibling',
             parent:)
    end
    shared_let(:work_package) do
      create(:work_package)
    end

    subject do
      work_package.parent = new_parent
      work_package.save!

      described_class
        .new(user:,
             work_package:)
        .call(%i(parent))
    end

    let(:new_parent) { parent }

    context 'for the previous ancestors (parent removed)' do
      let(:new_parent) { nil }

      before do
        work_package.parent = parent
        work_package.save

        [grandgrandparent, grandparent, parent, work_package].each do |wp|
          wp.update_column(:ignore_non_working_days, true)
        end

        [sibling].each do |wp|
          wp.update_column(:ignore_non_working_days, false)
        end
      end

      it 'is successful' do
        expect(subject)
          .to be_success
      end

      it 'returns the former ancestors in the dependent results' do
        expect(subject.dependent_results.map(&:result))
          .to contain_exactly(parent, grandparent, grandgrandparent)
      end

      it 'sets the ignore_non_working_days property of the former ancestor chain to the value of the
          only remaining child (former sibling)' do
        subject

        expect(parent.reload.ignore_non_working_days)
          .to be_falsey

        expect(grandparent.reload.ignore_non_working_days)
          .to be_falsey

        expect(grandgrandparent.reload.ignore_non_working_days)
          .to be_falsey

        expect(sibling.reload.ignore_non_working_days)
          .to be_falsey
      end
    end

    context 'for the new ancestors where the grandparent is on manual scheduling' do
      before do
        [grandgrandparent, work_package].each do |wp|
          wp.update_column(:ignore_non_working_days, true)
        end

        [grandparent, parent, sibling].each do |wp|
          wp.update_column(:ignore_non_working_days, false)
        end

        [grandparent].each do |wp|
          wp.update_column(:schedule_manually, true)
        end
      end

      it 'is successful' do
        expect(subject)
          .to be_success
      end

      it 'returns the former ancestors in the dependent results' do
        expect(subject.dependent_results.map(&:result))
          .to contain_exactly(parent)
      end

      it 'sets the ignore_non_working_days property of the new ancestors' do
        subject

        expect(parent.reload.ignore_non_working_days)
          .to be_truthy

        expect(grandparent.reload.ignore_non_working_days)
          .to be_falsey

        expect(grandgrandparent.reload.ignore_non_working_days)
          .to be_truthy

        expect(sibling.reload.ignore_non_working_days)
          .to be_falsey
      end
    end

    context 'for the new ancestors where the parent is on manual scheduling' do
      before do
        [grandgrandparent, grandparent, work_package].each do |wp|
          wp.update_column(:ignore_non_working_days, true)
        end

        [parent, sibling].each do |wp|
          wp.update_column(:ignore_non_working_days, false)
        end

        [parent].each do |wp|
          wp.update_column(:schedule_manually, true)
        end
      end

      it 'is successful' do
        expect(subject)
          .to be_success
      end

      it 'returns the former ancestors in the dependent results' do
        expect(subject.dependent_results.map(&:result))
          .to be_empty
      end

      it 'sets the ignore_non_working_days property of the new ancestors' do
        subject

        expect(parent.reload.ignore_non_working_days)
          .to be_falsey

        expect(grandparent.reload.ignore_non_working_days)
          .to be_truthy

        expect(grandgrandparent.reload.ignore_non_working_days)
          .to be_truthy

        expect(sibling.reload.ignore_non_working_days)
          .to be_falsey
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class WorkPackages::UpdateService < BaseServices::Update
  include ::WorkPackages::Shared::UpdateAncestors
  include Attachments::ReplaceAttachments

  attr_accessor :cause_of_update

  def initialize(user:, model:, contract_class: nil, contract_options: {}, cause_of_update: nil)
    super(user:, model:, contract_class:, contract_options:)
    self.cause_of_update = cause_of_update || model
  end

  private

  def after_perform(service_call)
    update_related_work_packages(service_call)
    cleanup(service_call.result)

    service_call
  end

  def update_related_work_packages(service_call)
    update_ancestors([service_call.result]).each do |ancestor_service_call|
      ancestor_service_call.dependent_results.each do |ancestor_dependent_service_call|
        service_call.add_dependent!(ancestor_dependent_service_call)
      end
    end

    update_related(service_call.result).each do |related_service_call|
      service_call.add_dependent!(related_service_call)
    end
  end

  def update_related(work_package)
    consolidated_calls(update_descendants(work_package) + reschedule_related(work_package))
      .each { |dependent_call| dependent_call.result.save(validate: false) }
  end

  def update_descendants(work_package)
    if work_package.saved_change_to_project_id?
      attributes = { project: work_package.project }

      work_package.descendants.map do |descendant|
        set_descendant_attributes(attributes, descendant)
      end
    else
      []
    end
  end

  def set_descendant_attributes(attributes, descendant)
    WorkPackages::SetAttributesService
      .new(user:,
           model: descendant,
           contract_class: WorkPackages::UpdateDependentContract)
      .call(attributes)
  end

  def cleanup(work_package)
    if work_package.saved_change_to_project_id?
      moved_work_packages = [work_package] + work_package.descendants
      delete_relations(moved_work_packages)
      move_time_entries(moved_work_packages, work_package.project_id)
      move_work_package_memberships(moved_work_packages, work_package.project_id)
    end
    if work_package.saved_change_to_type_id?
      reset_custom_values(work_package)
    end
  end

  def delete_relations(work_packages)
    unless Setting.cross_project_work_package_relations?
      Relation
        .of_work_package(work_packages)
        .destroy_all
    end
  end

  def move_time_entries(work_packages, project_id)
    TimeEntry
      .on_work_packages(work_packages)
      .update_all(project_id:)
  end

  def move_work_package_memberships(work_packages, project_id)
    Member
      .where(entity: work_packages)
      .update_all(project_id:)
  end

  def reset_custom_values(work_package)
    work_package.reset_custom_values!
  end

  def reschedule_related(work_package)
    rescheduled = if work_package.saved_change_to_parent_id? && work_package.parent_id_before_last_save
                    reschedule_former_siblings(work_package).dependent_results
                  else
                    []
                  end

    rescheduled + reschedule(work_package, [work_package]).dependent_results
  end

  def reschedule_former_siblings(work_package)
    reschedule(work_package, WorkPackage.where(parent_id: work_package.parent_id_before_last_save))
  end

  def reschedule(work_package, work_packages)
    WorkPackages::SetScheduleService
      .new(user:, work_package: work_packages, initiated_by: cause_of_update)
      .call(work_package.saved_changes.keys.map(&:to_sym))
  end

  # When multiple services change a work package, we still only want one update to the database due to:
  # * performance
  # * having only one journal entry
  # * stale object errors
  # we thus consolidate the results so that one instance contains the changes made by all the services.
  def consolidated_calls(service_calls)
    service_calls
      .group_by { |sc| sc.result.id }
      .map do |(_, same_work_package_calls)|
      same_work_package_calls.pop.tap do |master|
        same_work_package_calls.each do |sc|
          master.result.attributes = sc.result.changes.transform_values(&:last)
        end
      end
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'
require 'services/base_services/behaves_like_update_service'

RSpec.describe WorkPackages::UpdateService, type: :model do
  # This is now only a very basic test testing the structure of the service.
  # The domain tests are in the update_service_integration_spec.rb
  it_behaves_like 'BaseServices update service' do
    before do
      allow(set_attributes_errors)
        .to receive(:merge!)
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class WorkPackages::DeleteService < BaseServices::Delete
  include ::WorkPackages::Shared::UpdateAncestors

  private

  def persist(service_result)
    descendants = model.descendants.to_a

    result = super

    if result.success?
      update_ancestors_all_attributes(result.all_results).each do |ancestor_result|
        result.merge!(ancestor_result)
      end

      destroy_descendants(descendants, result)
      delete_associated(model)
    end

    result
  end

  def destroy(work_package)
    work_package.destroy
  rescue ActiveRecord::StaleObjectError
    destroy(work_package.reload)
  end

  def destroy_descendants(descendants, result)
    descendants.each do |descendant|
      result.add_dependent!(ServiceResult.new(success: destroy(descendant), result: descendant))
    end
  end

  def delete_associated(model)
    delete_notifications_resource(model.id)
  end

  def delete_notifications_resource(id)
    Notification
      .where(resource_type: :WorkPackage, resource_id: id)
      .delete_all
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe WorkPackages::DeleteService do
  let(:user) do
    build_stubbed(:user)
  end
  let(:work_package) do
    build_stubbed(:work_package, type: build_stubbed(:type))
  end
  let(:instance) do
    described_class
      .new(user:,
           model: work_package)
  end
  let(:destroyed_result) { true }
  let(:destroy_allowed) { true }

  subject { instance.call }

  before do
    allow(work_package).to receive(:reload).and_return(work_package)
    expect(work_package).to receive(:destroy).and_return(destroyed_result)
    allow(work_package).to receive(:destroyed?).and_return(destroyed_result)

    mock_permissions_for(user) do |mock|
      mock.allow_in_project :delete_work_packages, project: work_package.project
    end
  end

  it 'destroys the work package' do
    subject
  end

  it 'is successful' do
    expect(subject)
      .to be_success
  end

  it 'returns the destroyed work package' do
    expect(subject.result)
      .to eql work_package
  end

  it 'returns an empty errors array' do
    expect(subject.errors)
      .to be_empty
  end

  context 'when the work package could not be destroyed' do
    let(:destroyed_result) { false }

    it 'is no success' do
      expect(subject)
        .not_to be_success
    end
  end

  context 'with ancestors' do
    let(:parent) do
      build_stubbed(:work_package)
    end
    let(:grandparent) do
      build_stubbed(:work_package)
    end
    let(:expect_inherited_attributes_service_calls) do
      inherited_service_instance = double(WorkPackages::UpdateAncestorsService)

      service_result = ServiceResult.success(result: work_package)

      service_result.dependent_results += [ServiceResult.success(result: parent),
                                           ServiceResult.success(result: grandparent)]

      expect(WorkPackages::UpdateAncestorsService)
        .to receive(:new)
        .with(user:,
              work_package:)
        .and_return(inherited_service_instance)

      expect(inherited_service_instance)
        .to receive(:call)
        .with(work_package.attributes.keys.map(&:to_sym))
        .and_return(service_result)
    end
    let(:expect_no_inherited_attributes_service_calls) do
      expect(WorkPackages::UpdateAncestorsService)
        .not_to receive(:new)
    end

    it 'calls the inherit attributes service for each ancestor' do
      expect_inherited_attributes_service_calls

      subject
    end

    context 'when the work package could not be destroyed' do
      let(:destroyed_result) { false }

      it 'does not call inherited attributes service' do
        expect_no_inherited_attributes_service_calls

        subject
      end
    end
  end

  context 'with descendants' do
    let(:child) do
      build_stubbed(:work_package)
    end
    let(:grandchild) do
      build_stubbed(:work_package)
    end
    let(:descendants) do
      [child, grandchild]
    end

    before do
      allow(work_package)
        .to receive(:descendants)
        .and_return(descendants)

      descendants.each do |descendant|
        allow(descendant)
          .to receive(:destroy)
      end
    end

    it 'destroys the descendants' do
      descendants.each do |descendant|
        expect(descendant)
          .to receive(:destroy)
      end

      subject
    end

    it 'returns the descendants as part of the result' do
      subject

      expect(subject.all_results)
        .to match_array [work_package] + descendants
    end

    context 'if the work package could not be destroyed' do
      let(:destroyed_result) { false }

      it 'does not destroy the descendants' do
        descendants.each do |descendant|
          expect(descendant)
            .not_to receive(:destroy)
        end

        subject
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class WorkPackages::ScheduleDependency::Dependency
  def initialize(work_package, schedule_dependency)
    self.work_package = work_package
    self.schedule_dependency = schedule_dependency
  end

  attr_accessor :work_package,
                :schedule_dependency

  # Returns the work package ids that this work package directly depends on to
  # determine its own dates. This is used for the order of the dates
  # computations.
  #
  # The dates of a work package depend on its descendants and predecessors
  # dates.
  def dependent_ids
    @dependent_ids ||= (descendants + moving_predecessors).map(&:id).uniq
  end

  def moving_predecessors
    @moving_predecessors ||= follows_relations
      .map(&:to)
      .filter { |predecessor| schedule_dependency.moving?(predecessor) }
  end

  def soonest_start_date
    @soonest_start_date ||=
      follows_relations
        .filter_map(&:successor_soonest_start)
        .max
  end

  def start_date
    descendants_dates.min
  end

  def due_date
    descendants_dates.max
  end

  def has_descendants?
    descendants.any?
  end

  private

  def descendants
    schedule_dependency.descendants(work_package)
  end

  def follows_relations
    schedule_dependency.follows_relations(work_package)
  end

  def descendants_dates
    descendants.filter_map(&:due_date) + descendants.filter_map(&:start_date)
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'rails_helper'

# Scenario: a work package has been moved on the calendar. The moved work
# package has children, parents, followers, and/or predecessors. The
# +ScheduleDependency+ created for the moved work package will have one
# +Dependency+ instance per work package that may need to change due to the
# move. These dependencies are the subjects under test.
RSpec.describe WorkPackages::ScheduleDependency::Dependency do
  subject(:dependency) { dependency_for(work_package_used_in_dependency) }

  create_shared_association_defaults_for_work_package_factory

  shared_let(:work_package) { create(:work_package, subject: 'moved') }

  let(:schedule_dependency) { WorkPackages::ScheduleDependency.new(work_package) }

  def dependency_for(work_package)
    dependency = schedule_dependency.dependencies[work_package]
    if dependency.nil?
      available = schedule_dependency.dependencies.keys.map(&:subject).map(&:inspect).to_sentence
      raise ArgumentError, "Unable to find dependency for work package #{work_package.subject.inspect}; " \
                           "ScheduleDependency instance has dependencies for work packages #{available}"
    end

    dependency
  end

  def create_predecessor_of(work_package, **attributes)
    create(:work_package, subject: "predecessor of #{work_package.subject}", **attributes).tap do |predecessor|
      create(:follows_relation, from: work_package, to: predecessor)
    end
  end

  def create_follower_of(work_package, **attributes)
    create(:work_package, subject: "follower of #{work_package.subject}", **attributes).tap do |follower|
      create(:follows_relation, from: follower, to: work_package)
    end
  end

  def create_parent_of(work_package)
    create(:work_package, subject: "parent of #{work_package.subject}").tap do |parent|
      work_package.update(parent:)
    end
  end

  def create_child_of(work_package)
    create(:work_package, subject: "child of #{work_package.subject}", parent: work_package)
  end

  describe '#dependent_ids' do
    context 'when the work_package has a follower' do
      let!(:follower) { create_follower_of(work_package) }

      context 'for dependency of the follower' do
        let(:work_package_used_in_dependency) { follower }

        it 'returns an array with the work package id' do
          expect(subject.dependent_ids).to eq([work_package.id])
        end
      end
    end

    context 'when the work_package has a parent' do
      let!(:parent) { create_parent_of(work_package) }

      context 'for dependency of the parent' do
        let(:work_package_used_in_dependency) { parent }

        it 'returns an array with the work package id' do
          expect(subject.dependent_ids).to eq([work_package.id])
        end
      end
    end

    context 'when the work_package has a follower which has a child' do
      let!(:follower) { create_follower_of(work_package) }
      let!(:follower_child) { create_child_of(follower) }

      context 'for dependency of the child' do
        let(:work_package_used_in_dependency) { follower_child }

        it 'returns an array with the work_package id' do
          expect(subject.dependent_ids).to eq([work_package.id])
        end
      end

      context 'for dependency of the follower' do
        let(:work_package_used_in_dependency) { follower }

        it 'returns an array with the work_package id and the follower child id' do
          expect(subject.dependent_ids).to contain_exactly(work_package.id, follower_child.id)
        end
      end
    end

    context 'when the work_package has multiple parents and followers' do
      let!(:first_follower) { create_follower_of(work_package) }
      let!(:second_follower) { create_follower_of(work_package) }
      let!(:first_follower_parent) { create_parent_of(first_follower) }
      let!(:first_follower_grandparent) { create_parent_of(first_follower_parent) }

      context 'for dependency of the first follower parent' do
        let(:work_package_used_in_dependency) { first_follower_parent }

        it 'returns an array with the work_package and the first follower ids' do
          expect(subject.dependent_ids).to contain_exactly(work_package.id, first_follower.id)
        end
      end

      context 'for dependency of the first follower grandparent' do
        let(:work_package_used_in_dependency) { first_follower_grandparent }

        it 'returns an array with the work_package, the first follower, and the first follower parent ids' do
          expect(subject.dependent_ids).to contain_exactly(work_package.id, first_follower.id, first_follower_parent.id)
        end
      end

      context 'for dependency of the second follower' do
        let(:work_package_used_in_dependency) { second_follower }

        it 'returns an array with the work_package id' do
          expect(subject.dependent_ids).to contain_exactly(work_package.id)
        end
      end
    end

    context 'with more complex relations' do
      context 'when has two consecutive followers' do
        let!(:follower) { create_follower_of(work_package) }
        let!(:follower_follower) { create_follower_of(follower) }

        context 'for dependency of the first follower' do
          let(:work_package_used_in_dependency) { follower }

          it 'returns an array with the work_package id' do
            expect(subject.dependent_ids).to contain_exactly(work_package.id)
          end
        end

        context 'for dependency of the second follower' do
          let(:work_package_used_in_dependency) { follower_follower }

          it 'returns an array with only the first follower id' do
            expect(subject.dependent_ids).to contain_exactly(follower.id)
          end
        end
      end

      context 'when has a follower which has a predecessor' do
        let!(:follower) { create_follower_of(work_package) }
        let!(:follower_predecessor) { create_predecessor_of(follower) }

        context 'for dependency of the follower' do
          let(:work_package_used_in_dependency) { follower }

          it 'returns an array with the work_package id' do
            expect(subject.dependent_ids).to contain_exactly(work_package.id)
          end
        end
      end

      context 'when has a predecessor which has a parent and a child' do
        let!(:follower) { create_follower_of(work_package) }
        let!(:follower_parent) { create_parent_of(follower) }
        let!(:follower_child) { create_child_of(follower) }

        context 'for dependency of the follower child' do
          let(:work_package_used_in_dependency) { follower_child }

          it 'returns an array with the work_package id' do
            expect(subject.dependent_ids).to contain_exactly(work_package.id)
          end
        end

        context 'for dependency of the follower parent' do
          let(:work_package_used_in_dependency) { follower_parent }

          it 'returns an array with the work_package, the follower, and the follower child ids' do
            expect(subject.dependent_ids).to contain_exactly(work_package.id, follower.id, follower_child.id)
          end
        end
      end
    end
  end

  describe '#soonest_start_date' do
    let(:work_package_used_in_dependency) { work_package }

    before do
      work_package.update(due_date: Time.zone.today)
    end

    context 'with a moved predecessor' do
      it 'returns the soonest start date from the predecessors' do
        follower = create_follower_of(work_package)
        expect(dependency_for(follower).soonest_start_date).to eq(work_package.due_date + 1.day)
      end
    end

    context 'with an unmoved predecessor' do
      it 'returns the soonest start date from the predecessors' do
        follower = create_follower_of(work_package)
        unmoved_follower_predecessor = create_predecessor_of(follower, due_date: Time.zone.today + 4.days)
        expect(dependency_for(follower).soonest_start_date).to eq(unmoved_follower_predecessor.due_date + 1.day)
      end
    end

    context 'with non working days' do
      let!(:tomorrow_we_do_not_work!) { create(:non_working_day, date: Time.zone.tomorrow) }

      it 'returns the soonest start date being a working day' do
        follower = create_follower_of(work_package)
        expect(dependency_for(follower).soonest_start_date).to eq(work_package.due_date + 2.days)
      end
    end
  end
end