const Reporter = require('../lib/reporter');
const semver = require('semver');
const os = require('os');
const path = require('path');
const fs = require('fs-plus');
let osVersion = `${os.platform()}-${os.arch()}-${os.release()}`;

let getReleaseChannel = version => {
  return version.indexOf('beta') > -1
    ? 'beta'
    : version.indexOf('dev') > -1
    ? 'dev'
    : 'stable';
};

describe('Reporter', () => {
  let reporter,
    requests,
    initialStackTraceLimit,
    initialFsGetHomeDirectory,
    mockActivePackages;

  beforeEach(() => {
    reporter = new Reporter({
      request: (url, options) => requests.push(Object.assign({ url }, options)),
      alwaysReport: true,
      reportPreviousErrors: false
    });
    requests = [];
    mockActivePackages = [];
    spyOn(atom.packages, 'getActivePackages').andCallFake(
      () => mockActivePackages
    );

    initialStackTraceLimit = Error.stackTraceLimit;
    Error.stackTraceLimit = 1;

    initialFsGetHomeDirectory = fs.getHomeDirectory;
  });

  afterEach(() => {
    fs.getHomeDirectory = initialFsGetHomeDirectory;
    Error.stackTraceLimit = initialStackTraceLimit;
  });

  describe('.reportUncaughtException(error)', () => {
    it('posts errors originated inside Atom Core to BugSnag', () => {
      const repositoryRootPath = path.join(__dirname, '..');
      reporter = new Reporter({
        request: (url, options) =>
          requests.push(Object.assign({ url }, options)),
        alwaysReport: true,
        reportPreviousErrors: false,
        resourcePath: repositoryRootPath
      });

      let error = new Error();
      Error.captureStackTrace(error);
      reporter.reportUncaughtException(error);
      let [lineNumber, columnNumber] = error.stack
        .match(/.js:(\d+):(\d+)/)
        .slice(1)
        .map(s => parseInt(s));

      expect(requests.length).toBe(1);
      let [request] = requests;
      expect(request.method).toBe('POST');
      expect(request.url).toBe('https://notify.bugsnag.com');
      expect(request.headers.get('Content-Type')).toBe('application/json');
      let body = JSON.parse(request.body);
      // Delete `inProject` field because tests may fail when run as part of Atom core
      // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
      delete body.events[0].exceptions[0].stacktrace[0].inProject;

      expect(body).toEqual({
        apiKey: Reporter.API_KEY,
        notifier: {
          name: 'Atom',
          version: Reporter.LIB_VERSION,
          url: 'https://www.atom.io'
        },
        events: [
          {
            payloadVersion: '2',
            exceptions: [
              {
                errorClass: 'Error',
                message: '',
                stacktrace: [
                  {
                    method: semver.gt(process.versions.electron, '1.6.0')
                      ? 'Spec.<anonymous>'
                      : '<anonymous>',
                    file: 'spec/reporter-spec.js',
                    lineNumber: lineNumber,
                    columnNumber: columnNumber
                  }
                ]
              }
            ],
            severity: 'error',
            user: {},
            app: {
              version: atom.getVersion(),
              releaseStage: getReleaseChannel(atom.getVersion())
            },
            device: {
              osVersion: osVersion
            }
          }
        ]
      });
    });

    it('posts errors originated outside Atom Core to BugSnag', () => {
      fs.getHomeDirectory = () => path.join(__dirname, '..', '..');

      let error = new Error();
      Error.captureStackTrace(error);
      reporter.reportUncaughtException(error);
      let [lineNumber, columnNumber] = error.stack
        .match(/.js:(\d+):(\d+)/)
        .slice(1)
        .map(s => parseInt(s));

      expect(requests.length).toBe(1);
      let [request] = requests;
      expect(request.method).toBe('POST');
      expect(request.url).toBe('https://notify.bugsnag.com');
      expect(request.headers.get('Content-Type')).toBe('application/json');
      let body = JSON.parse(request.body);
      // Delete `inProject` field because tests may fail when run as part of Atom core
      // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
      delete body.events[0].exceptions[0].stacktrace[0].inProject;

      expect(body).toEqual({
        apiKey: Reporter.API_KEY,
        notifier: {
          name: 'Atom',
          version: Reporter.LIB_VERSION,
          url: 'https://www.atom.io'
        },
        events: [
          {
            payloadVersion: '2',
            exceptions: [
              {
                errorClass: 'Error',
                message: '',
                stacktrace: [
                  {
                    method: semver.gt(process.versions.electron, '1.6.0')
                      ? 'Spec.<anonymous>'
                      : '<anonymous>',
                    file: '~/exception-reporting/spec/reporter-spec.js',
                    lineNumber: lineNumber,
                    columnNumber: columnNumber
                  }
                ]
              }
            ],
            severity: 'error',
            user: {},
            app: {
              version: atom.getVersion(),
              releaseStage: getReleaseChannel(atom.getVersion())
            },
            device: {
              osVersion: osVersion
            }
          }
        ]
      });
    });

    describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => {
      let [error, notification] = [];

      beforeEach(() => {
        atom.notifications.clear();
        spyOn(atom.notifications, 'addInfo').andCallThrough();

        error = new Error();
        Error.captureStackTrace(error);

        error.metadata = { foo: 'bar' };
        error.privateMetadata = { baz: 'quux' };
        error.privateMetadataDescription = 'The contents of baz';
      });

      it('posts a notification asking for consent', () => {
        reporter.reportUncaughtException(error);
        expect(atom.notifications.addInfo).toHaveBeenCalled();
      });

      it('submits the error with the private metadata if the user consents', () => {
        spyOn(reporter, 'reportUncaughtException').andCallThrough();
        reporter.reportUncaughtException(error);
        reporter.reportUncaughtException.reset();

        notification = atom.notifications.getNotifications()[0];

        let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
        expect(notificationOptions.buttons[1].text).toMatch(/Yes/);

        notificationOptions.buttons[1].onDidClick();
        expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error);
        expect(reporter.reportUncaughtException.callCount).toBe(1);
        expect(error.privateMetadata).toBeUndefined();
        expect(error.privateMetadataDescription).toBeUndefined();
        expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' });

        expect(notification.isDismissed()).toBe(true);
      });

      it('submits the error without the private metadata if the user does not consent', () => {
        spyOn(reporter, 'reportUncaughtException').andCallThrough();
        reporter.reportUncaughtException(error);
        reporter.reportUncaughtException.reset();

        notification = atom.notifications.getNotifications()[0];

        let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
        expect(notificationOptions.buttons[0].text).toMatch(/No/);

        notificationOptions.buttons[0].onDidClick();
        expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error);
        expect(reporter.reportUncaughtException.callCount).toBe(1);
        expect(error.privateMetadata).toBeUndefined();
        expect(error.privateMetadataDescription).toBeUndefined();
        expect(error.metadata).toEqual({ foo: 'bar' });

        expect(notification.isDismissed()).toBe(true);
      });

      it('submits the error without the private metadata if the user dismisses the notification', () => {
        spyOn(reporter, 'reportUncaughtException').andCallThrough();
        reporter.reportUncaughtException(error);
        reporter.reportUncaughtException.reset();

        notification = atom.notifications.getNotifications()[0];
        notification.dismiss();

        expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error);
        expect(reporter.reportUncaughtException.callCount).toBe(1);
        expect(error.privateMetadata).toBeUndefined();
        expect(error.privateMetadataDescription).toBeUndefined();
        expect(error.metadata).toEqual({ foo: 'bar' });
      });
    });

    it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
      mockActivePackages = [
        {
          name: 'user-1',
          path: '/Users/user/.atom/packages/user-1',
          metadata: { version: '1.0.0' }
        },
        {
          name: 'user-2',
          path: '/Users/user/.atom/packages/user-2',
          metadata: { version: '1.2.0' }
        },
        {
          name: 'bundled-1',
          path:
            '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1',
          metadata: { version: '1.0.0' }
        },
        {
          name: 'bundled-2',
          path:
            '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2',
          metadata: { version: '1.2.0' }
        }
      ];

      const packageDirPaths = ['/Users/user/.atom/packages'];

      spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths);

      let error = new Error();
      Error.captureStackTrace(error);
      reporter.reportUncaughtException(error);

      expect(error.metadata.userPackages).toEqual({
        'user-1': '1.0.0',
        'user-2': '1.2.0'
      });
      expect(error.metadata.bundledPackages).toEqual({
        'bundled-1': '1.0.0',
        'bundled-2': '1.2.0'
      });
    });

    it('adds previous error messages and assertion failures to the reported metadata', () => {
      reporter.reportPreviousErrors = true;

      reporter.reportUncaughtException(new Error('A'));
      reporter.reportUncaughtException(new Error('B'));
      reporter.reportFailedAssertion(new Error('X'));
      reporter.reportFailedAssertion(new Error('Y'));

      reporter.reportUncaughtException(new Error('C'));

      expect(requests.length).toBe(5);

      const lastRequest = requests[requests.length - 1];
      const body = JSON.parse(lastRequest.body);

      console.log(body);
      expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']);
      expect(body.events[0].metaData.previousAssertionFailures).toEqual([
        'X',
        'Y'
      ]);
    });
  });

  describe('.reportFailedAssertion(error)', () => {
    it('posts warnings to bugsnag', () => {
      fs.getHomeDirectory = () => path.join(__dirname, '..', '..');

      let error = new Error();
      Error.captureStackTrace(error);
      reporter.reportFailedAssertion(error);
      let [lineNumber, columnNumber] = error.stack
        .match(/.js:(\d+):(\d+)/)
        .slice(1)
        .map(s => parseInt(s));

      expect(requests.length).toBe(1);
      let [request] = requests;
      expect(request.method).toBe('POST');
      expect(request.url).toBe('https://notify.bugsnag.com');
      expect(request.headers.get('Content-Type')).toBe('application/json');
      let body = JSON.parse(request.body);
      // Delete `inProject` field because tests may fail when run as part of Atom core
      // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
      delete body.events[0].exceptions[0].stacktrace[0].inProject;

      expect(body).toEqual({
        apiKey: Reporter.API_KEY,
        notifier: {
          name: 'Atom',
          version: Reporter.LIB_VERSION,
          url: 'https://www.atom.io'
        },
        events: [
          {
            payloadVersion: '2',
            exceptions: [
              {
                errorClass: 'Error',
                message: '',
                stacktrace: [
                  {
                    method: semver.gt(process.versions.electron, '1.6.0')
                      ? 'Spec.<anonymous>'
                      : '<anonymous>',
                    file: '~/exception-reporting/spec/reporter-spec.js',
                    lineNumber: lineNumber,
                    columnNumber: columnNumber
                  }
                ]
              }
            ],
            severity: 'warning',
            user: {},
            app: {
              version: atom.getVersion(),
              releaseStage: getReleaseChannel(atom.getVersion())
            },
            device: {
              osVersion: osVersion
            }
          }
        ]
      });
    });

    describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => {
      let [error, notification] = [];

      beforeEach(() => {
        atom.notifications.clear();
        spyOn(atom.notifications, 'addInfo').andCallThrough();

        error = new Error();
        Error.captureStackTrace(error);

        error.metadata = { foo: 'bar' };
        error.privateMetadata = { baz: 'quux' };
        error.privateMetadataDescription = 'The contents of baz';
      });

      it('posts a notification asking for consent', () => {
        reporter.reportFailedAssertion(error);
        expect(atom.notifications.addInfo).toHaveBeenCalled();
      });

      it('submits the error with the private metadata if the user consents', () => {
        spyOn(reporter, 'reportFailedAssertion').andCallThrough();
        reporter.reportFailedAssertion(error);
        reporter.reportFailedAssertion.reset();

        notification = atom.notifications.getNotifications()[0];

        let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
        expect(notificationOptions.buttons[1].text).toMatch(/Yes/);

        notificationOptions.buttons[1].onDidClick();
        expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error);
        expect(reporter.reportFailedAssertion.callCount).toBe(1);
        expect(error.privateMetadata).toBeUndefined();
        expect(error.privateMetadataDescription).toBeUndefined();
        expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' });

        expect(notification.isDismissed()).toBe(true);
      });

      it('submits the error without the private metadata if the user does not consent', () => {
        spyOn(reporter, 'reportFailedAssertion').andCallThrough();
        reporter.reportFailedAssertion(error);
        reporter.reportFailedAssertion.reset();

        notification = atom.notifications.getNotifications()[0];

        let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
        expect(notificationOptions.buttons[0].text).toMatch(/No/);

        notificationOptions.buttons[0].onDidClick();
        expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error);
        expect(reporter.reportFailedAssertion.callCount).toBe(1);
        expect(error.privateMetadata).toBeUndefined();
        expect(error.privateMetadataDescription).toBeUndefined();
        expect(error.metadata).toEqual({ foo: 'bar' });

        expect(notification.isDismissed()).toBe(true);
      });

      it('submits the error without the private metadata if the user dismisses the notification', () => {
        spyOn(reporter, 'reportFailedAssertion').andCallThrough();
        reporter.reportFailedAssertion(error);
        reporter.reportFailedAssertion.reset();

        notification = atom.notifications.getNotifications()[0];
        notification.dismiss();

        expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error);
        expect(reporter.reportFailedAssertion.callCount).toBe(1);
        expect(error.privateMetadata).toBeUndefined();
        expect(error.privateMetadataDescription).toBeUndefined();
        expect(error.metadata).toEqual({ foo: 'bar' });
      });

      it("only notifies the user once for a given 'privateMetadataRequestName'", () => {
        let fakeStorage = {};
        spyOn(global.localStorage, 'setItem').andCallFake(
          (key, value) => (fakeStorage[key] = value)
        );
        spyOn(global.localStorage, 'getItem').andCallFake(
          key => fakeStorage[key]
        );

        error.privateMetadataRequestName = 'foo';

        reporter.reportFailedAssertion(error);
        expect(atom.notifications.addInfo).toHaveBeenCalled();
        atom.notifications.addInfo.reset();

        reporter.reportFailedAssertion(error);
        expect(atom.notifications.addInfo).not.toHaveBeenCalled();

        let error2 = new Error();
        Error.captureStackTrace(error2);
        error2.privateMetadataDescription = 'Something about you';
        error2.privateMetadata = { baz: 'quux' };
        error2.privateMetadataRequestName = 'bar';

        reporter.reportFailedAssertion(error2);
        expect(atom.notifications.addInfo).toHaveBeenCalled();
      });
    });

    it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
      mockActivePackages = [
        {
          name: 'user-1',
          path: '/Users/user/.atom/packages/user-1',
          metadata: { version: '1.0.0' }
        },
        {
          name: 'user-2',
          path: '/Users/user/.atom/packages/user-2',
          metadata: { version: '1.2.0' }
        },
        {
          name: 'bundled-1',
          path:
            '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1',
          metadata: { version: '1.0.0' }
        },
        {
          name: 'bundled-2',
          path:
            '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2',
          metadata: { version: '1.2.0' }
        }
      ];

      const packageDirPaths = ['/Users/user/.atom/packages'];

      spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths);

      let error = new Error();
      Error.captureStackTrace(error);
      reporter.reportFailedAssertion(error);

      expect(error.metadata.userPackages).toEqual({
        'user-1': '1.0.0',
        'user-2': '1.2.0'
      });
      expect(error.metadata.bundledPackages).toEqual({
        'bundled-1': '1.0.0',
        'bundled-2': '1.2.0'
      });
    });

    it('adds previous error messages and assertion failures to the reported metadata', () => {
      reporter.reportPreviousErrors = true;

      reporter.reportUncaughtException(new Error('A'));
      reporter.reportUncaughtException(new Error('B'));
      reporter.reportFailedAssertion(new Error('X'));
      reporter.reportFailedAssertion(new Error('Y'));

      reporter.reportFailedAssertion(new Error('C'));

      expect(requests.length).toBe(5);

      const lastRequest = requests[requests.length - 1];
      const body = JSON.parse(lastRequest.body);

      expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']);
      expect(body.events[0].metaData.previousAssertionFailures).toEqual([
        'X',
        'Y'
      ]);
    });
  });
});
