/// <reference types="cypress" />

/**
 * @type {Cypress.PluginConfig}
 */

import fs from "fs-extra";
import * as path from "path";
import del from "del";
import _ from "lodash";
import fetch, { Headers } from "node-fetch";
import { getLastCommit } from "git-last-commit";
import FormData from "form-data";

export interface AssurkaConfig {
  projectId: string;
  secret: string;
  token?: string;
  testSpecs?: any[];
  testRun?: any;
  testPlanId: any;
  testPlan?: any;
  testPlanParams?: any;
  host?: string;
  port?: number;
}

export const assurka = (on, config, assurkaConfig: AssurkaConfig) => {
  // console.log("config", config);

  on("before:run", async (details) => {
    // console.log("before:run", details);

    // reset any config's
    assurkaConfig.testSpecs = [];
    assurkaConfig.testRun = null;

    // check if we have an existing token
    if (!assurkaConfig.token) {
      const tokenResponse: any = await api(`auth/api`, "POST", {
        id: assurkaConfig.projectId,
        secret: assurkaConfig.secret,
      });
      assurkaConfig.token = tokenResponse.token;
      const commit = await getGitCommit(details.config.projectRoot);
      const payload = details;
      delete details.specs;
      assurkaConfig.testRun = await api(
        `test-run/${assurkaConfig.testPlanId}/create`,
        "POST",
        {
          ...payload,
          commit,
          platform: "cypress",
        }
      );
    }
  });

  on("after:run", async (results) => {
    console.log("after:run", results);

    if (results) {
      const testRunPayload = {
        duration: results.totalDuration,
        status: results.status,
        browserName: results.browserName,
        browserVersion: results.browserVersion,
        cypressVersion: results.cypressVersion,
        osName: results.osName,
        osVersion: results.osVersion,
        endedTestsAt: results.endedTestsAt,
        startedTestsAt: results.startedTestsAt,
        numFailedTests: results.totalFailed,
        numPassedTests: results.totalPassed,
        numPendingTests: results.totalPending,
        numSkippedTest: results.totalSkipped,
        numTestSuites: results.totalSuites,
        numTotalTests: results.totalTests,
      };
      //update the test run
      await api(
        `test-run/${assurkaConfig.testPlanId}/update/${assurkaConfig.testRun.id}`,
        "PATCH",
        testRunPayload
      );

      //update the test plan
      await api(
        `test-run/${assurkaConfig.projectId}/update/${assurkaConfig.testPlan}`,
        "PATCH",
        testRunPayload
      );

      // results will be undefined in interactive mode
      console.log(results.totalPassed, "out of", results.totalTests, "passed");
    }
  });

  on("before:spec", async (spec) => {
    //console.log("before:spec", spec);

    const testSpec = await api(
      `test-spec/${assurkaConfig.testRun.id}/create`,
      "POST",
      {
        name: spec.name,
        testRunId: assurkaConfig.testRun.id,
      }
    );
    assurkaConfig.testSpecs.push(testSpec);
  });

  on("after:spec", async (spec, results) => {
    // console.log("after:spec", spec, results);

    // get the test spec
    const testSpec = assurkaConfig.testSpecs.find((f) => f.name === spec.name);

    // make sure we have some results
    if (results) {
      const screenshots = [];
      // do we have any screenshots to upload?
      if (results && results.screenshots) {
        // upload the screenshots for the spec
        for (const screenshot of results.screenshots) {
          const result = await upload(screenshot.path);
          screenshots.push(result.filename);
        }
      }
      // do we have a video?
      let video = undefined;
      if (results && results.video) {
        // upload the video
        const videoResponse = await upload(results.video);
        if (videoResponse && videoResponse.filename) {
          video = videoResponse.filename;
        }
      }
      // setup the payload for the spec file
      let specPayload = {
        numPassingTests: results.stats.passes,
        numPendingTests: results.stats.pending,
        numFailingTests: results.stats.failures,
        duration: results.stats.wallClockDuration,
        runtime: results.stats.wallClockDuration,
        screenshots: screenshots,
        video: video,
        testRunId: assurkaConfig.testRun.id,
      };
      if (results.video) {
      }
      await api(
        `test-spec/${assurkaConfig.testRun.id}/update/${testSpec.id}`,
        "PATCH",
        specPayload
      );
      for (let index = 0; index < results.tests.length; index++) {
        const test = results.tests[index];
        const testPayload = {
          title: test.title[test.title.length - 1],
          ancestorTitles: test.title,
          status: test.state,
          attempts: test.attempts,
          body: test.body,
          displayError: test.displayError,
          testSpecId: testSpec.id,
          testRunId: assurkaConfig.testRun.id,
          position: index,
        };
        await api(`test-case/${testSpec.id}/create`, "POST", testPayload);
      }

      // delete the recorded video if the spec passed
      if (results && results.stats.failures === 0 && results.video) {
        // `del()` returns a promise, so it's important to return it to ensure
        // deleting the video is finished before moving on
        return del(results.video);
      }

      // delete the recorded video if no tests retried
      if (results && results.video) {
        // Do we have failures for any retry attempts?
        const failures = _.some(results.tests, (test) => {
          return _.some(test.attempts, { state: "failed" });
        });
        if (!failures) {
          // delete the video if the spec passed and no tests retried
          return del(results.video);
        }
      }
    }
  });

  on("after:screenshot", (details) => {
    //console.log("after:screenshot", details);
  });

  on("before:browser:launch", (browser, launchOptions) => {
    // console.log("before:browser:launch", browser, launchOptions);

    if (browser.name === "chrome" && browser.isHeadless) {
      // fullPage screenshot size is 1400x1200 on non-retina screens
      // and 2800x2400 on retina screens
      launchOptions.args.push("--window-size=1400,1200");

      // force screen to be non-retina (1400x1200 size)
      launchOptions.args.push("--force-device-scale-factor=1");
    }

    if (browser.name === "electron" && browser.isHeadless) {
      // fullPage screenshot size is 1400x1200
      launchOptions.preferences.width = 1400;
      launchOptions.preferences.height = 1200;
    }

    if (browser.name === "firefox" && browser.isHeadless) {
      // menubars take up height on the screen
      // so fullPage screenshot size is 1400x1126
      launchOptions.args.push("--width=1400");
      launchOptions.args.push("--height=1200");
    }

    return launchOptions;
  });

  /**
   *Sends results to the Assurka API
   * @param {string} url - the url
   * @param {string} method - the api method
   * @param {*} body - the api body
   * @memberof AssurkaCustomReporter
   */
  const api = async (url: string, method: string, body?: any) => {
    let headers = new Headers({ "Content-Type": "application/json" });
    if (assurkaConfig.token) {
      headers.append("Authorization", "Bearer " + assurkaConfig.token);
    }
    return fetch(`${assurkaConfig.host}/${url}`, {
      method,
      headers: headers,
      body: JSON.stringify(body),
    }).then((response) => {
      if (!response.ok) {
        return null;
      }
      return response.json();
    });
  };

  const upload = async (filePath: string) => {
    const form = new FormData();
    const fileStream = fs.createReadStream(filePath);
    form.append("file", fileStream);
    const options = {
      method: "POST",
      credentials: "include",
      body: form,
    };

    return fetch(`${assurkaConfig.host}/file/upload`, { ...options }).then(
      (response) => {
        if (!response.ok) {
          console.warn(response.statusText);
          return null;
        }
        return response.json();
      }
    );
  };

  const getGitCommit = async (path: string) => {
    return new Promise((res, _rej) => {
      getLastCommit(
        (err, commit) => {
          if (err) return res(null);
          return res(commit);
        },
        { dst: path }
      );
    });
  };
};
