// Copyright 2013 The Flutter Authors. 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:js_interop';

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

import '../common/spy.dart';

@JS('window._flutter_internal_on_benchmark')
external set jsBenchmarkValueCallback(JSAny? object);

void main() {
  internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
  group('$Profiler', () {
    _profilerTests();
  });

  group('$Instrumentation', () {
    _instrumentationTests();
  });
}

void _profilerTests() {
  final List<String> warnings = <String>[];
  late void Function(String) oldPrintWarning;

  setUpAll(() {
    oldPrintWarning = printWarning;
    printWarning = (String warning) {
      warnings.add(warning);
    };
  });

  setUp(() {
    warnings.clear();
    Profiler.isBenchmarkMode = true;
    Profiler.ensureInitialized();
  });

  tearDownAll(() {
    printWarning = oldPrintWarning;
  });

  tearDown(() {
    jsBenchmarkValueCallback = null;
    ui_web.benchmarkValueCallback = null;
    Profiler.isBenchmarkMode = false;
  });

  test('works when there is no listener', () {
    expect(() => Profiler.instance.benchmark('foo', 123), returnsNormally);
  });

  test('can listen to benchmarks', () {
    final List<BenchmarkDatapoint> data = <BenchmarkDatapoint>[];
    ui_web.benchmarkValueCallback = (String name, double value) {
      data.add((name, value));
    };

    Profiler.instance.benchmark('foo', 123);
    expect(data, <BenchmarkDatapoint>[('foo', 123)]);
    data.clear();

    Profiler.instance.benchmark('bar', 0.0125);
    expect(data, <BenchmarkDatapoint>[('bar', 0.0125)]);
    data.clear();

    // Remove listener and make sure nothing breaks and the data isn't being
    // sent to the old callback anymore.
    ui_web.benchmarkValueCallback = null;
    expect(() => Profiler.instance.benchmark('baz', 99.999), returnsNormally);
    expect(data, isEmpty);
  });

  // TODO(mdebbar): Remove this group once the JS API is removed.
  // https://github.com/flutter/flutter/issues/127395
  group('[JS API]', () {
    test('can listen to benchmarks', () {
      final List<BenchmarkDatapoint> data = <BenchmarkDatapoint>[];
      jsBenchmarkValueCallback = (String name, double value) {
        data.add((name, value));
      }.toJS;

      Profiler.instance.benchmark('foo', 123);
      expect(warnings, hasLength(1));
      expect(warnings.single, contains('deprecated'));
      expect(warnings.single, contains('benchmarkValueCallback'));
      expect(warnings.single, contains('dart:ui_web'));
      warnings.clear();

      expect(data, <BenchmarkDatapoint>[('foo', 123)]);
      data.clear();

      Profiler.instance.benchmark('bar', 0.0125);
      expect(data, <BenchmarkDatapoint>[('bar', 0.0125)]);
      data.clear();

      // Remove listener and make sure nothing breaks and the data isn't being
      // sent to the old callback anymore.
      jsBenchmarkValueCallback = null;
      expect(() => Profiler.instance.benchmark('baz', 99.999), returnsNormally);
      expect(data, isEmpty);
    });

    test('throws on wrong listener type', () {
      final List<BenchmarkDatapoint> data = <BenchmarkDatapoint>[];

      // Wrong callback signature.
      jsBenchmarkValueCallback = (double value) {
        data.add(('bad', value));
      }.toJS;
      expect(
        () => Profiler.instance.benchmark('foo', 123),

        // dart2js throws a NoSuchMethodError, dart2wasm throws a TypeError here.
        // Just make sure it throws an error in this case.
        throwsA(isA<Error>()),
      );
      expect(data, isEmpty);

      // Not even a callback.
      jsBenchmarkValueCallback = 'string'.toJS;
      expect(
        () => Profiler.instance.benchmark('foo', 123),
        // dart2js throws a TypeError, while dart2wasm throws an explicit
        // exception.
        throwsA(anything),
      );
    });

    test('can be combined with ui_web API', () {
      final List<BenchmarkDatapoint> uiWebData = <BenchmarkDatapoint>[];
      final List<BenchmarkDatapoint> jsData = <BenchmarkDatapoint>[];

      ui_web.benchmarkValueCallback = (String name, double value) {
        uiWebData.add((name, value));
      };
      jsBenchmarkValueCallback = (String name, double value) {
        jsData.add((name, value));
      }.toJS;

      Profiler.instance.benchmark('foo', 123);
      expect(warnings, hasLength(1));
      expect(warnings.single, contains('deprecated'));
      expect(warnings.single, contains('benchmarkValueCallback'));
      expect(warnings.single, contains('dart:ui_web'));
      warnings.clear();

      expect(uiWebData, <BenchmarkDatapoint>[('foo', 123)]);
      expect(jsData, <BenchmarkDatapoint>[('foo', 123)]);
      uiWebData.clear();
      jsData.clear();

      Profiler.instance.benchmark('bar', 0.0125);
      expect(uiWebData, <BenchmarkDatapoint>[('bar', 0.0125)]);
      expect(jsData, <BenchmarkDatapoint>[('bar', 0.0125)]);
      uiWebData.clear();
      jsData.clear();

      ui_web.benchmarkValueCallback = null;
      jsBenchmarkValueCallback = null;
      expect(() => Profiler.instance.benchmark('baz', 99.999), returnsNormally);
      expect(uiWebData, isEmpty);
      expect(jsData, isEmpty);
    });
  });
}

void _instrumentationTests() {
  setUp(() {
    Instrumentation.enabled = false;
  });

  tearDown(() {
    Instrumentation.enabled = false;
  });

  test('when disabled throws instead of initializing', () {
    expect(() => Instrumentation.instance, throwsStateError);
  });

  test('when disabled throws instead of incrementing counter', () {
    Instrumentation.enabled = true;
    final Instrumentation instrumentation = Instrumentation.instance;
    Instrumentation.enabled = false;
    expect(() => instrumentation.incrementCounter('test'), throwsStateError);
  });

  test('when enabled increments counter', () {
    final ZoneSpy spy = ZoneSpy();
    spy.run(() {
      Instrumentation.enabled = true;
      final Instrumentation instrumentation = Instrumentation.instance;
      expect(instrumentation.debugPrintTimer, isNull);
      instrumentation.incrementCounter('foo');
      expect(instrumentation.debugPrintTimer, isNotNull);
      instrumentation.incrementCounter('foo');
      instrumentation.incrementCounter('bar');
      expect(spy.printLog, isEmpty);

      expect(instrumentation.debugPrintTimer, isNotNull);
      spy.fakeAsync.elapse(const Duration(seconds: 2));
      expect(instrumentation.debugPrintTimer, isNull);
      expect(spy.printLog, hasLength(1));
      expect(
        spy.printLog.single,
        'Engine counters:\n'
        '  bar: 1\n'
        '  foo: 2\n',
      );
    });
  });
}

typedef BenchmarkDatapoint = (String, double);
