"use strict";
/*
 * Copyright © 2020 Atomist, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeAutoInspects = void 0;
const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const slack_messages_1 = require("@atomist/slack-messages");
const _ = require("lodash");
const skillContext_1 = require("../../api/context/skillContext");
const PushImpactListenerRegistration_1 = require("../../api/registration/PushImpactListenerRegistration");
const ReviewerError_1 = require("../../api/registration/ReviewerError");
const types_1 = require("../../typings/types");
const minimalClone_1 = require("../goal/minimalClone");
const messages_1 = require("../misc/slack/messages");
const createPushImpactListenerInvocation_1 = require("./createPushImpactListenerInvocation");
const relevantCodeActions_1 = require("./relevantCodeActions");
/**
 * Execute auto inspections and route or react to review results using review listeners
 * @param autoInspectRegistrations
 * @param reviewListeners listeners to respond to reviews
 * @return {ExecuteGoal}
 */
function executeAutoInspects(options) {
    return async (goalInvocation) => {
        const { goalEvent, configuration, credentials, id, context } = goalInvocation;
        try {
            if (options.registrations.length === 0) {
                return { code: 0, description: "No code inspections configured", requireApproval: false };
            }
            logger_1.logger.debug("Planning inspection of %j with %d AutoInspects", id, options.registrations.length);
            return configuration.sdm.projectLoader.doWithProject({
                credentials,
                id,
                context,
                readOnly: true,
                cloneOptions: options.cloneOptions ? options.cloneOptions : minimalClone_1.minimalClone(goalEvent.push, { detachHead: true }),
            }, applyCodeInspections(goalInvocation, options));
        }
        catch (err) {
            logger_1.logger.debug("Error executing review of %j with %d reviewers: %s", id, options.registrations.length, err.message);
            logger_1.logger.debug(err.stack);
            return HandlerResult_1.failure(err);
        }
    };
}
exports.executeAutoInspects = executeAutoInspects;
// tslint:disable
/**
 * each inspection can return a result, which may be turned into a PushImpactResponse by its onInspectionResult,
 * OR it may return a ProjectReview, which will be processed by each ProjectReviewListener. The Listener may also return a PushImpactResponse.
 * Each of these PushReactionResponses may instruct the AutoInspect goal to fail or to require approval.
 *
 * ROD: which of these paths is deprecated? I'm guessing the ReviewListener is deprecated
 * and the onInspectionResult is the way to do this in the future.
 *
 * ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼     per AutoInspectRegistration      ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼              ▽▽▽▽▽▽▽▽  per Listener  ▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽
 *                         ┌────────────────────┐                                                                           ┌────────────────────┐
 *                         │                    │                                                                           │                    │
 *                         │                    │                              is a                                         │                    │
 *                         │                    │                         ProjectReview?                                    │                    │
 *                         │                    │                               Λ                                           │                    │
 *  ┌───────────┐          │                    │           ┌──────────┐       ╱ ╲     ┌────────────┐      consolidate      │      Listener      │          ┌────────────────────┐
 *  │  Project  │─────────▶│     Inspection     │────┬─────▶│   any    │─────▶▕   ▏───▶│   Review   │═════▶ with other ────▶│                    │─────┬───▶│PushImpactResponse│════╗
 *  └───────────┘          │                    │    │      └──────────┘       ╲ ╱     └────────────┘        Reviews        │                    │          └────────────────────┘    ║
 *                         │                    │    │                          V                                           │                    │     │                              ║
 *                         │                    │    │                                                                      │                    │                                    ║
 *                         │                    │    │                                                                      │                    │     │                              ║
 *                         │                    │    │                                                                      └────────────────────┘                                    ║
 *                         └────────────────────┘    │                                                                                 │               │                              ║
 *                                    │              ?     ┌────────────┐                                                                                                             ║
 *                                                   │     │            │                                                              └ ─ ─ ─ "fail"─ ┘                              ║
 *                                    │              │     │            │                                                                      and send to Slack                      ║     ┌──────────────────────────┐
 *                                                   │     │OnInspection│        ┌────────────────────┐                                                                               ║     │     check for "fail"     │      ┌──────────────────┐
 *                                    │              └────▶│   Result   │────?──▶│PushResponseResponse│═══════════════════════════════════════════════════════════════════════════════╩════▶│    check for "require    │─────▶│ExecuteGoalResult │
 *                                                         │            │        └────────────────────┘                                                                                     │        approval"         │      └──────────────────┘
 *                                    │                    │            │                                                                                                                   └──────────────────────────┘
 *                                                         │            │
 *                                    │                    └────────────┘
 *                                                                │
 *                                    │
 *                                    ▼                           │
 *                              ┌──────────┐                      ▼
 *                              │  Error   │                (errors are
 *                              └──────────┘                  ignored)
 */
// tslint:enable
/**
 * Apply code inspections
 * @param goalInvocation
 * @param autoInspectRegistrations
 * @param reviewListeners
 */
function applyCodeInspections(goalInvocation, options) {
    return async (project) => {
        const { addressChannels, progressLog } = goalInvocation;
        const cri = await createPushImpactListenerInvocation_1.createPushImpactListenerInvocation(goalInvocation, project);
        const relevantAutoInspects = await relevantCodeActions_1.relevantCodeActions(options.registrations, cri);
        const inspectionReviewsAndResults = await Promise.all(relevantAutoInspects
            .map(async (autoInspect) => {
            const cli = createParametersInvocation(goalInvocation, autoInspect);
            const papi = Object.assign(Object.assign({}, cli), { push: cri, progressLog });
            try {
                goalInvocation.progressLog.write("Running inspection " + autoInspect.name + "...");
                const inspectionResult = await autoInspect.inspection(project, papi);
                if (isProjectReview(inspectionResult)) {
                    goalInvocation.progressLog.write(describeProjectReview(autoInspect.name, inspectionResult));
                }
                const review = isProjectReview(inspectionResult) ? inspectionResult : undefined;
                const response = autoInspect.onInspectionResult &&
                    await autoInspect.onInspectionResult(inspectionResult, cli).catch(err => {
                        goalInvocation.progressLog.write("Warning: onInspectionResult failed: " + err.message);
                    }); // ignore errors
                return { review, response };
            }
            catch (error) {
                goalInvocation.progressLog.write(`Error running inspection ${autoInspect.name}: ` + error.message);
                return { error };
            }
        }));
        const reviewerErrors = inspectionReviewsAndResults.filter(e => !!e.error)
            .map(e => e.error);
        // tslint:disable-next-line:no-boolean-literal-compare
        if (options.reportToSlack === true) {
            await sendErrorsToSlack(reviewerErrors, addressChannels);
        }
        const responsesFromOnInspectionResult = inspectionReviewsAndResults.filter(r => !!r.response)
            .map(r => r.response);
        const reviews = inspectionReviewsAndResults.filter(r => !!r.review)
            .map(r => r.review);
        const responsesFromReviewListeners = await gatherResponsesFromReviewListeners(goalInvocation.progressLog, reviews, options.listeners, cri);
        const reviewCommentCount = _.flatten((reviews || []).map(r => r.comments || [])).length;
        const allReviewResponses = responsesFromOnInspectionResult.concat(responsesFromReviewListeners);
        const result = {
            code: allReviewResponses.some(rr => !!rr && rr === PushImpactListenerRegistration_1.PushImpactResponse.failGoals) ? 1 : 0,
            state: allReviewResponses.some(rr => !!rr && rr === PushImpactListenerRegistration_1.PushImpactResponse.requireApprovalToProceed)
                ? types_1.SdmGoalState.waiting_for_approval : undefined,
            description: reviewCommentCount > 0 ?
                `Code inspections raised ${reviewCommentCount} review ${reviewCommentCount > 1 ? "comments" : "comment"}` : undefined,
        };
        logger_1.logger.debug("Review responses are %j, result=%j", responsesFromReviewListeners, result);
        return result;
    };
}
function describeProjectReview(inspectionName, pr) {
    const commentStrings = pr.comments.map(describeComment);
    const allComments = commentStrings.length === 0 ? "" : ":\n" + commentStrings.join("\n");
    return `${pr.comments.length} comments on ${pr.repoId.owner}/${pr.repoId.repo} by ${inspectionName}` + allComments;
}
function describeComment(c) {
    let category = c.category;
    if (c.subcategory) {
        category += "/" + c.subcategory;
    }
    const location = c.sourceLocation ? `${c.sourceLocation.path}#${c.sourceLocation.lineFrom1}: ` : "";
    return `  - [${c.severity} ${category} ${location}${c.detail}]`;
}
async function gatherResponsesFromReviewListeners(progressLog, reviews, reviewListeners, pli) {
    const review = consolidate(reviews, pli.id);
    logger_1.logger.debug("Consolidated review of %j has %s comments", pli.id, review.comments.length);
    return Promise.all(reviewListeners.map(responseFromOneListener(progressLog, Object.assign(Object.assign({}, pli), { review }))));
}
function responseFromOneListener(progressLog, rli) {
    return async (l) => {
        try {
            progressLog.write(`Running review listener ${l.name}...`);
            const result = (await l.listener(rli)) || PushImpactListenerRegistration_1.PushImpactResponse.proceed;
            progressLog.write(`Review listener + ${l.name} result: ` + result);
            return result;
        }
        catch (err) {
            logger_1.logger.error("Review listener %s failed. Stack: %s", l.name, err.stack);
            progressLog.write(`Review listener ${l.name} error: ` + err.message);
            progressLog.write(`Failing autoinspect goal because a review listener failed.`);
            await rli.addressChannels(messages_1.slackErrorMessage("Review Listener", `Review listener ${slack_messages_1.italic(l.name)} failed${err.message ? `:
${slack_messages_1.codeBlock(err.message)}` : ""}`, rli.context));
            return PushImpactListenerRegistration_1.PushImpactResponse.failGoals;
        }
    };
}
function createParametersInvocation(goalInvocation, autoInspect) {
    return {
        addressChannels: goalInvocation.addressChannels,
        preferences: goalInvocation.preferences,
        context: goalInvocation.context,
        configuration: goalInvocation.configuration,
        credentials: goalInvocation.credentials,
        parameters: autoInspect.parametersInstance,
        skill: skillContext_1.createSkillContext(goalInvocation.context),
    };
}
function isProjectReview(o) {
    const r = o;
    return !!r.repoId && r.comments !== undefined;
}
function consolidate(reviews, repoId) {
    return {
        repoId,
        comments: _.flatten(reviews.map(review => review.comments)),
    };
}
async function sendErrorsToSlack(errors, addressChannels) {
    await Promise.all(errors.map(async (e) => addressChannels(ReviewerError_1.formatReviewerError(e))));
}
//# sourceMappingURL=executeAutoInspects.js.map