// Copyright 2014 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:async';

import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/devtools_launcher.dart';
import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;

import '../src/common.dart';
import '../src/fake_process_manager.dart';
import '../src/fake_vm_services.dart';
import '../src/fakes.dart';

final vm_service.Isolate isolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kResume,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[],
  libraries: <vm_service.LibraryRef>[
    vm_service.LibraryRef(
      id: '1',
      uri: 'file:///hello_world/main.dart',
      name: '',
    ),
  ],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
  isSystemIsolate: false,
  isolateFlags: <vm_service.IsolateFlag>[],
  extensionRPCs: <String>['ext.flutter.connectedVmServiceUri'],
);

final FakeVmServiceRequest listViews = FakeVmServiceRequest(
  method: kListViewsMethod,
  jsonResponse: <String, Object>{
    'views': <Object>[
      FlutterView(
        id: 'a',
        uiIsolate: isolate,
      ).toJson(),
    ],
  },
);

void main() {
  Cache.flutterRoot = '';

  testWithoutContext('Does not serve devtools if launcher is null', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      null,
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    await handler.serveAndAnnounceDevTools(flutterDevices: <FlutterDevice>[]);

    expect(handler.activeDevToolsServer, null);
  });

  testWithoutContext('Does not serve devtools if ResidentRunner does not support the service protocol', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher(),
      FakeResidentRunner()..supportsServiceProtocol = false,
      BufferLogger.test(),
    );

    await handler.serveAndAnnounceDevTools(flutterDevices: <FlutterDevice>[]);

    expect(handler.activeDevToolsServer, null);
  });

  testWithoutContext('Can use devtools with existing devtools URI', () async {
    final DevtoolsServerLauncher launcher = DevtoolsServerLauncher(
      processManager: FakeProcessManager.empty(),
      dartExecutable: 'dart',
      logger: BufferLogger.test(),
      botDetector: const FakeBotDetector(false),
    );
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      // Uses real devtools instance which should be a no-op if
      // URI is already set.
      launcher,
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    await handler.serveAndAnnounceDevTools(
      devToolsServerAddress: Uri.parse('http://localhost:8181'),
      flutterDevices: <FlutterDevice>[],
    );

    expect(handler.activeDevToolsServer!.host, 'localhost');
    expect(handler.activeDevToolsServer!.port, 8181);
  });

  testWithoutContext('serveAndAnnounceDevTools with attached device does not fail on null vm service', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()
        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
        ..devToolsUrl = Uri.parse('http://localhost:8080'),
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    // VM Service is intentionally null
    final FakeFlutterDevice device = FakeFlutterDevice();

    await handler.serveAndAnnounceDevTools(
      flutterDevices: <FlutterDevice>[device],
    );
  });

  testWithoutContext('serveAndAnnounceDevTools with invokes devtools and vm_service setter', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()
        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
        ..devToolsUrl = Uri.parse('http://localhost:8080'),
      FakeResidentRunner(),
      BufferLogger.test(),
    );
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        }
      ),
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        jsonResponse: isolate.toJson(),
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
      const FakeVmServiceRequest(
        method: 'streamCancel',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'ext.flutter.activeDevToolsServerAddress',
        args: <String, Object>{
          'isolateId': '1',
          'value': 'http://localhost:8080',
        },
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'ext.flutter.connectedVmServiceUri',
        args: <String, Object>{
          'isolateId': '1',
          'value': 'http://localhost:1234',
        },
      ),
    ], httpAddress: Uri.parse('http://localhost:1234'));

    final FakeFlutterDevice device = FakeFlutterDevice()
      ..vmService = fakeVmServiceHost.vmService;

    await handler.serveAndAnnounceDevTools(
      flutterDevices: <FlutterDevice>[device],
    );
  });

  testWithoutContext('serveAndAnnounceDevTools will bail if launching devtools fails', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()..activeDevToolsServer = null,
      FakeResidentRunner(),
      BufferLogger.test(),
    );
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[], httpAddress: Uri.parse('http://localhost:1234'));

    final FakeFlutterDevice device = FakeFlutterDevice()
      ..vmService = fakeVmServiceHost.vmService;

    await handler.serveAndAnnounceDevTools(
      flutterDevices: <FlutterDevice>[device],
    );
  });

  testWithoutContext('serveAndAnnounceDevTools with web device', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()
        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
        ..devToolsUrl = Uri.parse('http://localhost:8080'),
      FakeResidentRunner(),
      BufferLogger.test(),
    );
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        }
      ),
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        jsonResponse: isolate.toJson(),
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
      const FakeVmServiceRequest(
        method: 'streamCancel',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      const FakeVmServiceRequest(
        method: 'ext.flutter.activeDevToolsServerAddress',
        args: <String, Object>{
          'value': 'http://localhost:8080',
        },
      ),
      const FakeVmServiceRequest(
        method: 'ext.flutter.connectedVmServiceUri',
        args: <String, Object>{
          'value': 'http://localhost:1234',
        },
      ),
    ], httpAddress: Uri.parse('http://localhost:1234'));

    final FakeFlutterDevice device = FakeFlutterDevice()
      ..vmService = fakeVmServiceHost.vmService
      ..targetPlatform = TargetPlatform.web_javascript;

    await handler.serveAndAnnounceDevTools(
      flutterDevices: <FlutterDevice>[device],
    );
  });

  testWithoutContext('serveAndAnnounceDevTools with skips calling service extensions when VM service disappears', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
      FakeResidentRunner(),
      BufferLogger.test(),
    );
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      const FakeVmServiceRequest(
        method: kListViewsMethod,
        errorCode: RPCErrorCodes.kServiceDisappeared,
      ),
      const FakeVmServiceRequest(
        method: 'streamCancel',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
        errorCode: RPCErrorCodes.kServiceDisappeared,
      ),
    ], httpAddress: Uri.parse('http://localhost:1234'));

    final FakeFlutterDevice device = FakeFlutterDevice()
      ..vmService = fakeVmServiceHost.vmService;

    await handler.serveAndAnnounceDevTools(
      flutterDevices: <FlutterDevice>[device],
    );
  });

  testWithoutContext('serveAndAnnounceDevTools with multiple devices and VM service disappears on one', () async {
    final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()
        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
        ..devToolsUrl = Uri.parse('http://localhost:8080'),
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    final FakeVmServiceHost vmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      listViews,
      FakeVmServiceRequest(
        method: 'getIsolate',
        jsonResponse: isolate.toJson(),
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
      const FakeVmServiceRequest(
        method: 'streamCancel',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'ext.flutter.activeDevToolsServerAddress',
        args: <String, Object>{
          'isolateId': '1',
          'value': 'http://localhost:8080',
        },
      ),
      listViews,
      const FakeVmServiceRequest(
        method: 'ext.flutter.connectedVmServiceUri',
        args: <String, Object>{
          'isolateId': '1',
          'value': 'http://localhost:1234',
        },
      ),
    ], httpAddress: Uri.parse('http://localhost:1234'));

    final FakeVmServiceHost vmServiceHostThatDisappears = FakeVmServiceHost(requests: <VmServiceExpectation>[
      const FakeVmServiceRequest(
        method: 'streamListen',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
      ),
      const FakeVmServiceRequest(
        method: kListViewsMethod,
        errorCode: RPCErrorCodes.kServiceDisappeared,
      ),
      const FakeVmServiceRequest(
        method: 'streamCancel',
        args: <String, Object>{
          'streamId': 'Isolate',
        },
        errorCode: RPCErrorCodes.kServiceDisappeared,
      ),
    ], httpAddress: Uri.parse('http://localhost:5678'));

    await handler.serveAndAnnounceDevTools(
      flutterDevices: <FlutterDevice>[
        FakeFlutterDevice()
          ..vmService = vmServiceHostThatDisappears.vmService,
        FakeFlutterDevice()
          ..vmService = vmServiceHost.vmService,
      ],
    );
  });

  testWithoutContext('Does not launch devtools in browser if launcher is null', () async {
    final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      null,
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]);
    expect(handler.launchedInBrowser, isFalse);
    expect(handler.activeDevToolsServer, null);
  });

  testWithoutContext('Does not launch devtools in browser if ResidentRunner does not support the service protocol', () async {
    final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher(),
      FakeResidentRunner()..supportsServiceProtocol = false,
      BufferLogger.test(),
    );

    handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]);
    expect(handler.launchedInBrowser, isFalse);
    expect(handler.activeDevToolsServer, null);
  });

  testWithoutContext('launchDevToolsInBrowser launches after _devToolsLauncher.ready completes', () async {
    final Completer<void> completer = Completer<void>();
    final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()
        ..devToolsUrl = null
        // We need to set [activeDevToolsServer] to simulate the state we would
        // be in after serving devtools completes.
        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
        ..readyCompleter = completer,
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    expect(handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]), isTrue);
    expect(handler.launchedInBrowser, isFalse);

    completer.complete();
    // Await a short delay to give DevTools time to launch.
    await Future<void>.delayed(const Duration(microseconds: 100));

    expect(handler.launchedInBrowser, isTrue);
  });

  testWithoutContext('launchDevToolsInBrowser launches successfully', () async {
    final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
      FakeDevtoolsLauncher()
        ..devToolsUrl = Uri(host: 'localhost', port: 8080)
        ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
      FakeResidentRunner(),
      BufferLogger.test(),
    );

    expect(handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]), isTrue);
    expect(handler.launchedInBrowser, isTrue);
  });

  testWithoutContext('Converts a VM Service URI with a query parameter to a pretty display string', () {
    const String value = 'http://127.0.0.1:9100?uri=http%3A%2F%2F127.0.0.1%3A57922%2F_MXpzytpH20%3D%2F';
    final Uri uri = Uri.parse(value);

    expect(urlToDisplayString(uri), 'http://127.0.0.1:9100?uri=http://127.0.0.1:57922/_MXpzytpH20=/');
  });
}

class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
  @override
  DevToolsServerAddress? activeDevToolsServer;

  @override
  Uri? devToolsUrl;

  @override
  Future<DevToolsServerAddress?> serve() async => null;

  @override
  Future<void> get ready => readyCompleter.future;

  Completer<void> readyCompleter = Completer<void>()..complete();
}

class FakeResidentRunner extends Fake implements ResidentRunner {
  @override
  bool supportsServiceProtocol = true;

  @override
  bool reportedDebuggers = false;

  @override
  DebuggingOptions debuggingOptions = DebuggingOptions.disabled(BuildInfo.debug);
}

class FakeFlutterDevice extends Fake implements FlutterDevice {
  @override
  final Device device = FakeDevice();

  @override
  FlutterVmService? vmService;

  @override
  TargetPlatform targetPlatform = TargetPlatform.android_arm;
}

// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device {
  @override
  DartDevelopmentService get dds => FakeDartDevelopmentService();
}

class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
  bool started = false;
  bool disposed = false;

  @override
  final Uri uri = Uri.parse('http://127.0.0.1:1234/');

  @override
  Future<void> startDartDevelopmentService(
    Uri observatoryUri, {
    required Logger logger,
    int? hostPort,
    bool? ipv6,
    bool? disableServiceAuthCodes,
    bool cacheStartupProfile = false,
  }) async {
    started = true;
  }

  @override
  Future<void> shutdown() async {
    disposed = true;
  }

  @override
  void setExternalDevToolsUri(Uri uri) {}
}
