// Copyright (c) 2021, 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:async';
import 'dart:isolate';

import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/workspace/blaze.dart';
import 'package:analyzer/src/workspace/blaze_watcher.dart';
import 'package:analyzer_testing/resource_provider_mixin.dart';
import 'package:async/async.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'package:watcher/watcher.dart';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(BlazeWatcherTest);
  });
}

@reflectiveTest
class BlazeWatcherTest with ResourceProviderMixin {
  late final BlazeWorkspace workspace;

  void test_blazeFileWatcher() {
    _addResource('/workspace/${file_paths.blazeWorkspaceMarker}');
    var candidates = [
      convertPath('/workspace/blaze-bin/my/module/test1.dart'),
      convertPath('/workspace/blaze-genfiles/my/module/test1.dart'),
    ];
    var watcher = BlazeFilePoller(resourceProvider, candidates);

    // First do some tests with the first candidate path.
    var firstCandidate = _addResource(candidates[0]) as File;

    var event = watcher.poll()!;

    expect(event.type, ChangeType.ADD);
    expect(event.path, candidates[0]);

    modifyFile2(firstCandidate, 'const foo = 42;');

    event = watcher.poll()!;

    expect(event.type, ChangeType.MODIFY);
    expect(event.path, candidates[0]);

    _deleteResources([candidates[0]]);

    event = watcher.poll()!;

    expect(event.type, ChangeType.REMOVE);
    expect(event.path, candidates[0]);

    // Now check that if we add the *second* candidate, we'll get the
    // notification for it.
    _addResource(candidates[1]);

    event = watcher.poll()!;

    expect(event.type, ChangeType.ADD);
    expect(event.path, candidates[1]);

    // Next poll should be `null` since there were no changes.
    expect(watcher.poll(), isNull);
  }

  void test_blazeFileWatcherIsolate() async {
    _addResource('/workspace/${file_paths.blazeWorkspaceMarker}');
    var candidates1 = [
      convertPath('/workspace/blaze-bin/my/module/test1.dart'),
      convertPath('/workspace/blaze-genfiles/my/module/test1.dart'),
    ];
    var candidates2 = [
      convertPath('/workspace/blaze-bin/my/module/test2.dart'),
      convertPath('/workspace/blaze-genfiles/my/module/test2.dart'),
    ];
    var trigger = _MockPollTrigger();
    var recPort = ReceivePort();
    // Note that we provide below a dummy `ReceivePort` that will *not* be used.
    // We'll directly call `handleRequest` to avoid any problems with various
    // interleavings of async functions.
    var dummyRecPort = ReceivePort();
    var watcher = BlazeFileWatcherIsolate(
      dummyRecPort,
      recPort.sendPort,
      resourceProvider,
      pollTriggerFactory: (_) => trigger,
    )..start();
    var queue = StreamQueue(recPort);

    await queue.next as BlazeWatcherIsolateStarted;

    watcher.handleRequest(
      BlazeWatcherStartWatching(
        convertPath('/workspace'),
        BlazeSearchInfo(
          convertPath('/workspace/my/module/test1.dart'),
          candidates1,
        ),
      ),
    );
    watcher.handleRequest(
      BlazeWatcherStartWatching(
        convertPath('/workspace'),
        BlazeSearchInfo(
          convertPath('/workspace/my/module/test2.dart'),
          candidates2,
        ),
      ),
    );

    // First do some tests with the first candidate path.
    _addResource(candidates1[0]);

    trigger.controller.add('');
    var events = (await queue.next as BlazeWatcherEvents).events;

    expect(events, hasLength(1));
    expect(events[0].path, candidates1[0]);
    expect(events[0].type, ChangeType.ADD);

    // Now let's take a look at the second file.
    _addResource(candidates2[1]);

    trigger.controller.add('');
    events = (await queue.next as BlazeWatcherEvents).events;

    expect(events, hasLength(1));
    expect(events[0].path, candidates2[1]);
    expect(events[0].type, ChangeType.ADD);

    expect(watcher.numWatchedFiles(), 2);

    watcher.handleRequest(
      BlazeWatcherStopWatching(
        convertPath('/workspace'),
        convertPath('/workspace/my/module/test1.dart'),
      ),
    );

    expect(watcher.numWatchedFiles(), 1);

    watcher.handleRequest(
      BlazeWatcherStopWatching(
        convertPath('/workspace'),
        convertPath('/workspace/my/module/test2.dart'),
      ),
    );

    expect(watcher.numWatchedFiles(), 0);

    watcher.handleRequest(BlazeWatcherShutdownIsolate());
    await watcher.hasFinished;

    // We need to do this manually, since it's the callers responsibility to
    // close this port (the one owned by `watcher` should've been closed when
    // handling the "shutdown" request).
    recPort.close();
  }

  void test_blazeFileWatcherIsolate_multipleWorkspaces() async {
    _addResource('/workspace1/${file_paths.blazeWorkspaceMarker}');
    _addResource('/workspace2/${file_paths.blazeWorkspaceMarker}');
    var candidates1 = [
      convertPath('/workspace1/blaze-bin/my/module/test1.dart'),
      convertPath('/workspace1/blaze-genfiles/my/module/test1.dart'),
    ];
    var candidates2 = [
      convertPath('/workspace2/blaze-bin/my/module/test2.dart'),
      convertPath('/workspace2/blaze-genfiles/my/module/test2.dart'),
    ];
    _MockPollTrigger? trigger1;
    _MockPollTrigger? trigger2;
    _MockPollTrigger triggerFactory(String workspace) {
      if (workspace == convertPath('/workspace1')) {
        trigger1 = _MockPollTrigger();
        return trigger1!;
      } else if (workspace == convertPath('/workspace2')) {
        trigger2 = _MockPollTrigger();
        return trigger2!;
      } else {
        throw ArgumentError('Got unexpected workspace: `$workspace`');
      }
    }

    var recPort = ReceivePort();
    // Note that we provide below a dummy `ReceivePort` that will *not* be used.
    // We'll directly call `handleRequest` to avoid any problems with various
    // interleavings of async functions.
    var dummyRecPort = ReceivePort();
    var watcher = BlazeFileWatcherIsolate(
      dummyRecPort,
      recPort.sendPort,
      resourceProvider,
      pollTriggerFactory: triggerFactory,
    )..start();
    var queue = StreamQueue(recPort);

    await queue.next as BlazeWatcherIsolateStarted;

    watcher.handleRequest(
      BlazeWatcherStartWatching(
        convertPath('/workspace1'),
        BlazeSearchInfo(
          convertPath('/workspace1/my/module/test1.dart'),
          candidates1,
        ),
      ),
    );
    watcher.handleRequest(
      BlazeWatcherStartWatching(
        convertPath('/workspace2'),
        BlazeSearchInfo(
          convertPath('/workspace2/my/module/test2.dart'),
          candidates2,
        ),
      ),
    );

    // First do some tests with the first candidate path.
    _addResource(candidates1[0]);

    trigger1!.controller.add('');
    var events = (await queue.next as BlazeWatcherEvents).events;

    expect(events, hasLength(1));
    expect(events[0].path, candidates1[0]);
    expect(events[0].type, ChangeType.ADD);

    // Now let's take a look at the second file.
    _addResource(candidates2[1]);

    trigger2!.controller.add('');
    events = (await queue.next as BlazeWatcherEvents).events;

    expect(events, hasLength(1));
    expect(events[0].path, candidates2[1]);
    expect(events[0].type, ChangeType.ADD);

    expect(watcher.numWatchedFiles(), 2);

    watcher.handleRequest(
      BlazeWatcherStopWatching(
        convertPath('/workspace1'),
        convertPath('/workspace1/my/module/test1.dart'),
      ),
    );

    expect(watcher.numWatchedFiles(), 1);

    watcher.handleRequest(
      BlazeWatcherStopWatching(
        convertPath('/workspace2'),
        convertPath('/workspace2/my/module/test2.dart'),
      ),
    );

    expect(watcher.numWatchedFiles(), 0);

    watcher.handleRequest(BlazeWatcherShutdownIsolate());
    await watcher.hasFinished;

    // We need to do this manually, since it's the callers responsibility to
    // close this port (the one owned by `watcher` should've been closed when
    // handling the "shutdown" request).
    recPort.close();
  }

  void test_blazeFileWatcherWithFolder() {
    _addResource('/workspace/${file_paths.blazeWorkspaceMarker}');

    // The `_addResources`/`_deleteResources` functions recognize a folder by a
    // trailing `/`, but everywhere else we need to use normalized paths.
    void addFolder(path) => _addResource('$path/');
    void deleteFolder(path) => _deleteResources(['$path/']);

    var candidates = [
      convertPath('/workspace/blaze-out'),
      convertPath('/workspace/blaze-out'),
    ];
    var watcher = BlazeFilePoller(resourceProvider, candidates);

    // First do some tests with the first candidate path.
    addFolder(candidates[0]);
    var event = watcher.poll()!;

    expect(event.type, ChangeType.ADD);
    expect(event.path, candidates[0]);

    deleteFolder(candidates[0]);
    event = watcher.poll()!;

    expect(event.type, ChangeType.REMOVE);
    expect(event.path, candidates[0]);

    // Now check that if we add the *second* candidate, we'll get the
    // notification for it.
    addFolder(candidates[1]);
    event = watcher.poll()!;

    expect(event.type, ChangeType.ADD);
    expect(event.path, candidates[1]);

    // Next poll should be `null` since there were no changes.
    expect(watcher.poll(), isNull);
  }

  /// Creates a new file or directory from [resourcePath].
  Resource _addResource(String resourcePath) {
    if (resourcePath.endsWith('/')) {
      return newFolder(resourcePath.substring(0, resourcePath.length - 1));
    } else {
      return newFile(resourcePath, '');
    }
  }

  /// Create new files and directories from [paths].
  void _deleteResources(List<String> paths) {
    for (String path in paths) {
      if (path.endsWith('/')) {
        deleteFolder(path.substring(0, path.length - 1));
      } else {
        deleteFile(path);
      }
    }
  }
}

class _MockPollTrigger implements PollTrigger {
  final controller = StreamController<Object>();

  @override
  Stream<Object> get stream => controller.stream;

  @override
  void cancel() {
    return;
  }
}
