/**
 * Copyright 2017 Google Inc. All rights reserved.
 * Modifications copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { test as it, expect } from './pageTest';
import util from 'util';

it('should work @smoke', async ({ page, browserName, channel }) => {
  let message = null;
  page.once('console', m => message = m);
  await Promise.all([
    page.evaluate(() => console.log('hello', 5, { foo: 'bar' })),
    page.waitForEvent('console')
  ]);
  if (browserName !== 'firefox' || channel?.startsWith('moz-firefox'))
    expect(message.text()).toEqual('hello 5 {foo: bar}');
  else
    expect(message.text()).toEqual('hello 5 JSHandle@object');
  expect(message.type()).toEqual('log');
  expect(await message.args()[0].jsonValue()).toEqual('hello');
  expect(await message.args()[1].jsonValue()).toEqual(5);
  expect(await message.args()[2].jsonValue()).toEqual({ foo: 'bar' });
});

it('should emit same log twice', async ({ page }) => {
  const messages = [];
  page.on('console', m => messages.push(m.text()));
  await page.evaluate(() => {
    for (let i = 0; i < 2; ++i)
      console.log('hello');
  });
  expect(messages).toEqual(['hello', 'hello']);
});

it('should use text() for inspection', async ({ page }) => {
  let text;
  const inspect = value => {
    text = util.inspect(value);
  };
  page.on('console', inspect);
  await page.evaluate(() => console.log('Hello world'));
  expect(text).toEqual('Hello world');
});

it('should work for different console API calls', async ({ page }) => {
  const messages = [];
  page.on('console', msg => messages.push(msg));
  // All console events will be reported before `page.evaluate` is finished.
  await page.evaluate(() => {
    // A pair of time/timeEnd generates only one Console API call.
    console.time('calling console.time');
    console.timeEnd('calling console.time');
    console.trace('calling console.trace');
    console.dir('calling console.dir');
    console.warn('calling console.warn');
    console.error('calling console.error');
    console.info('calling console.info');
    console.debug('calling console.debug');
    console.log(Promise.resolve('should not wait until resolved!'));
  });
  // WebKit uses console.debug() to report binding calls, make sure they don't get reported.
  await page.exposeBinding('foobar', async (_, value) => page.evaluate(value => console.log(value), value));
  await page.evaluate(() => window['foobar']('Using bindings'));

  expect(messages.map(msg => msg.type())).toEqual([
    'timeEnd', 'trace', 'dir', 'warning', 'error', 'info', 'debug', 'log', 'log'
  ]);
  expect(messages[0].text()).toContain('calling console.time');
  expect(messages.slice(1).map(msg => msg.text())).toEqual([
    'calling console.trace',
    'calling console.dir',
    'calling console.warn',
    'calling console.error',
    'calling console.info',
    'calling console.debug',
    'Promise',
    'Using bindings',
  ]);
});

it('should format the message correctly with time/timeLog/timeEnd', async ({ page, browserName, isBidi }) => {
  it.fixme(browserName === 'firefox' && !isBidi, 'https://github.com/microsoft/playwright/issues/10580');
  const messages = [];
  page.on('console', msg => messages.push(msg));
  await page.evaluate(async () => {
    console.time('foo time');
    await new Promise(x => window.builtins.setTimeout(x, 100));
    console.timeLog('foo time');
    await new Promise(x => window.builtins.setTimeout(x, 100));
    console.timeEnd('foo time');
  });
  expect(messages.length).toBe(2);
  if (browserName === 'webkit')
    expect(messages[0].type()).toBe('timeEnd');
  else if (browserName === 'chromium')
    expect(messages[0].type()).toBe('log');
  else if (browserName === 'firefox')
    expect(messages[0].type()).toBe('timeLog');
  expect(messages[1].type()).toBe('timeEnd');

  // WebKit has no space before the unit: https://bugs.webkit.org/show_bug.cgi?id=233556
  // Firefox/Bidi has no fractional part.
  expect(messages[0].text()).toMatch(/foo time: \d+(.\d+)? ?ms/);
  expect(messages[1].text()).toMatch(/foo time: \d+(.\d+)? ?ms/);
});

it('should not fail for window object', async ({ page, browserName, channel }) => {
  let message = null;
  page.once('console', msg => message = msg);
  await Promise.all([
    page.evaluate(() => console.error(window)),
    page.waitForEvent('console')
  ]);
  if (browserName !== 'firefox' || channel?.startsWith('moz-firefox'))
    expect(message.text()).toEqual('Window');
  else
    expect(message.text()).toEqual('JSHandle@object');
});

it('should trigger correct Log', async ({ page, server, browserName, isWindows }) => {
  it.skip(browserName === 'webkit' && isWindows, 'Upstream issue https://bugs.webkit.org/show_bug.cgi?id=229515');
  await page.goto('about:blank');
  const [message] = await Promise.all([
    page.waitForEvent('console'),
    page.evaluate(async url => fetch(url).catch(e => {}), server.EMPTY_PAGE)
  ]);
  expect(message.text()).toMatch(/Access-Control-Allow-Origin|CORS/);
  expect(message.type()).toEqual('error');
});

it('should have location for console API calls', async ({ page, server }) => {
  await page.goto(server.EMPTY_PAGE);
  const [message] = await Promise.all([
    page.waitForEvent('console', m => m.text().startsWith('here:')),
    page.goto(server.PREFIX + '/consolelog.html'),
  ]);
  expect(message.type()).toBe('log');
  const location = message.location();
  // Engines have different column notion.
  delete location.columnNumber;
  expect(location).toEqual({
    url: server.PREFIX + '/consolelog.html',
    lineNumber: 7,
  });
});

it('should not throw when there are console messages in detached iframes', async ({ page, server }) => {
  // @see https://github.com/GoogleChrome/puppeteer/issues/3865
  await page.goto(server.EMPTY_PAGE);
  const [popup] = await Promise.all([
    page.waitForEvent('popup'),
    page.evaluate(async () => {
      // 1. Create a popup that Playwright is not connected to.
      const win = window.open('');
      window['_popup'] = win;
      if (window.document.readyState !== 'complete')
        await new Promise(f => window.addEventListener('load', f));
      // 2. In this popup, create an iframe that console.logs a message.
      win.document.body.innerHTML = `<iframe src='/consolelog.html'></iframe>`;
      const frame = win.document.querySelector('iframe');
      if (!frame.contentDocument || frame.contentDocument.readyState !== 'complete')
        await new Promise(f => frame.addEventListener('load', f));
      // 3. After that, remove the iframe.
      frame.remove();
    }),
  ]);
  // 4. Connect to the popup and make sure it doesn't throw.
  expect(await popup.evaluate('1 + 1')).toBe(2);
});

it('should use object previews for arrays and objects', async ({ page, browserName, channel }) => {
  let text: string;
  page.on('console', message => {
    text = message.text();
  });
  await page.evaluate(() => console.log([1, 2, 3], { a: 1 }, window));

  if (browserName !== 'firefox' || channel?.startsWith('moz-firefox'))
    expect(text).toEqual('[1, 2, 3] {a: 1} Window');
  else
    expect(text).toEqual('Array JSHandle@object JSHandle@object');
});

it('should use object previews for errors', async ({ page, browserName, isBidi }) => {
  let text: string;
  page.on('console', message => {
    text = message.text();
  });
  await page.evaluate(() => console.log(new Error('Exception')));
  if (isBidi || browserName === 'firefox')
    expect(text).toEqual('Error');
  else if (browserName === 'chromium')
    expect(text).toContain('.evaluate');
  else if (browserName === 'webkit')
    expect(text).toEqual('Error: Exception');
});

it('do not update console count on unhandled rejections', async ({ page }) => {
  const messages: string[] = [];
  const consoleEventListener = m => messages.push(m.text());
  page.addListener('console', consoleEventListener);

  await page.evaluate(() => {
    const fail = async () => Promise.reject(new Error('error'));
    console.log('begin');
    void fail();
    void fail();
    fail().catch(() => {
      console.log('end');
    });
  });

  await expect.poll(() => messages).toEqual(['begin', 'end']);
});

it('should have timestamp', async ({ page, isAndroid }) => {
  it.skip(isAndroid, 'there is a time difference between android emulator and host machine');

  const before = Date.now() - 1;  // Account for the rounding of fractional timestamps.
  const [message] = await Promise.all([
    page.waitForEvent('console'),
    page.evaluate(() => console.log('timestamp test')),
  ]);
  const after = Date.now() + 1;  // Account for the rounding of fractional timestamps.
  expect(message.timestamp()).toBeGreaterThanOrEqual(before);
  expect(message.timestamp()).toBeLessThanOrEqual(after);
});

it('should have increasing timestamps', async ({ page }) => {
  const messages = [];
  page.on('console', msg => messages.push(msg));
  await page.evaluate(() => {
    console.log('first');
    console.log('second');
    console.log('third');
  });
  expect(messages.length).toBe(3);
  for (let i = 1; i < messages.length; i++)
    expect(messages[i].timestamp()).toBeGreaterThanOrEqual(messages[i - 1].timestamp());
});

it('should have timestamp in consoleMessages', async ({ page, isAndroid }) => {
  it.skip(isAndroid, 'there is a time difference between android emulator and host machine');

  const before = Date.now() - 1;  // Account for the rounding of fractional timestamps.
  await page.evaluate(() => console.log('stored message'));
  const after = Date.now() + 1;  // Account for the rounding of fractional timestamps.
  const messages = await page.consoleMessages();
  expect(messages.length).toBeGreaterThanOrEqual(1);
  const last = messages[messages.length - 1];
  expect(last.text()).toBe('stored message');
  expect(last.timestamp()).toBeGreaterThanOrEqual(before);
  expect(last.timestamp()).toBeLessThanOrEqual(after);
});

it('consoleMessages should work', async ({ page }) => {
  await page.evaluate(() => {
    for (let i = 0; i < 301; i++)
      console.log('message' + i);
  });

  const messages = await page.consoleMessages();
  const objects = messages.map(m => ({ text: m.text(), type: m.type(), page: m.page() }));

  const expected = [];
  for (let i = 201; i < 301; i++)
    expected.push(expect.objectContaining({ text: 'message' + i, type: 'log', page }));

  expect(objects.length, 'should be at least 100 messages').toBeGreaterThanOrEqual(100);
  expect(objects.slice(objects.length - expected.length), 'should return last messages').toEqual(expected);
});

it('clearConsoleMessages should work', async ({ page }) => {
  await page.evaluate(() => {
    console.log('message1');
    console.log('message2');
  });

  let messages = await page.consoleMessages();
  expect(messages.map(m => m.text())).toContain('message1');
  expect(messages.map(m => m.text())).toContain('message2');

  await page.clearConsoleMessages();

  messages = await page.consoleMessages();
  expect(messages).toEqual([]);

  await page.evaluate(() => console.log('message3'));
  messages = await page.consoleMessages();
  expect(messages.length).toBe(1);
  expect(messages[0].text()).toBe('message3');
});

it('consoleMessages since-navigation filter should work', async ({ page, server }) => {
  await page.evaluate(() => console.log('before navigation'));
  await page.goto(server.EMPTY_PAGE);
  await page.evaluate(() => console.log('after navigation'));

  const all = await page.consoleMessages({ filter: 'all' });
  expect(all.map(m => m.text())).toContain('before navigation');
  expect(all.map(m => m.text())).toContain('after navigation');

  // since-navigation is the default
  const sinceNav = await page.consoleMessages();
  expect(sinceNav.map(m => m.text())).not.toContain('before navigation');
  expect(sinceNav.map(m => m.text())).toContain('after navigation');
});

it('pageErrors since-navigation filter should work', async ({ page, server }) => {
  server.setContent('/page1', `<script>throw new Error('page1 error');</script>`, 'text/html');
  server.setContent('/page2', `<script>throw new Error('page2 error');</script>`, 'text/html');

  await page.goto(server.PREFIX + '/page1');
  await page.goto(server.PREFIX + '/page2');

  const all = await page.pageErrors({ filter: 'all' });
  expect(all.map(e => e.message)).toContain('page1 error');
  expect(all.map(e => e.message)).toContain('page2 error');

  // since-navigation is the default
  const sinceNav = await page.pageErrors();
  expect(sinceNav.map(e => e.message)).not.toContain('page1 error');
  expect(sinceNav.map(e => e.message)).toContain('page2 error');
});
