import test from 'ava';
import '../../support/polyfills/polyfills';
import sinon, { SinonSandbox } from 'sinon';

import { ServiceWorker } from '../../../src/service-worker/ServiceWorker';
import { setBrowser } from '../../support/tester/browser';
import { BrowserUserAgent, TestEnvironment } from '../../support/sdk/TestEnvironment';

import Database from '../../../src/services/Database';
import { ConfigIntegrationKind } from '../../../src/models/AppConfig';
import { PushSubscriptionChangeEvent } from "../../support/mocks/service-workers/models/PushSubscriptionChangeEvent";
import ServiceWorkerGlobalScope from '../../support/mocks/service-workers/ServiceWorkerGlobalScope';
import PushManager from '../../support/mocks/service-workers/models/PushManager';
import Random from "../../support/tester/Random";
import PushSubscription from '../../support/mocks/service-workers/models/PushSubscription';
import { SubscriptionManager } from '../../../src/managers/SubscriptionManager';
import OneSignalApiSW from '../../../src/OneSignalApiSW';

declare var self: ServiceWorkerGlobalScope;
const appId = Random.getRandomUuid();
const existingDeviceId = Random.getRandomUuid();
let oldSubscription: PushSubscription;
let newSubscription: PushSubscription;

let sinonSandbox: SinonSandbox;

test.beforeEach(async() => {
  sinonSandbox = sinon.sandbox.create();
  sinonSandbox.stub(OneSignalApiSW, 'downloadServerAppConfig')
    .resolves(TestEnvironment.getFakeServerAppConfig(ConfigIntegrationKind.Custom));
  sinonSandbox.stub(OneSignalApiSW, 'updatePlayer').resolves();

  oldSubscription = await new PushManager().subscribe({
    userVisibleOnly: true,
    applicationServerKey: Random.getRandomUint8Array(65).buffer
  });
  
  newSubscription = await new PushManager().subscribe({
    userVisibleOnly: true,
    applicationServerKey: Random.getRandomUint8Array(65).buffer
  });
  
  await TestEnvironment.initializeForServiceWorker({
    url: new URL(`https://site.com/service-worker.js?appId=${appId}`)
  });
  
  setBrowser(BrowserUserAgent.ChromeMacSupported);
});

test.afterEach(function (_t: TestContext) {
  sinonSandbox.restore();
});

test(`called with an old and new subscription successfully updates the subscription`, async t => {
  /*
    For this test, pretend the user revoked permissions, pushsubscriptionchange's event provides
    an oldSubscription but not a new subscription, and the service worker tries to resubscribe
    anyways.

    It's going to fail due to the blocked permission, so let's simulate that here.
   */
  sinonSandbox
    .stub(SubscriptionManager.prototype, 'subscribeFcmFromWorker')
    .throws('some-error');

  await setInitialDatabaseState(existingDeviceId, oldSubscription.endpoint);

  //before subscription change
  let subscription = await Database.getSubscription();
  t.deepEqual(subscription.deviceId, existingDeviceId);
  t.deepEqual(subscription.subscriptionToken, oldSubscription.endpoint);
  
  const event = new PushSubscriptionChangeEvent();
  event.oldSubscription = oldSubscription;
  event.newSubscription = newSubscription;

  // navigator.permissions is undefined in Firefox service worker context, lets simulate that here
  sinonSandbox.stub(navigator, 'permissions').value(undefined);
  
  await runPushSubscriptionChange(event);

  // After pushsubscriptionchange
  subscription = await Database.getSubscription();
  // The device record ID should stay the same
  t.deepEqual(subscription.deviceId, existingDeviceId);
  // The subscription endpoint should be the new endpoint
  t.deepEqual(subscription.subscriptionToken, newSubscription.endpoint);
});

test(`without an existing device ID, lookup existing device ID, updates the looked-up record`, async t => {
  const idReturnedByLookupCall = Random.getRandomUuid();
  sinonSandbox.stub(OneSignalApiSW, 'getUserIdFromSubscriptionIdentifier').resolves(idReturnedByLookupCall);

  // Before pushsubscriptionchange
  let subscription = await Database.getSubscription();
  // There should be no existing device ID, we'll look this up by push endpoint in the test
  t.deepEqual(subscription.deviceId, null);
  // Don't check existence of old endpoint, it isn't used, pushsubscriptionchange provides the old endpoint

  const event = new PushSubscriptionChangeEvent();
  event.oldSubscription = oldSubscription;
  event.newSubscription = newSubscription;
  await runPushSubscriptionChange(event);

  // After pushsubscriptionchange
  subscription = await Database.getSubscription();
  // We should now have a device ID
  t.deepEqual(subscription.deviceId, idReturnedByLookupCall);
  // Our push endpoint should be the new updated one
  t.deepEqual(subscription.subscriptionToken, newSubscription.endpoint);
});

test(`called with an old and without a new subscription, custom resubscription succeeds and updates record endpoint`,
  async t => {
  const newSubscriptionByReregistration = newSubscription;

  sinonSandbox.stub(PushManager.prototype, 'subscribe')
    .callsFake(async _options => {
      const subscription = await self.registration.pushManager.getSubscription();
      if (!subscription) {
        return newSubscriptionByReregistration;
      } else {
        return oldSubscription;
      }
    });
    
  await setInitialDatabaseState(existingDeviceId, oldSubscription.endpoint);

  // Before pushsubscriptionchange
  let subscription = await Database.getSubscription();
  t.deepEqual(subscription.deviceId, existingDeviceId);
  t.deepEqual(subscription.subscriptionToken, oldSubscription.endpoint);

  const event = new PushSubscriptionChangeEvent();
  event.oldSubscription = oldSubscription;
  await runPushSubscriptionChange(event);

  // After pushsubscriptionchange
  subscription = await Database.getSubscription();
  t.deepEqual(subscription.deviceId, existingDeviceId);
  t.deepEqual(subscription.subscriptionToken, newSubscriptionByReregistration.endpoint);
});

test(
  `called with an existing device ID, with old and without new subscription, custom resubscription fails ` +
  `and updates existing device record to clear subscription`,
  async t => {
    sinonSandbox
      .stub(SubscriptionManager.prototype, 'subscribeFcmFromWorker')
      .throws('some-error');

    await setInitialDatabaseState(existingDeviceId, oldSubscription.endpoint);

    // Before pushsubscriptionchange
    let subscription = await Database.getSubscription();
    t.deepEqual(subscription.deviceId, existingDeviceId);
    t.deepEqual(subscription.subscriptionToken, oldSubscription.endpoint);

    const event = new PushSubscriptionChangeEvent();
    event.oldSubscription = oldSubscription;
    await runPushSubscriptionChange(event);

    // After pushsubscriptionchange
    subscription = await Database.getSubscription();
    t.deepEqual(subscription.deviceId, existingDeviceId);
    t.deepEqual(subscription.subscriptionToken, null);
});

test(`called without an existing device ID, without old and new subscription, custom resubscription fails ` +
`and local data is cleared`, async t => {
  sinonSandbox
    .stub(SubscriptionManager.prototype, 'subscribeFcmFromWorker')
    .throws('some-error');

  // Before pushsubscriptionchange
  let subscription = await Database.getSubscription();
  t.deepEqual(subscription.deviceId, null);
  t.deepEqual(subscription.subscriptionToken, null);

  const event = new PushSubscriptionChangeEvent();
  await runPushSubscriptionChange(event);

  // After pushsubscriptionchange
  subscription = await Database.getSubscription();
  t.deepEqual(subscription.deviceId, null);
  t.deepEqual(subscription.subscriptionToken, null);
});


/**
 * Helpers
 */
async function setInitialDatabaseState(deviceId?: string, subscriptionToken?: string) {
  let subscription = await Database.getSubscription();
  subscription.deviceId = deviceId;
  subscription.subscriptionToken = subscriptionToken;
  await Database.setSubscription(subscription);
}

async function runPushSubscriptionChange(event: PushSubscriptionChangeEvent): Promise<void> {
  const testPromise = new Promise(resolve => {
    self.addEventListener("pushsubscriptionchange", async (evt: any) => {
      await ServiceWorker.onPushSubscriptionChange(evt);
      resolve();
    });
  });
  self.dispatchEvent(event);
  await testPromise;
}
