lib/lazylead/task/propagate_down.rb
# frozen_string_literal: true

# The MIT License
#
# Copyright (c) 2019-2022 Yurii Dubinka
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom
# the Software is  furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.

require_relative "../log"
require_relative "../opts"
require_relative "../version"
require_relative "../system/jira"

module Lazylead
  module Task
    #
    # Propagate particular JIRA fields from parent ticket to sub-tasks.
    #
    # The task algorithm:
    #  - fetch parent issues from remote ticketing system by JQL
    #  - reject issues without sub-tasks
    #  - fetch sub-tasks details
    #  - make a diff for pre-defined fields provided by the user
    #  - apply diff to sub-tasks
    #  - make a comment to sub-task with clarification.
    class PropagateDown
      def initialize(log = Log.new)
        @log = log
      end

      # @todo #/DEV Define a new module Lazylead::Task with basic methods like
      #  split, groupBy(assignee, reporter, etc), blank?
      def run(sys, _, opts)
        fields = opts.slice("propagate", ",")
        sys.issues(
          opts["jql"],
          {
            expand: "changelog",
            max_results: opts.fetch("max_results", 50),
            fields: ["subtasks"] + opts.jira_fields
          }
        )
           .map { |i| Parent.new(i, sys, fields) }
           .select(&:subtasks?)
           .each(&:fetch)
           .each(&:propagate)
      end
    end

    # The parent ticket as a source for propagation to children.
    class Parent
      def initialize(issue, sys, fields)
        @issue = issue
        @sys = sys
        @fields = fields
      end

      # Ensure that parent ticket has sub-tasks.
      def subtasks?
        !@issue.fields["subtasks"].empty?
      end

      # Take sub-tasks with their fields from external JIRA system.
      def fetch
        @subtasks = @issue.fields["subtasks"]
                          .map do |sub|
          @sys.raw { |j| j.Issue.find(sub["id"]) }
        end
      end

      # Fill pre-defined fields for sub-tasks from parent ticket
      #  and post comment to ticket with clarification.
      def propagate
        expected = @fields.to_h { |f| [f, @issue.fields[f]] }
        @subtasks.each do |subtask|
          actual = @fields.to_h { |f| [f, subtask.fields[f]] }
          diff = diff(expected, actual)
          next if diff.empty?
          subtask.save(fields: diff)
          subtask.comments.build.save!(body: comment(diff))
        end
      end

      # Detect difference between fields in parent ticket and sub-task.
      # The sub-tasks expected be updated by this diff.
      def diff(expected, actual)
        diff = {}
        actual.each_with_object(diff) do |a, d|
          k = a.first
          v = a.last
          d[k] = expected[k] if v.nil? || v.blank?
          next if v.nil?
          d[k] = "#{v},#{expected[k]}" unless v.to_s.include? expected[k]
        end
      end

      # Build a jira comment in markdown(*.md) format with diff table.
      def comment(diff)
        markdown = [
          "The following fields were propagated from #{@issue.key}:",
          "||Field||Value||"
        ]
        diff.each { |k, v| markdown << "|#{k}|#{v}|" }
        markdown << "Posted by [lazylead v#{Lazylead::VERSION}|" \
                    "https://bit.ly/2NjdndS]"
        markdown.join("\r\n")
      end
    end
  end
end
