import JiraApiUtil from "@impl/api/JiraApiUtil";
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult } from "@playwright/test/reporter";
import { formatDate, formatDateForFilename } from "@impl/utils/DateUtils";
import { GetIssueResponse, JiraIssue, JiraIssueFields } from "@business-logic/types/JiraApi";
import OrtoniReport, { OrtoniReportConfig } from "ortoni-report";

let jiraCardNumber = process.env.JIRA_CARD_NUMBER || '';
const isRegressionRun = process.env.IS_REGRESSION === 'true'? true: false;
const isCreateBugSubtasks = !isRegressionRun;   // DO NOT create anything for a regression run. Temporary for this project. In library package, this will be a config option.
const isCreateTestSubtasks = !isRegressionRun;
const jiraProjectKey = process.env.JIRA_PROJECT_KEY!;
const jiraEpicKey = process.env.JIRA_EPIC_KEY!;
const passTransitionId = '111';
const failTransitionId = '101';
const inTestTransitionId = '271';
const currentDate = formatDateForFilename(new Date());

// Ortoni config
const reportName = `ortoni_report_${currentDate}.html`;
const reportConfig: OrtoniReportConfig = {
    port: 1994,
    open: "never",
    folderPath: "ortoni-report",
    filename: reportName,
    logo: 'playwright-tests/business-logic/data/logo.png',
    title: "Test Report",
    showProject: false,
    projectName: "ISS-Nextgen-Playwright-Report",
    testType: `E2E- Environment: ${process.env.NODE_ENV} `,
    preferredTheme: "light",
    base64Image: true,
};

export default class JiraWritebackReporter implements Reporter {
    readonly jiraApiUtil: JiraApiUtil = new JiraApiUtil();
    readonly ortoniReport: OrtoniReport = new OrtoniReport(reportConfig);
    readonly loadIssueCalls: (() => void)[] = [];   // Calls to loadIssue must be deferred because they depend on information from the API.
    readonly issuesToPass: string[] = [];           // Issues to transition to "Pass".
    readonly issuesToFail: string[] = [];           // Issues to transition to "Fail".
    readonly issuesToCreate: JiraIssue[] = [];      // Issues to be batch created.
    existingSubtasks: JiraIssue[] = [];             // Array to hold existing subtasks of the dev card.
    testCaseTypeId: string|undefined = undefined;   // ID of the test case subtask type in Jira. Will be filled by API call.
    bugTypeId:string|undefined = undefined;         // ID of the bug subtask type.
    userStoryTypeId:string|undefined = undefined;
    parentCard: GetIssueResponse | undefined = undefined;   // Variable to hold the parent card. Will be filled by API call.

    // Will need this if we want to move ortoni report upload into this reporter.
    // readonly ortoniReport = new OrtoniReport(reportConfig);

    /**
     * This function loads the IDs for the Jira Issue Types we use.
     * 
     */
    async loadJiraIssueTypes() {
        console.log(`JiraWritebackReporter >> Loading Jira Issue Types for project '${jiraProjectKey}'...`);
        const issueTypesRes = await this.jiraApiUtil.getIssueTypes(jiraProjectKey);
        const issueTypes = issueTypesRes.data;
        this.testCaseTypeId = issueTypes.issueTypes.find(issueType => {
            return issueType.subtask === true && issueType.name === 'Test Case Sub-task'
        })?.id;
        this.bugTypeId = issueTypes.issueTypes.find(issueType => {
            return issueType.subtask === true && issueType.name === 'Bug Sub-task'
        })?.id;
        this.userStoryTypeId = issueTypes.issueTypes.find(issueType => {
            return issueType.name === 'Story';
        })?.id;
        console.log('JiraWritebackReporter >> Loaded issue types.');
    }

    /**
     * This function generates a test subtask based on the test results we pass in.
     * @param test TestCase from onTestEnd()
     * @param result TestResult from onTestEnd()
     * @returns Jira Test Subtask based on test result.
     */
    getCreateTestSubtask(test: TestCase, result: TestResult) {
        if (result.status === 'skipped') {
            return undefined;
        }
        const testStepTitles = result.steps.map(step => {
            return `-\t${step.title}`;
        }).join('\n');
        const allErrors = result.errors.map(value => {
            return value.message
        }).join('\n');

        const description = `Most recent test status: ${result.status}.\nDuration: ${result.duration/1000} seconds.\nErrors:\n${allErrors}`
        const testSubtaskBody: JiraIssueFields = {
            summary: test.title,
            description: {
                content: [
                    {
                        "content": [
                            {
                                "text": description,
                                "type": "text"
                            }
                        ],
                        "type": "paragraph"
                    }
                ],
                "type": "doc",
                "version": 1
            },
            project: {
                key: jiraProjectKey
            },
            issuetype: {
                id: this.testCaseTypeId!
            },
            parent: {
                key: jiraCardNumber
            },
            customfield_14857: {
                type: 'doc',
                version: 1,
                content: [
                    {
                        type: 'paragraph',
                        content: [
                            {
                                type: 'text',
                                text: testStepTitles
                            }
                        ]
                    }
                ]
            }
        };
        return testSubtaskBody;
    }

    /**
     * This function generates a bug subtask based on the test results we pass in.
     * @param test TestCase from onTestEnd()
     * @param result TestResult from onTestEnd()
     * @returns Jira Bug Subtask based on test result.
     */
    getCreateBug(test: TestCase, result: TestResult) {
        if (result.status === 'passed' || result.status === 'skipped') {
            return undefined;
        }
        const allErrors = result.errors.map(value => {
            return value.message
        }).join('\n');

        const bug: JiraIssueFields = {
            summary: `TEST FAILED ${formatDate(new Date())}: ${test.title}`,
            description: {
                content: [
                    {
                        "content": [
                            {
                                "text": allErrors,
                                "type": "text"
                            }
                        ],
                        "type": "paragraph"
                    }
                ],
                "type": "doc",
                "version": 1
            },
            project: {
                key: jiraProjectKey
            },
            issuetype: {
                id: this.bugTypeId!
            },
            parent: {
                key: jiraCardNumber
            }
        };
        return bug;
    }

    /**
     * This function gives us the correct transition for a test subtask based on this test result.
     * @param result TestResult from onTestEnd()
     * @returns Correct transition to pass to the Jira API.
     */
    getSubtaskTransition(result: TestResult): { id: string } | undefined {
        if (result.status === 'skipped') {
            return undefined;
        }

        if (result.status === 'passed') {
            return {
                id: passTransitionId
            };
        } else {
            return {
                id: failTransitionId
            };
        }
    }

    /**
     * 
     * @param cardName 
     * @param sprintId 
     * @returns 
     */
    getCreateRegressionCardRequestBody(cardName: string, sprintId: number) {
        const body: JiraIssueFields = {
            summary: cardName,
            description: {
                content: [
                    {
                        "content": [
                            {
                                "text": 'Card to hold regression test results for this sprint.',
                                "type": "text"
                            }
                        ],
                        "type": "paragraph"
                    }
                ],
                "type": "doc",
                "version": 1
            },
            project: {
                key: jiraProjectKey
            },
            issuetype: { id: this.userStoryTypeId!},
            parent: { key: jiraEpicKey },
            customfield_10007: sprintId,
            customfield_13100: { id: '557058:a314fc5b-aed9-4472-90f8-00f106e06207' }    // Mark UAT Tester as N/A
        }
        return body;
    }

    /**
     * Grabs parent card and existing subtasks.
     */
    async loadReporterData() {
        let parentCard: GetIssueResponse;
        await this.loadJiraIssueTypes();
        if (isRegressionRun) {
            console.log('JiraWritebackReporter >> Searching for current regression card...');
            const sprint = await this.jiraApiUtil.getCurrentSprint();
            if (!sprint) {
                console.error('JiraWritebackReporter >> Could not find current sprint');
            } else {
                console.log(`JiraWritebackReporter >> Found sprint with id '${sprint.id}' called '${sprint.name}'`);
            }
            const regressionCardName = `Playwright Automated Regression Tests ${sprint?.name}`;
            const parentRes = await this.jiraApiUtil.getJqlSearchIssue({ jql: `sprint = ${sprint?.id} and summary ~ "${regressionCardName}"` });
            if (parentRes.data.issues.length > 1) {
                console.log('JiraWritebackReporter >> WARNING: Multiple regression cards found. Using first one.');
                parentCard = (await this.jiraApiUtil.getIssue(parentRes.data.issues[0].id!)).data;
                await this.jiraApiUtil.postTransitionIssue(parentCard.id!, {
                    transition: {
                        id: inTestTransitionId
                    }
                });
            } else if (parentRes.data.issues.length < 1) {
                // NO regression card found. Make one.
                console.log('JiraWritebackReporter >> No regression card was found for this sprint. Creating one.');
                const fields = this.getCreateRegressionCardRequestBody(regressionCardName, sprint!.id);
                const regressionCard: JiraIssue = {
                    fields: fields,
                    transition: {
                        id: inTestTransitionId
                    }
                };
                const createRes = await this.jiraApiUtil.postCreateIssue(regressionCard);
                regressionCard.id = createRes.data.id;
                regressionCard.key = createRes.data.key;
                parentCard = regressionCard;
            } else {
                // Found one
                console.log('JiraWritebackReporter >> Found one regression card. Using it.');
                parentCard = (await this.jiraApiUtil.getIssue(parentRes.data.issues[0].id!)).data;
                await this.jiraApiUtil.postTransitionIssue(parentCard.id!, {
                    transition: {
                        id: inTestTransitionId
                    }
                });
            }
            jiraCardNumber = parentCard.key!;
            console.log(`JiraWritebackReporter >> Retrieved current regression card: ${parentCard.key}: ${parentCard.fields.summary}`);
        } else {
            console.log('JiraWritebackReporter >> Searching for parent card...');
            const parentRes = await this.jiraApiUtil.getIssue(jiraCardNumber);
            parentCard = parentRes.data;
            console.log(`JiraWritebackReporter >> Retrieved parent card: ${parentCard.key}: ${parentCard.fields.summary}`);
        }

        console.log(`JiraWritebackReporter >> Searching for all subtasks of parent card '${jiraCardNumber}'...`);
        const subtaskIds = parentCard.fields.subtasks?.map(subtask => {
            return subtask.id;
        });

        if (subtaskIds && subtaskIds.length > 0) {
            for (let i = 0; i < subtaskIds.length; i+=50) {
                const batch = subtaskIds.slice(i, i+50);
                const subTasksRes = await this.jiraApiUtil.postBulkFetchIssues({ issueIdsOrKeys: batch });
                this.existingSubtasks.push(...subTasksRes.data.issues);
            }
            console.log('JiraWritebackReporter >> Subtasks retrieved.');
        } else {
            console.log('JiraWritebackReporter >> No subtasks were found.');
        }

    }

    /**
     * Bulk creates test cases and bugs in batches of up to 50. Bulk transitions existing test case subtasks.
     */
    async writeResults() {
        for (let i = 0; i < this.issuesToCreate.length; i+=50) {
            const batch = this.issuesToCreate.slice(i, i + 50);
            console.log('JiraWritebackReporter >> Creating issue batch...');
            // fire request
            await this.jiraApiUtil.postBulkCreateIssue({ issueUpdates: batch });
            console.log('JiraWritebackReporter >> Issue batch created.');
        }

        if (this.issuesToPass.length > 0) {
            console.log('JiraWritebackReporter >> Transitioning passed test cases to "Pass"...')
            await this.jiraApiUtil.postBulkTransitionIssues( {
                bulkTransitionInputs: [{
                    selectedIssueIdsOrKeys: this.issuesToPass, 
                    transitionId: passTransitionId
                }], 
                sendBulkNotification: false 
            });
            console.log('JiraWritebackReporter >> Transition success.');
        }

        if (this.issuesToFail.length > 0) {
            console.log('JiraWritebackReporter >> Transitioning failed test cases to "Fail"...')
            await this.jiraApiUtil.postBulkTransitionIssues( {
                bulkTransitionInputs: [{
                    selectedIssueIdsOrKeys: this.issuesToFail,
                    transitionId: failTransitionId
                }],
                sendBulkNotification: false
            });
            console.log('JiraWritebackReporter >> Transition success.')
        }
    }

    /**
     * Builds and loads appropriate response based on test result. This can be a test subtask/bug subtask or a call to transition a test subtask.
     * @param test TestCase from onTestEnd()
     * @param result TestResult from onTestEnd()
     * @returns Void
     */
    loadIssue(test: TestCase, result: TestResult) {
        if (result.status === 'skipped') return;
        if (result.status !== 'passed' && result.retry < test.retries) return;  // Skip if test case failed and this isn't the last retry.

        let existingTestSubtask: JiraIssue | undefined = undefined;
        let existingBug: JiraIssue | undefined = undefined;
        const subtaskTransition = this.getSubtaskTransition(result)!;   // Get either a Pass or Fail transition depending on test results.

        console.log('JiraWritebackReporter >> Checking for existing subtasks for this test...');
        for (const card of this.existingSubtasks) {
            if (card.fields.issuetype?.id === this.testCaseTypeId) {
                if (card.fields.summary === test.title) {
                    existingTestSubtask = card;
                    console.log('JiraWritebackReporter >> Found existing test subtask.');
                }
            } else if (card.fields.issuetype?.id === this.bugTypeId) {
                if (new RegExp(`^TEST FAILED [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]: ${test.title}$`).test(card.fields.summary)) {
                    existingBug = card;
                    console.log('JiraWritebackReporter >> Found existing bug.');
                }
            }
        }
        if (!(existingTestSubtask || existingBug)) {
            console.log('JiraWritebackReporter >> No existing test subtask or bug was found.')
        }

        const subTask = this.getCreateTestSubtask(test, result);    // Will return undefined if test status is 'skipped'
        const bug = this.getCreateBug(test, result);    // Will return undefined if we don't need one
        if (subTask && isCreateTestSubtasks) {
            // Create or Edit Subtask
            if (existingTestSubtask) {
                if (result.status === 'passed') {
                    this.issuesToPass.push(existingTestSubtask.key!);
                } else {
                    this.issuesToFail.push(existingTestSubtask.key!);
                }
            } else {
                this.issuesToCreate.push({
                    fields: subTask,
                    transition: subtaskTransition
                });
            }
        }

        if (bug && isCreateBugSubtasks) {
            // Create or Edit Bug
            if (existingBug) {
                // Leave it alone. This reporter should not modify existing bugs.
            } else {
                this.issuesToCreate.push({
                    fields: bug
                });
            }
        }
    }

    /**
     * This function loads calls to loadIssue() in an array to be executed when the necessary information from the Jira API is available.
     * @param test 
     * @param result 
     */
    onTestEnd(test: TestCase, result: TestResult) {
        this.ortoniReport.onTestEnd(test, result);
        if (isCreateBugSubtasks || isCreateTestSubtasks) {  // IF any type of subtasks should be created/modified, call loadIssue(). Otherwise, do not call it.
            this.loadIssueCalls.push(() => { this.loadIssue(test, result); });
        }
    }

    /**
     * Makes calls to load and execute API calls to Jira.
     * @param result 
     * @returns Promise to write results to Jira.
     */
    onEnd(result: FullResult): Promise<{ status?: FullResult["status"]; } | undefined | void>|void {
        return this.ortoniReport.onEnd(result).then(async () => {
            await this.loadReporterData().then(async () => {
                this.loadIssueCalls.map(fn => fn());
                await this.writeResults();
            }).then(async () => {
                console.log('JiraWritebackReporter >> Uploading Ortoni HTML Report to Jira...');
                await this.jiraApiUtil.postUploadAttachment(jiraCardNumber, `${reportConfig.folderPath}/${reportConfig.filename}`);
                console.log('JiraWritebackReporter >> Uploaded Ortoni HTML Report to Jira.');
                console.log(`JiraWritebackReporter >> Posted necessary changes for '${jiraCardNumber}'`);
            });
        });
    }

    onBegin(config: FullConfig, suite: Suite): Promise<void> {
        return this.ortoniReport.onBegin(config, suite);
    }

    onError(error: TestError): void {
        return this.ortoniReport.onError(error);
    }

    onExit(): Promise<void> {
        return this.ortoniReport.onExit();
    }

    onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void {
        return this.ortoniReport.onStdOut(chunk, test, result);
    }
}