// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:puppeteer/puppeteer.dart';
import 'package:test/test.dart';
import 'package:test_common/utilities.dart';

import '../fixtures/context.dart';
import '../fixtures/utilities.dart';

enum ConsoleSource { background, devTools, worker }

final _backgroundLogs = [];
final _devToolsLogs = [];
final _workerLogs = [];

Future<String> buildDebugExtension({required bool isMV3}) async {
  final extensionDir = absolutePath(pathFromDwds: 'debug_extension');
  await Process.run('dart', [
    p.join('tool', 'build_extension.dart'),
    if (isMV3) '--mv3',
  ], workingDirectory: extensionDir);
  return p.join(extensionDir, 'compiled');
}

Future<Browser> setUpExtensionTest(
  TestContext context, {
  required String extensionPath,
  bool serveDevTools = true,
  bool useSse = false,
  bool isInternalBuild = false,
  bool isFlutterApp = false,
  bool openChromeDevTools = false,
  String? workspaceName,
}) async {
  // TODO(elliette): Only start a TestServer, that way we can get rid of the
  // launchChrome parameter: https://github.com/dart-lang/webdev/issues/1779
  await context.setUp(
    testSettings: TestSettings(launchChrome: false, isFlutterApp: isFlutterApp),
    appMetadata: TestAppMetadata(
      isInternalBuild: isInternalBuild,
      workspaceName: workspaceName,
    ),
    debugSettings: serveDevTools
        ? TestDebugSettings.withDevToolsLaunch(
            context,
          ).copyWith(enableDebugExtension: true, useSse: useSse)
        : TestDebugSettings.noDevToolsLaunch().copyWith(
            enableDebugExtension: true,
            useSse: useSse,
          ),
  );
  return await puppeteer.launch(
    devTools: openChromeDevTools,
    headless: false,
    timeout: Duration(seconds: 60),
    args: [
      '--load-extension=$extensionPath',
      '--disable-extensions-except=$extensionPath',
      '--disable-features=DialMediaRouteProvider',
    ],
  );
}

Future<void> tearDownHelper({Worker? worker, Page? backgroundPage}) async {
  _logConsoleMsgsOnFailure();
  _backgroundLogs.clear();
  _workerLogs.clear();
  _devToolsLogs.clear();
  await _clearStorage(worker: worker, backgroundPage: backgroundPage);
}

Future<Worker> getServiceWorker(Browser browser) async {
  final serviceWorkerTarget = await browser.waitForTarget(
    (target) => target.type == 'service_worker',
  );
  final worker = (await serviceWorkerTarget.worker)!;
  return Worker(
    worker.client,
    worker.url,
    onConsoleApiCalled: (type, jsHandles, _) {
      for (final handle in jsHandles) {
        _saveConsoleMsg(
          source: ConsoleSource.worker,
          type: '$type',
          msg: '$handle',
        );
      }
    },
    onExceptionThrown: null,
  );
}

Future<Page> getBackgroundPage(Browser browser) async {
  final backgroundPageTarget = await browser.waitForTarget(
    (target) => target.type == 'background_page',
  );
  final backgroundPage = await backgroundPageTarget.page;
  backgroundPage.onConsole.listen((msg) {
    _saveConsoleMsg(
      source: ConsoleSource.background,
      type: '${msg.type}',
      msg: msg.text ?? '',
    );
  });
  return backgroundPage;
}

Future<Page> getChromeDevToolsPage(Browser browser) async {
  final chromeDevToolsTarget = browser.targets.firstWhere(
    (target) => target.url.startsWith('devtools://devtools'),
  );
  chromeDevToolsTarget.type = 'page';
  final chromeDevToolsPage = await chromeDevToolsTarget.page;
  chromeDevToolsPage.onConsole.listen((msg) {
    _saveConsoleMsg(
      source: ConsoleSource.devTools,
      type: '${msg.type}',
      msg: msg.text ?? '',
    );
  });
  return chromeDevToolsPage;
}

Future evaluate(
  String jsExpression, {
  Worker? worker,
  Page? backgroundPage,
}) async {
  if (worker != null) {
    assert(backgroundPage == null);
    return worker.evaluate(jsExpression);
  } else {
    return backgroundPage!.evaluate(jsExpression);
  }
}

Future<void> clickOnExtensionIcon({
  required Browser browser,
  Worker? worker,
  Page? backgroundPage,
}) async {
  await evaluate(
    _clickIconJs(isMV3: worker != null),
    worker: worker,
    backgroundPage: backgroundPage,
  );
  final popupTarget = await browser.waitForTarget(
    (target) => target.url.contains('popup'),
  );
  final popupPage = await popupTarget.page;
  final launchDevToolsButton = await popupPage.$OrNull('#launchDevToolsButton');
  await launchDevToolsButton?.click();
}

// Note: The following delay is required to reduce flakiness. It makes
// sure the service worker execution context is ready.
Future<void> workerEvalDelay() async {
  await Future.delayed(Duration(seconds: 1));
  return;
}

Future<Page> navigateToPage(
  Browser browser, {
  required String url,
  bool isNew = false,
}) async {
  final page = isNew
      ? await browser.newPage()
      : await _getPageForUrl(browser, url: url);
  if (isNew) {
    await page.goto(url, wait: Until.domContentLoaded);
  }
  await page.bringToFront();
  return page;
}

String getExtensionOrigin(Browser browser) {
  final chromeExtension = 'chrome-extension:';
  final extensionUrl = _getUrlsInBrowser(
    browser,
  ).firstWhere((url) => url.contains(chromeExtension));
  final urlSegments = p.split(extensionUrl);
  final extensionId = urlSegments[urlSegments.indexOf(chromeExtension) + 1];
  return '$chromeExtension//$extensionId';
}

void _saveConsoleMsg({
  required ConsoleSource source,
  required String type,
  required String msg,
}) {
  if (msg.isEmpty) return;
  final conciseMsg = msg.startsWith('JSHandle:') ? msg.substring(9) : msg;
  final formatted = 'console.$type: $conciseMsg';
  switch (source) {
    case ConsoleSource.background:
      _backgroundLogs.add(formatted);
      break;
    case ConsoleSource.devTools:
      _devToolsLogs.add(formatted);
      break;
    case ConsoleSource.worker:
      _workerLogs.add(formatted);
      break;
  }
}

void _logConsoleMsgsOnFailure() {
  if (_backgroundLogs.isNotEmpty) {
    printOnFailure(['Background Page logs:', ..._backgroundLogs].join('\n'));
  }
  if (_workerLogs.isNotEmpty) {
    printOnFailure(['Service Worker logs:', ..._workerLogs].join('\n'));
  }
  if (_devToolsLogs.isNotEmpty) {
    printOnFailure(['Chrome DevTools logs:', ..._devToolsLogs].join('\n'));
  }
}

Iterable<String> _getUrlsInBrowser(Browser browser) {
  return browser.targets.map((target) => target.url);
}

Future<Page> _getPageForUrl(Browser browser, {required String url}) {
  final pageTarget = browser.targets.firstWhere((target) => target.url == url);
  return pageTarget.page;
}

Future<void> _clearStorage({Worker? worker, Page? backgroundPage}) async {
  return evaluate(
    _clearStorageJs(isMV3: worker != null),
    worker: worker,
    backgroundPage: backgroundPage,
  ).catchError((_) {});
}

String _clickIconJs({bool isMV3 = false}) =>
    '''
  async () => {
    const activeTabs = await chrome.tabs.query({ active: true }, (tabs) => {
      const tab = tabs[0];
      chrome.${isMV3 ? 'action' : 'browserAction'}.onClicked.dispatch(tab);
    });
  }
''';

String _clearStorageJs({required bool isMV3}) =>
    '''
    async () => {
      await chrome.storage.local.clear();
      ${isMV3 ? 'await chrome.storage.session.clear();' : ''}
      return true;
    }
''';
