/**
 * @license
 * Copyright 2018 Google Inc.
 * SPDX-License-Identifier: Apache-2.0
 */

import expect from 'expect';
import {KnownDevices, PredefinedNetworkConditions} from 'puppeteer';

import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';

const iPhone = KnownDevices['iPhone 6'];
const iPhoneLandscape = KnownDevices['iPhone 6 landscape'];

describe('Emulation', () => {
  setupTestBrowserHooks();

  describe('Page.viewport', function () {
    it('should get the proper viewport size', async () => {
      const {page} = await getTestState();

      expect(page.viewport()).toEqual({width: 800, height: 600});
      await page.setViewport({width: 123, height: 456});
      expect(page.viewport()).toEqual({width: 123, height: 456});
    });
    it('should support mobile emulation', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.PREFIX + '/mobile.html');
      expect(
        await page.evaluate(() => {
          return window.innerWidth;
        }),
      ).toBe(800);
      await page.setViewport(iPhone.viewport);
      expect(
        await page.evaluate(() => {
          return window.innerWidth;
        }),
      ).toBe(375);
      await page.setViewport({width: 400, height: 300});
      expect(
        await page.evaluate(() => {
          return window.innerWidth;
        }),
      ).toBe(400);
    });
    it('should support touch emulation', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.PREFIX + '/mobile.html');
      expect(
        await page.evaluate(() => {
          return 'ontouchstart' in window;
        }),
      ).toBe(false);
      await page.setViewport(iPhone.viewport);
      expect(
        await page.evaluate(() => {
          return 'ontouchstart' in window;
        }),
      ).toBe(true);
      expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
      await page.setViewport({width: 100, height: 100});
      expect(
        await page.evaluate(() => {
          return 'ontouchstart' in window;
        }),
      ).toBe(false);

      function dispatchTouch() {
        let fulfill!: (value: string) => void;
        const promise = new Promise(x => {
          fulfill = x;
        });
        window.ontouchstart = () => {
          fulfill('Received touch');
        };
        window.dispatchEvent(new Event('touchstart'));

        fulfill('Did not receive touch');

        return promise;
      }
    });
    it('should be detectable by Modernizr', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.PREFIX + '/detect-touch.html');
      expect(
        await page.evaluate(() => {
          return document.body.textContent!.trim();
        }),
      ).toBe('NO');
      await page.setViewport(iPhone.viewport);
      await page.goto(server.PREFIX + '/detect-touch.html');
      expect(
        await page.evaluate(() => {
          return document.body.textContent!.trim();
        }),
      ).toBe('YES');
    });
    it('should detect touch when applying viewport with touches', async () => {
      const {page, server} = await getTestState();

      await page.setViewport({width: 800, height: 600, hasTouch: true});
      await page.addScriptTag({url: server.PREFIX + '/modernizr.js'});
      expect(
        await page.evaluate(() => {
          return (globalThis as any).Modernizr.touchevents;
        }),
      ).toBe(true);
    });
    it('should support landscape emulation', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.PREFIX + '/mobile.html');
      expect(
        await page.evaluate(() => {
          return screen.orientation.type;
        }),
      ).toBe('portrait-primary');
      await page.setViewport(iPhoneLandscape.viewport);
      expect(
        await page.evaluate(() => {
          return screen.orientation.type;
        }),
      ).toBe('landscape-primary');
      await page.setViewport({width: 100, height: 100});
      expect(
        await page.evaluate(() => {
          return screen.orientation.type;
        }),
      ).toBe('portrait-primary');
    });
    it('should update media queries when resoltion changes', async () => {
      const {page, server} = await getTestState();

      async function getFontSize() {
        return await page.evaluate(() => {
          return parseInt(
            window.getComputedStyle(document.querySelector('p')!).fontSize,
            10,
          );
        });
      }

      for (const dpr of [1, 2, 3]) {
        await page.setViewport({
          width: 800,
          height: 600,
          deviceScaleFactor: dpr,
        });

        await page.goto(server.PREFIX + '/resolution.html');

        await expect(getFontSize()).resolves.toEqual(dpr);

        const screenshot = await page.screenshot({
          fullPage: false,
        });
        expect(screenshot).toBeGolden(`device-pixel-ratio${dpr}.png`);
      }
    });
    it('should load correct pictures when emulation dpr', async () => {
      const {page, server} = await getTestState();

      async function getCurrentSrc() {
        return await page.evaluate(() => {
          return document.querySelector('img')!.currentSrc;
        });
      }

      for (const dpr of [1, 2, 3]) {
        await page.setViewport({
          width: 800,
          height: 600,
          deviceScaleFactor: dpr,
        });

        await page.goto(server.PREFIX + '/picture.html');

        await expect(getCurrentSrc()).resolves.toMatch(
          new RegExp(`logo-${dpr}x.png`),
        );
      }
    });
  });

  describe('Page.emulate', function () {
    it('should work', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.PREFIX + '/mobile.html');
      await page.emulate(iPhone);
      expect(
        await page.evaluate(() => {
          return window.innerWidth;
        }),
      ).toBe(375);
      expect(
        await page.evaluate(() => {
          return navigator.userAgent;
        }),
      ).toContain('iPhone');
    });

    it('should work twice on about:blank', async () => {
      const {page} = await getTestState();

      await page.goto('about:blank');
      await page.emulate(KnownDevices['iPhone 13']);
      await page.emulate(KnownDevices['iPad Pro landscape']);
    });

    it('should support clicking', async () => {
      const {page, server} = await getTestState();

      await page.emulate(iPhone);
      await page.goto(server.PREFIX + '/input/button.html');
      using button = (await page.$('button'))!;
      await page.evaluate(button => {
        return (button.style.marginTop = '200px');
      }, button);
      await button.click();
      expect(
        await page.evaluate(() => {
          return (globalThis as any).result;
        }),
      ).toBe('Clicked');
    });
  });

  describe('Page.emulateMediaType', function () {
    it('should work', async () => {
      const {page} = await getTestState();

      expect(
        await page.evaluate(() => {
          return matchMedia('screen').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('print').matches;
        }),
      ).toBe(false);
      await page.emulateMediaType('print');
      expect(
        await page.evaluate(() => {
          return matchMedia('screen').matches;
        }),
      ).toBe(false);
      expect(
        await page.evaluate(() => {
          return matchMedia('print').matches;
        }),
      ).toBe(true);
      await page.emulateMediaType();
      expect(
        await page.evaluate(() => {
          return matchMedia('screen').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('print').matches;
        }),
      ).toBe(false);
    });
    it('should throw in case of bad argument', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page.emulateMediaType('bad').catch(error_ => {
        return (error = error_);
      });
      expect(error.message).toBe('Unsupported media type: bad');
    });
  });

  describe('Page.emulateMediaFeatures', function () {
    it('should work', async () => {
      const {page} = await getTestState();

      await page.emulateMediaFeatures([
        {name: 'prefers-reduced-motion', value: 'reduce'},
      ]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-reduced-motion: reduce)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-reduced-motion: no-preference)').matches;
        }),
      ).toBe(false);
      await page.emulateMediaFeatures([
        {name: 'prefers-color-scheme', value: 'light'},
      ]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-color-scheme: light)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-color-scheme: dark)').matches;
        }),
      ).toBe(false);
      await page.emulateMediaFeatures([
        {name: 'prefers-color-scheme', value: 'dark'},
      ]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-color-scheme: dark)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-color-scheme: light)').matches;
        }),
      ).toBe(false);
      await page.emulateMediaFeatures([
        {name: 'prefers-reduced-motion', value: 'reduce'},
        {name: 'prefers-color-scheme', value: 'light'},
      ]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-reduced-motion: reduce)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-reduced-motion: no-preference)').matches;
        }),
      ).toBe(false);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-color-scheme: light)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(prefers-color-scheme: dark)').matches;
        }),
      ).toBe(false);
      await page.emulateMediaFeatures([{name: 'color-gamut', value: 'srgb'}]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: p3)').matches;
        }),
      ).toBe(false);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: srgb)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: rec2020)').matches;
        }),
      ).toBe(false);
      await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: p3)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: srgb)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: rec2020)').matches;
        }),
      ).toBe(false);
      await page.emulateMediaFeatures([
        {name: 'color-gamut', value: 'rec2020'},
      ]);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: p3)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: srgb)').matches;
        }),
      ).toBe(true);
      expect(
        await page.evaluate(() => {
          return matchMedia('(color-gamut: rec2020)').matches;
        }),
      ).toBe(true);
    });
    it('should throw in case of bad argument', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page
        .emulateMediaFeatures([{name: 'bad', value: ''}])
        .catch(error_ => {
          return (error = error_);
        });
      expect(error.message).toBe('Unsupported media feature: bad');
    });
  });

  describe('Page.emulateTimezone', function () {
    it('should work', async () => {
      const {page} = await getTestState();

      await page.evaluate(() => {
        (globalThis as any).date = new Date(1479579154987);
      });
      await page.emulateTimezone('America/Jamaica');
      expect(
        await page.evaluate(() => {
          return (globalThis as any).date.toString();
        }),
      ).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)');

      await page.emulateTimezone('Pacific/Honolulu');
      expect(
        await page.evaluate(() => {
          return (globalThis as any).date.toString();
        }),
      ).toBe(
        'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)',
      );

      await page.emulateTimezone('America/Buenos_Aires');
      expect(
        await page.evaluate(() => {
          return (globalThis as any).date.toString();
        }),
      ).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)');

      await page.emulateTimezone('Europe/Berlin');
      expect(
        await page.evaluate(() => {
          return (globalThis as any).date.toString();
        }),
      ).toBe(
        'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)',
      );
    });

    it('should support timezone offset', async () => {
      const {page} = await getTestState();

      await page.evaluate(() => {
        (globalThis as any).date = new Date(1479579154987);
      });
      await page.emulateTimezone('GMT+10:00');
      expect(
        await page.evaluate(() => {
          return (globalThis as any).date.toString();
        }),
      ).toBe('Sun Nov 20 2016 04:12:34 GMT+1000 (GMT+10:00)');

      await page.emulateTimezone('GMT-12:34');
      expect(
        await page.evaluate(() => {
          return (globalThis as any).date.toString();
        }),
      ).toBe('Sat Nov 19 2016 05:38:34 GMT-1234 (GMT-12:34)');
    });

    it('should throw for invalid timezone IDs', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page.emulateTimezone('Foo/Bar').catch(error_ => {
        return (error = error_);
      });
      expect(error.message).atLeastOneToContain([
        'Invalid timezone ID: Foo/Bar', // CDP
        'invalid argument', // BiDi.
      ]);
      // Assert the error message is informative.
      expect(error.message).toContain('Foo/Bar');
      await page.emulateTimezone('Baz/Qux').catch(error_ => {
        return (error = error_);
      });
      expect(error.message).atLeastOneToContain([
        'Invalid timezone ID: Baz/Qux', // CDP
        'invalid argument', // BiDi.
      ]);
      // Assert the error message is informative.
      expect(error.message).toContain('Baz/Qux');
    });
  });

  describe('Page.emulateVisionDeficiency', function () {
    it('should work', async () => {
      const {page, server} = await getTestState();

      await page.setViewport({width: 500, height: 500});
      await page.goto(server.PREFIX + '/grid.html');

      {
        await page.emulateVisionDeficiency('none');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('screenshot-sanity.png');
      }

      {
        await page.emulateVisionDeficiency('achromatopsia');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png');
      }

      {
        await page.emulateVisionDeficiency('blurredVision');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png');
      }

      {
        await page.emulateVisionDeficiency('deuteranopia');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png');
      }

      {
        await page.emulateVisionDeficiency('protanopia');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('vision-deficiency-protanopia.png');
      }

      {
        await page.emulateVisionDeficiency('tritanopia');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png');
      }

      {
        await page.emulateVisionDeficiency('none');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('screenshot-sanity.png');
      }

      {
        await page.emulateVisionDeficiency('reducedContrast');
        const screenshot = await page.screenshot();
        expect(screenshot).toBeGolden('vision-deficiency-reducedContrast.png');
      }
    });

    it('should throw for invalid vision deficiencies', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page
        // @ts-expect-error deliberately passing invalid deficiency
        .emulateVisionDeficiency('invalid')
        .catch(error_ => {
          return (error = error_);
        });
      expect(error.message).toBe('Unsupported vision deficiency: invalid');
    });
  });

  describe('Page.emulateNetworkConditions', function () {
    it('should support offline', async () => {
      const {isFirefox, page, server} = await getTestState();

      await page.emulateNetworkConditions({
        offline: true,
        download: 0,
        upload: 0,
        latency: 0,
      });

      try {
        await page.goto(server.EMPTY_PAGE);
        throw new Error('not reached');
      } catch (err) {
        let expectedError;
        if (isFirefox) {
          expectedError = /NS_ERROR_OFFLINE/;
        } else {
          expectedError = /ERR_INTERNET_DISCONNECTED/;
        }
        expect((err as Error).message).toMatch(expectedError);
      }
    });

    it('should change navigator.connection.effectiveType', async () => {
      const {page} = await getTestState();

      const slow3G = PredefinedNetworkConditions['Slow 3G']!;
      const fast3G = PredefinedNetworkConditions['Fast 3G']!;

      expect(
        await page.evaluate('window.navigator.connection.effectiveType'),
      ).toBe('4g');
      await page.emulateNetworkConditions(fast3G);
      expect(
        await page.evaluate('window.navigator.connection.effectiveType'),
      ).toBe('3g');
      await page.emulateNetworkConditions(slow3G);
      expect(
        await page.evaluate('window.navigator.connection.effectiveType'),
      ).toBe('2g');
      await page.emulateNetworkConditions(null);
    });
  });

  describe('Page.emulateCPUThrottling', function () {
    it('should change the CPU throttling rate successfully', async () => {
      const {page} = await getTestState();

      await page.emulateCPUThrottling(100);
      await page.emulateCPUThrottling(null);
    });
  });

  describe('Page.emulateFocusedPage', function () {
    it('should emulate focus', async () => {
      const {page, context} = await getTestState();

      await page.emulateFocusedPage(true);
      expect(
        await page.evaluate(() => {
          return document.hasFocus();
        }),
      ).toBe(true);

      const page2 = await context.newPage();
      // Move page into background by focusing page2.
      await page2.bringToFront();

      expect(
        await page.evaluate(() => {
          return document.hasFocus();
        }),
      ).toBe(true);
    });

    it('should reset focus', async () => {
      const {page, context} = await getTestState();

      await page.emulateFocusedPage(true);

      const page2 = await context.newPage();
      // Move page into background by focusing page2.
      await page2.bringToFront();

      expect(
        await page.evaluate(() => {
          return document.hasFocus();
        }),
      ).toBe(true);

      await page.emulateFocusedPage(false);
      expect(
        await page.evaluate(() => {
          return document.hasFocus();
        }),
      ).toBe(false);
    });
  });
});
