# frozen_string_literal: true

require 'nokogiri'
require 'active_support/core_ext/enumerable'

module Gitlab
  module QA
    module Report
      # Uses the API to create or update GitLab issues with the results of tests from RSpec report files.
      class ResultsInIssues < ReportAsIssue
        private

        RESULTS_SECTION_TEMPLATE = "\n\n### DO NOT EDIT BELOW THIS LINE\n\nActive and historical test results:"

        def run!
          puts "Reporting test results in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."

          test_results_per_file do |test_results|
            puts "Reporting tests in #{test_results.path}"

            test_results.each do |test|
              puts "Reporting test: #{test.file} | #{test.name}\n"

              report_test(test) unless test.skipped
            end

            test_results.write
          end
        end

        def report_test(test)
          testcase = find_testcase(test) || create_testcase(test)
          test.testcase ||= testcase.web_url.sub('/issues/', '/quality/test_cases/')

          issue = find_linked_results_issue_by_iid(testcase, test)

          if issue
            issue = update_issue_title(issue, test, 'issue') if issue.title.strip != title_from_test(test)
          else
            puts "No valid issue link found"
            issue = find_or_create_results_issue(test)

            add_issue_to_testcase(testcase, issue)
          end

          update_labels(testcase, test)
          update_issue(issue, test)
        end

        def find_testcase(test)
          testcase = find_testcase_by_iid(test)

          if testcase
            testcase = update_issue_title(testcase, test, 'test_case') if testcase.title.strip != title_from_test(test)
          else
            testcase = find_issue(test, 'test_case')
          end

          testcase
        end

        def find_testcase_by_iid(test)
          iid = testcase_iid_from_url(test.testcase)

          return unless iid

          find_issue_by_iid(iid, 'test_case')
        end

        def find_linked_results_issue_by_iid(testcase, test)
          iid = issue_iid_from_testcase(testcase)

          return unless iid

          find_issue_by_iid(iid, 'issue')
        end

        def find_issue_by_iid(iid, issue_type)
          issues = gitlab.find_issues(iid: iid) do |issue|
            issue.state == 'opened' && issue.issue_type == issue_type
          end

          warn(%(#{issue_type} iid "#{iid}" not valid)) if issues.empty?

          issues.first
        end

        def update_issue_title(issue, test, issue_type)
          warn(%(#{issue_type} title needs to be updated from '#{issue.title.strip}' to '#{title_from_test(test)}'))

          gitlab.edit_issue(iid: issue.iid, options: { title: title_from_test(test) })
        end

        def create_testcase(test)
          title = title_from_test(test)
          puts "Creating test case '#{title}' ..."

          gitlab.create_issue(
            title: title,
            description: new_testcase_description(test),
            labels: new_issue_labels(test),
            issue_type: 'test_case'
          )
        end

        def testcase_iid_from_url(url)
          return warn(%(\nPlease update #{url} to test case url")) if url&.include?('/-/issues/')

          url && url.split('/').last.to_i
        end

        def new_testcase_description(test)
          "#{new_issue_description(test)}#{RESULTS_SECTION_TEMPLATE}"
        end

        def issue_iid_from_testcase(testcase)
          results = testcase.description.partition(RESULTS_SECTION_TEMPLATE).last if testcase.description.include?(RESULTS_SECTION_TEMPLATE)

          return puts "No issue link found" unless results

          issue_iid = results.split('/').last

          issue_iid&.to_i
        end

        def find_or_create_results_issue(test)
          issue = find_issue(test, 'issue')

          if issue
            puts "Found existing issue: #{issue.web_url}"
          else
            issue = create_issue(test)
            puts "Created new issue: #{issue.web_url}"
          end

          issue
        end

        def find_issue(test, issue_type)
          issues = gitlab.find_issues(options: { search: search_term(test) }) do |issue|
            issue.state == 'opened' && issue.issue_type == issue_type && issue.title.strip == title_from_test(test)
          end

          warn(%(Too many #{issue_type}s found with the file path "#{test.file}" and name "#{test.name}")) if issues.many?

          issues.first
        end

        def add_issue_to_testcase(testcase, issue)
          results_section = testcase.description.include?(RESULTS_SECTION_TEMPLATE) ? '' : RESULTS_SECTION_TEMPLATE

          gitlab.edit_issue(iid: testcase.iid, options: { description: (testcase.description + results_section + "\n\n#{issue.web_url}") })

          puts "Added issue #{issue.web_url} to testcase #{testcase.web_url}"
        end

        def update_issue(issue, test)
          new_labels = issue_labels(issue)
          new_labels |= ['Testcase Linked']

          labels_updated = update_labels(issue, test, new_labels)
          note_posted = note_status(issue, test)

          if labels_updated || note_posted
            puts "Issue updated."
          else
            puts "Test passed, no update needed."
          end
        end

        def new_issue_labels(test)
          ['Quality', "devops::#{test.stage}", 'status::automated']
        end

        def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
          labels = super
          labels |= new_issue_labels(test).to_set
          labels.delete_if { |label| label.start_with?("#{pipeline}::") }
          labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
        end

        def search_term(test)
          %("#{partial_file_path(test.file)}" "#{search_safe(test.name)}")
        end

        def note_status(issue, test)
          return false if test.skipped
          return false if test.failures.empty?

          note = note_content(test)

          gitlab.find_issue_discussions(iid: issue.iid).each do |discussion|
            return gitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid, discussion_id: discussion.id, body: failure_summary) if new_note_matches_discussion?(note, discussion)
          end

          gitlab.create_issue_note(iid: issue.iid, note: note)

          true
        end

        def note_content(test)
          errors = test.failures.each_with_object([]) do |failure, text|
            text << <<~TEXT
              Error:
              ```
              #{failure['message']}
              ```

              Stacktrace:
              ```
              #{failure['stacktrace']}
              ```
            TEXT
          end.join("\n\n")

          "#{failure_summary}\n\n#{errors}"
        end

        def failure_summary
          summary = [":x: ~\"#{pipeline}::failed\""]
          summary << "~\"quarantine\"" if quarantine_job?
          summary << "in job `#{Runtime::Env.ci_job_name}` in #{Runtime::Env.ci_job_url}"
          summary.join(' ')
        end

        def new_note_matches_discussion?(note, discussion)
          note_error = error_and_stack_trace(note)
          discussion_error = error_and_stack_trace(discussion.notes.first['body'])

          return false if note_error.empty? || discussion_error.empty?

          note_error == discussion_error
        end

        def error_and_stack_trace(text)
          text.strip[/Error:(.*)/m, 1].to_s
        end
      end
    end
  end
end