import os from 'os';
import path from 'path';
import fs from 'fs';
import { promisify } from 'util';
import { expect } from 'chai';
import type { MongoshAnalytics } from './analytics-helpers';
import {
  ToggleableAnalytics,
  ThrottledAnalytics,
  SampledAnalytics,
} from './analytics-helpers';

const wait = promisify(setTimeout);

const timestamp = new Date();

describe('analytics helpers', function () {
  let events: any[];
  let target: MongoshAnalytics;

  beforeEach(function () {
    events = [];
    target = {
      identify(info: any) {
        events.push(['identify', info]);
      },
      track(info: any) {
        events.push(['track', info]);
      },
      async flush() {
        return Promise.resolve();
      },
    };
  });

  describe('ToggleableAnalytics', function () {
    it('starts out in paused state and can be toggled on and off', function () {
      const toggleable = new ToggleableAnalytics(target);
      expect(events).to.have.lengthOf(0);

      toggleable.identify({
        userId: 'me',
        traits: {
          platform: '1234',
          session_id: 'abc',
          device_id: 'test-device-id',
        },
        timestamp,
      });
      toggleable.track({
        userId: 'me',
        event: 'something',
        properties: { mongosh_version: '1.2.3', session_id: 'abc' },
        timestamp,
      });
      expect(events).to.have.lengthOf(0);

      toggleable.enable();
      expect(events).to.have.lengthOf(2);

      toggleable.track({
        userId: 'me',
        event: 'something2',
        properties: { mongosh_version: '1.2.3', session_id: 'abc' },
        timestamp,
      });
      expect(events).to.have.lengthOf(3);

      toggleable.pause();
      toggleable.track({
        userId: 'me',
        event: 'something3',
        properties: { mongosh_version: '1.2.3', session_id: 'abc' },
        timestamp,
      });
      expect(events).to.have.lengthOf(3);

      toggleable.disable();
      expect(events).to.have.lengthOf(3);
      toggleable.enable();

      expect(events).to.deep.equal([
        [
          'identify',
          {
            userId: 'me',
            traits: {
              platform: '1234',
              session_id: 'abc',
              device_id: 'test-device-id',
            },
            timestamp,
          },
        ],
        [
          'track',
          {
            userId: 'me',
            event: 'something',
            properties: { mongosh_version: '1.2.3', session_id: 'abc' },
            timestamp,
          },
        ],
        [
          'track',
          {
            userId: 'me',
            event: 'something2',
            properties: { mongosh_version: '1.2.3', session_id: 'abc' },
            timestamp,
          },
        ],
      ]);
    });

    it('emits an error for invalid messages if telemetry is enabled', function () {
      const toggleable = new ToggleableAnalytics(target);

      toggleable.identify({} as any);
      expect(() => toggleable.enable()).to.throw(
        'Telemetry setup is missing userId or anonymousId'
      );

      toggleable.disable();
      expect(() => toggleable.enable()).to.not.throw();
      expect(() => toggleable.track({} as any)).to.throw(
        'Telemetry setup is missing userId or anonymousId'
      );
    });
  });

  describe('ThrottledAnalytics', function () {
    const metadataPath = os.tmpdir();
    const userId = 'u-' + Date.now();
    const iEvt = {
      userId,
      traits: {
        platform: 'what',
        session_id: 'abc',
        device_id: 'test-device-id',
      },
    };
    const tEvt = {
      userId,
      event: 'hi',
      properties: { mongosh_version: '1.2.3', session_id: 'abc' },
    };
    const t2Evt = {
      userId,
      event: 'bye',
      properties: { mongosh_version: '1.2.3', session_id: 'abc' },
    };

    afterEach(async function () {
      try {
        await fs.promises.unlink(
          path.resolve(metadataPath, `am-${userId}.json`)
        );
      } catch (e) {
        // ignore
      }
    });

    it('should not throttle events by default', async function () {
      const analytics = new ThrottledAnalytics({ target });
      analytics.identify(iEvt);
      analytics.track(tEvt);
      analytics.track(tEvt);
      analytics.track(tEvt);
      analytics.track(tEvt);
      await analytics.flush();
      expect(events).to.have.lengthOf(5);
    });

    it('should throttle when throttling options are provided', async function () {
      const analytics = new ThrottledAnalytics({
        target,
        throttle: { rate: 5, metadataPath },
      });
      analytics.identify(iEvt);
      for (let i = 0; i < 100; i++) {
        analytics.track(tEvt);
      }
      await analytics.flush();
      expect(events).to.have.lengthOf(5);
    });

    it('should reset counter after a timeout', async function () {
      const analytics = new ThrottledAnalytics({
        target,
        throttle: { rate: 5, metadataPath, timeframe: 200 },
      });
      analytics.identify(iEvt);
      for (let i = 0; i < 100; i++) {
        analytics.track(tEvt);
      }
      // More than 200 to make sure we are outside of the previous frame
      await wait(300);
      for (let i = 0; i < 100; i++) {
        analytics.track(tEvt);
      }
      await analytics.flush();
      expect(events).to.have.lengthOf(10);
    });

    it('should persist throttled state and throttle across sessions', async function () {
      const metadataPath = os.tmpdir();

      // first "session"
      const a1 = new ThrottledAnalytics({
        target,
        throttle: { rate: 5, metadataPath },
      });
      a1.identify(iEvt);
      a1.track(tEvt);
      a1.track(tEvt);
      await a1.flush();
      expect(events).to.have.lengthOf(3);

      // second "session"
      const a2 = new ThrottledAnalytics({
        target,
        throttle: { rate: 5, metadataPath },
      });
      a2.identify(iEvt);
      for (let i = 0; i < 100; i++) {
        a2.track(tEvt);
      }
      await a2.flush();
      expect(events).to.have.lengthOf(5);
    });

    it('should only allow one analytics instance to send events', async function () {
      // first "session"
      const a1 = new ThrottledAnalytics({
        target,
        throttle: { rate: 5, metadataPath },
      });
      // second "session"
      const a2 = new ThrottledAnalytics({
        target,
        throttle: { rate: 5, metadataPath },
      });

      a1.identify(iEvt);
      a2.identify(iEvt);
      a1.track(tEvt);
      a2.track(t2Evt);
      a1.track(tEvt);
      a2.track(t2Evt);
      a1.track(tEvt);
      a2.track(t2Evt);

      await a1.flush();
      await a2.flush();

      expect(events).to.have.lengthOf(4);
      expect(
        events
          .filter((e) => e[0] === 'track')
          .map((e) => e[1].event)
          .join(',')
        // can't be fully sure which instance 'won' the lock because fs operations are inherently subject to race conditions
      ).to.match(/^(hi,hi,hi|bye,bye,bye)$/);
    });
  });

  describe('SampledAnalytics', function () {
    const userId = `u-${Date.now()}`;
    const iEvt = {
      userId,
      traits: {
        platform: 'what',
        session_id: 'abc',
        device_id: 'test-device-id',
      },
    };
    const tEvt = {
      userId,
      event: 'hi',
      properties: { mongosh_version: '1.2.3', session_id: 'abc' },
    };

    it('should send the event forward when sampled', function () {
      const analytics = new SampledAnalytics({
        target,
        sampling: () => true,
      });

      expect(analytics.enabled).to.be.true;

      analytics.identify(iEvt);
      analytics.track(tEvt);

      expect(events.length).to.equal(2);
    });

    it('should not send the event forward when not sampled', function () {
      const analytics = new SampledAnalytics({
        target,
        sampling: () => false,
      });

      expect(analytics.enabled).to.be.false;

      analytics.identify(iEvt);
      analytics.track(tEvt);

      expect(events.length).to.equal(0);
    });
  });
});
