import test from 'ava';
import TestHelper from './helpers/testHelper';
import * as testedChanges from './helpers/testedValues';

/**
 * @type {TestHelper}
 */
let helper = null;

test.before(async t => {
  helper = await TestHelper.init(t);
  await helper.initWombat();
});

test.beforeEach(async t => {
  t.context.sandbox = helper.sandbox();
  t.context.testPage = helper.testPage();
  t.context.server = helper.server();
});

test.after.always(async t => {
  await helper.stop();
});

test('internal globals: should not have removed _WBWombat from window', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      _WBWombat: {
        exists: window._WBWombat != null,
        type: typeof window._WBWombat
      },
      _WBWombatInit: {
        exists: window._WBWombatInit != null,
        type: typeof window._WBWombatInit
      }
    })),
    {
      _WBWombat: { exists: true, type: 'function' },
      _WBWombatInit: { exists: true, type: 'function' }
    },
    'The internal globals _WBWombat and _WBWombatInit are not as expected after initialization'
  );
});

test('internal globals: should add the property __WB_replay_top to window that is equal to the same window', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      exists: window.__WB_replay_top != null,
      eq: window.__WB_replay_top === window
    })),
    {
      exists: true,
      eq: true
    },
    'The internal global __WB_replay_top is not as expected after initialization'
  );
});

test('internal globals: should define the property __WB_top_frame when it is the top replayed page', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      exists: window.__WB_top_frame != null,
      neq: window.__WB_top_frame !== window
    })),
    {
      exists: true,
      neq: true
    },
    'The internal global __WB_top_frame is not as expected after initialization'
  );
});

test('internal globals: should define the WB_wombat_top property on Object.prototype', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => {
      const descriptor = Reflect.getOwnPropertyDescriptor(
        Object.prototype,
        'WB_wombat_top'
      );
      if (!descriptor) return { exists: false };
      return {
        exists: true,
        configurable: descriptor.configurable,
        enumerable: descriptor.enumerable,
        get: typeof descriptor.get,
        set: typeof descriptor.set
      };
    }),
    {
      exists: true,
      configurable: true,
      enumerable: false,
      get: 'function',
      set: 'function'
    },
    'The property descriptor added to the prototype of Object for WB_wombat_top is not correct'
  );
});

test('internal globals: should add the _WB_wombat_location property to window', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      _WB_wombat_location: {
        exists: window._WB_wombat_location != null,
        type: typeof window._WB_wombat_location
      },
      WB_wombat_location: {
        exists: window.WB_wombat_location != null,
        type: typeof window.WB_wombat_location
      }
    })),
    {
      _WB_wombat_location: { exists: true, type: 'object' },
      WB_wombat_location: { exists: true, type: 'object' }
    },
    'Wombat location properties on window are not correct'
  );
});

test('internal globals: should add the __wb_Date_now property to window', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      exists: window.__wb_Date_now != null,
      type: typeof window.__wb_Date_now
    })),
    { exists: true, type: 'function' },
    'The __wb_Date_now property of window is incorrect'
  );
});

test('internal globals: should add the __WB_timediff property to window.Date', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      exists: window.Date.__WB_timediff != null,
      type: typeof window.Date.__WB_timediff
    })),
    { exists: true, type: 'number' },
    'The __WB_timediff property of window.Date is incorrect'
  );
});

test('internal globals: should persist the original window.postMessage as __orig_postMessage', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => ({
      exists: window.__orig_postMessage != null,
      type: typeof window.__orig_postMessage,
      isO: window.__orig_postMessage.toString().includes('[native code]')
    })),
    { exists: true, type: 'function', isO: true },
    'The __WB_timediff property of window.Date is incorrect'
  );
});

test('internal globals: should not expose WombatLocation on window', async t => {
  const { sandbox, server } = t.context;
  t.true(
    await sandbox.evaluate(() => !('WombatLocation' in window)),
    'WombatLocation should not be exposed directly'
  );
});

test('exposed functions - extract_orig: should should extract the original url', async t => {
  const { sandbox, server } = t.context;
  t.true(
    await sandbox.evaluate(
      () =>
        window._wb_wombat.extract_orig(
          'http://localhost:3030/jberlin/sw/20180510171123/https://n0tan3rd.github.io/replay_test/'
        ) === 'https://n0tan3rd.github.io/replay_test/'
    ),
    'extract_orig could not extract the original URL'
  );
});

test('exposed functions - extract_orig: should not modify an un-rewritten url', async t => {
  const { sandbox, server } = t.context;
  t.true(
    await sandbox.evaluate(
      () =>
        window._wb_wombat.extract_orig(
          'https://n0tan3rd.github.io/replay_test/'
        ) === 'https://n0tan3rd.github.io/replay_test/'
    ),
    'extract_orig modified an original URL'
  );
});

test('exposed functions - extract_orig: should be able to extract the original url from an encoded string', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => {
      const expected = 'https://n0tan3rd.github.io/replay_test/';
      const extractO = window._wb_wombat.extract_orig;
      const unicode = extractO(
        '\u0068\u0074\u0074\u0070\u003a\u002f\u002f\u006c\u006f\u0063\u0061\u006c\u0068\u006f\u0073\u0074\u003a\u0033\u0030\u0033\u0030\u002f\u006a\u0062\u0065\u0072\u006c\u0069\u006e\u002f\u0073\u0077\u002f\u0032\u0030\u0031\u0038\u0030\u0035\u0031\u0030\u0031\u0037\u0031\u0031\u0032\u0033\u002f\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u006e\u0030\u0074\u0061\u006e\u0033\u0072\u0064\u002e\u0067\u0069\u0074\u0068\u0075\u0062\u002e\u0069\u006f\u002f\u0072\u0065\u0070\u006c\u0061\u0079\u005f\u0074\u0065\u0073\u0074\u002f'
      );
      const hex = extractO(
        '\x68\x74\x74\x70\x3a\x2f\x2f\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x3a\x33\x30\x33\x30\x2f\x6a\x62\x65\x72\x6c\x69\x6e\x2f\x73\x77\x2f\x32\x30\x31\x38\x30\x35\x31\x30\x31\x37\x31\x31\x32\x33\x2f\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6e\x30\x74\x61\x6e\x33\x72\x64\x2e\x67\x69\x74\x68\x75\x62\x2e\x69\x6f\x2f\x72\x65\x70\x6c\x61\x79\x5f\x74\x65\x73\x74\x2f'
      );
      return {
        unicode:
          unicode === expected &&
          unicode ===
            '\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u006e\u0030\u0074\u0061\u006e\u0033\u0072\u0064\u002e\u0067\u0069\u0074\u0068\u0075\u0062\u002e\u0069\u006f\u002f\u0072\u0065\u0070\u006c\u0061\u0079\u005f\u0074\u0065\u0073\u0074\u002f',
        hex:
          hex === expected &&
          hex ===
            '\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6e\x30\x74\x61\x6e\x33\x72\x64\x2e\x67\x69\x74\x68\x75\x62\x2e\x69\x6f\x2f\x72\x65\x70\x6c\x61\x79\x5f\x74\x65\x73\x74\x2f'
      };
    }),
    { unicode: true, hex: true },
    'extract_orig could not extract the original URL from an encoded string'
  );
});

test('exposed functions - rewrite_url: should be able to rewrite an encoded string', async t => {
  const { sandbox, server } = t.context;
  t.deepEqual(
    await sandbox.evaluate(() => {
      const expected = 'https://n0tan3rd.github.io/replay_test/';
      const rewrite_url = window._wb_wombat.rewrite_url;
      const unicode = rewrite_url(
        '\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u006e\u0030\u0074\u0061\u006e\u0033\u0072\u0064\u002e\u0067\u0069\u0074\u0068\u0075\u0062\u002e\u0069\u006f\u002f\u0072\u0065\u0070\u006c\u0061\u0079\u005f\u0074\u0065\u0073\u0074\u002f'
      );
      const hex = rewrite_url(
        '\x68\x74\x74\x70\x73\x3a\x2f\x2f\x6e\x30\x74\x61\x6e\x33\x72\x64\x2e\x67\x69\x74\x68\x75\x62\x2e\x69\x6f\x2f\x72\x65\x70\x6c\x61\x79\x5f\x74\x65\x73\x74\x2f'
      );
      return {
        unicode:
          unicode ===
          `${window.wbinfo.prefix}${window.wbinfo.wombat_ts}${
            window.wbinfo.mod
          }/${expected}`,
        hex:
          hex ===
          `${window.wbinfo.prefix}${window.wbinfo.wombat_ts}${
            window.wbinfo.mod
          }/${expected}`
      };
    }),
    { unicode: true, hex: true },
    'rewrite_url could not rewrite an encoded string'
  );
});

testedChanges.TestedPropertyDescriptorUpdates.forEach(aTest => {
  const msg = 'an property descriptor override should have been applied';
  aTest.props.forEach(prop => {
    if (aTest.docOrWin) {
      test(`${aTest.docOrWin}.${prop}: ${msg}`, async t => {
        const { sandbox, server } = t.context;
        const result = await sandbox.evaluate(
          testFn,
          aTest.docOrWin,
          prop,
          aTest.expectedInterface,
          aTest.skipGet,
          aTest.skipSet
        );
        t.deepEqual(
          result.main,
          { exists: true, good: true },
          `The property descriptor for ${aTest.docOrWin}.${prop} is incorrect`
        );
        for (let i = 0; i < result.sub.length; i++) {
          const subTest = result.sub[i];
          t.true(subTest.result, subTest.what);
        }
      });
    } else if (aTest.objPaths) {
      aTest.objPaths.forEach(objPath => {
        test(`${objPath.replace('window.', '')}.${prop}: ${msg}`, async t => {
          const { sandbox, server } = t.context;
          const result = await sandbox.evaluate(
            testFn,
            objPath,
            prop,
            aTest.expectedInterface,
            aTest.skipGet,
            aTest.skipSet
          );
          t.deepEqual(
            result.main,
            { exists: true, good: true },
            `The property descriptor for ${objPath.replace(
              'window.',
              ''
            )}.${prop} is incorrect`
          );
          for (let i = 0; i < result.sub.length; i++) {
            const subTest = result.sub[i];
            t.true(subTest.result, subTest.what);
          }
        });
      });
    } else {
      test(`${aTest.objPath.replace(
        'window.',
        ''
      )}.${prop}: ${msg}`, async t => {
        const { sandbox, server } = t.context;
        const result = await sandbox.evaluate(
          testFn,
          aTest.objPath,
          prop,
          aTest.expectedInterface,
          aTest.skipGet,
          aTest.skipSet
        );
        t.deepEqual(
          result.main,
          { exists: true, good: true },
          `The property descriptor for ${aTest.objPath.replace(
            'window.',
            ''
          )}.prop is incorrect`
        );
        for (let i = 0; i < result.sub.length; i++) {
          const subTest = result.sub[i];
          t.true(subTest.result, subTest.what);
        }
      });
    }
  });
  function testFn(objectPath, prop, expectedInterface, skipGet, skipSet) {
    // get the original and wombat object represented by the object path expression
    // eg if objectPath is window.Node.prototype then original === window.Node.prototype and obj === wombatSandbox.window.Node.prototype
    const original = window.WombatTestUtil.getOriginalWinDomViaPath(objectPath);
    const existing = window.WombatTestUtil.getViaPath(
      window.wombatSandbox,
      objectPath
    );
    // sometimes we need to skip a property get/set toString check
    let skipGetCheck = !!(skipGet && skipGet.indexOf(prop) > -1);
    let skipSetCheck = !!(skipSet && skipSet.indexOf(prop) > -1);
    // use the reflect object in the wombat sandbox context
    const newPD = Reflect.getOwnPropertyDescriptor(existing, prop);
    const expectedEntries = Object.entries(expectedInterface);
    const tResults = {
      main: {
        exists: newPD != null,
        good: expectedEntries.every(([pk, ptype]) => typeof newPD[pk] === ptype)
      },
      sub: []
    };
    const originalPD = window.WombatTestUtil.getOriginalPropertyDescriptorFor(
      original,
      prop
    );
    if (originalPD) {
      // do a quick deep check first to see if we modified something
      tResults.sub.push({
        what: 'newPD !== originalPD',
        result: expectedEntries.some(
          ([pk, ptype]) => newPD[pk] !== originalPD[pk]
        )
      });
      // now check each part of the expected property descriptor to make sure nothing went wrong
      if (
        !skipGetCheck &&
        expectedInterface.get &&
        newPD.get &&
        originalPD.get
      ) {
        tResults.sub.push({
          what: `${objectPath}.${prop} getter.toString() !== original getter.toString()`,
          result: newPD.get.toString() !== originalPD.get.toString()
        });
      }
      if (
        !skipSetCheck &&
        expectedInterface.set &&
        newPD.set &&
        originalPD.set
      ) {
        tResults.sub.push({
          what: `${objectPath}.${prop} setter.toString() !== original setter.toString()`,
          result: newPD.set.toString() !== originalPD.set.toString()
        });
      }
      if (originalPD.configurable != null && newPD.configurable != null) {
        tResults.sub.push({
          what: `${objectPath}.${prop} the "new" configurable pd does not equals the original`,
          result: newPD.configurable === originalPD.configurable
        });
      }
      if (originalPD.writable != null && newPD.writable != null) {
        tResults.sub.push({
          what: `${objectPath}.${prop} the "new" writable pd does not equals the original`,
          result: newPD.writable === originalPD.writable
        });
      }
      if (originalPD.value != null && newPD.value != null) {
        tResults.sub.push({
          what: `${objectPath}.${prop} the "new" value pd does equals the original`,
          result: newPD.value !== originalPD.value
        });
      }
    }
    return tResults;
  }
});

testedChanges.TestFunctionChanges.forEach(aTest => {
  if (aTest.constructors) {
    aTest.constructors.forEach(ctor => {
      const niceC = ctor.replace('window.', '');
      test(`${niceC}: an constructor override should have been applied`, async t => {
        const { sandbox, server } = t.context;
        t.true(
          await sandbox.evaluate(testFN, ctor),
          `The ${ctor} was not updated`
        );
        function testFN(ctor) {
          const existing = window.WombatTestUtil.getViaPath(
            window.wombatSandbox,
            ctor
          );
          const original = window.WombatTestUtil.getOriginalWinDomViaPath(ctor);
          return existing.toString() !== original.toString();
        }
      });
    });
  } else if (aTest.objPath && aTest.origs) {
    for (let i = 0; i < aTest.fns.length; i++) {
      const fn = aTest.fns[i];
      const ofn = aTest.origs[i];
      const niceWhat = `${aTest.objPath.replace('window.', '')}.${fn}`;
      test(`${niceWhat}: an function override should have been applied`, async t => {
        const { sandbox, server } = t.context;
        t.deepEqual(
          await sandbox.evaluate(testFN, aTest.objPath, fn, ofn),
          { ne: true, persisted: true },
          `The ${niceWhat} was not updated correctly`
        );
        function testFN(objPath, fn, ofn) {
          const existing = window.WombatTestUtil.getViaPath(
            window.wombatSandbox,
            objPath
          );
          const original = window.WombatTestUtil.getOriginalWinDomViaPath(
            objPath
          );
          return {
            ne: existing[fn].toString() !== original[fn].toString(),
            persisted: existing[ofn].toString() === original[fn].toString()
          };
        }
      });
    }
  } else if (aTest.fnPath) {
    test(`${
      aTest.fnPath
    }: an function override should have been applied`, async t => {
      const { sandbox, server } = t.context;
      const result = await sandbox.evaluate(testFN, aTest.fnPath, aTest.oPath);
      t.true(result.ne, `${aTest.fnPath} was not updated`);
      if (result.originalPersisted) {
        t.true(
          result.originalPersisted,
          `The persisted original function for ${
            aTest.fnPath
          } does not match the original`
        );
      }
      function testFN(fnPath, oPath) {
        const existing = window.WombatTestUtil.getViaPath(
          window.wombatSandbox,
          fnPath
        );
        const original = window.WombatTestUtil.getOriginalWinDomViaPath(fnPath);
        const result = {
          ne: existing.toString() !== original.toString()
        };
        if (oPath) {
          const ofnOnfn = window.WombatTestUtil.getViaPath(
            window.wombatSandbox,
            oPath
          );
          result.originalPersisted = ofnOnfn.toString() === original.toString();
        }
        return result;
      }
    });
  } else if (aTest.fns) {
    aTest.fns.forEach(fn => {
      const niceWhat = `${aTest.objPath.replace('window.', '')}.${fn}`;
      test(`${niceWhat}: an function override should have been applied`, async t => {
        const { sandbox, server } = t.context;
        const results = await sandbox.evaluate(testFn, aTest.objPath, fn);
        if (results.tests) {
          results.tests.forEach(({ test, msg }) => {
            t.true(test, msg);
          });
        } else {
          t.true(results.test, `${aTest.objPath}.${fn} was not updated`);
        }
        function testFn(objPath, fn) {
          const existing = window.WombatTestUtil.getViaPath(
            window.wombatSandbox,
            objPath
          );
          const original = window.WombatTestUtil.getOriginalWinDomViaPath(
            objPath
          );
          const result = {};
          if (existing[fn] && original[fn]) {
            result.test = existing[fn].toString() !== original[fn].toString();
          } else if (existing[fn]) {
            result.tests = [
              {
                test: existing[fn] !== null,
                msg: `${objPath}.${fn} was not overridden (is undefined/null)`
              },
              {
                test: !existing[fn].toString().includes('[native code]'),
                msg: `${objPath}.${fn} was not overridden at all`
              }
            ];
          } else {
            result.test = false;
          }
          return result;
        }
      });
    });
  } else if (aTest.persisted) {
    aTest.persisted.forEach(fn => {
      const niceWhat = `${aTest.objPath.replace('window.', '')}.${fn}`;
      test(`${niceWhat}: the original function should exist on the overridden object`, async t => {
        const { sandbox, server } = t.context;
        t.true(
          await sandbox.evaluate(testFn, aTest.objPath, fn),
          `the original function '${niceWhat}' was not persisted`
        );
        function testFn(objPath, fn) {
          const existing = window.WombatTestUtil.getViaPath(
            window.wombatSandbox,
            objPath
          );
          const original = window.WombatTestUtil.getOriginalWinDomViaPath(
            objPath
          );
          return existing[fn].toString() === original[fn].toString();
        }
      });
    });
  }
});