'use strict';

// This script depends on the following scripts:
//    resources/test-helpers.js
//    resources/collecting-file-system-observer.js
//    resources/change-observer-scope-test.js
//    script-tests/FileSystemObserver-writable-file-stream.js

promise_test(async t => {
  try {
    const observer = new FileSystemObserver(() => {});
  } catch {
    assert_unreached();
  }
}, 'Creating a FileSystemObserver from a supported global succeeds');

directory_test(async (t, root_dir) => {
  const observer = new FileSystemObserver(() => {});
  try {
    observer.unobserve(root_dir);
  } catch {
    assert_unreached();
  }
}, 'Calling unobserve() without a corresponding observe() shouldn\'t throw');

directory_test(async (t, root_dir) => {
  const observer = new FileSystemObserver(() => {});
  try {
    observer.unobserve(root_dir);
    observer.unobserve(root_dir);
  } catch {
    assert_unreached();
  }
}, 'unobserve() is idempotent');

promise_test(async t => {
  const observer = new FileSystemObserver(() => {});
  try {
    observer.disconnect();
  } catch {
    assert_unreached();
  }
}, 'Calling disconnect() without observing shouldn\'t throw');

promise_test(async t => {
  const observer = new FileSystemObserver(() => {});
  try {
    observer.disconnect();
    observer.disconnect();
  } catch {
    assert_unreached();
  }
}, 'disconnect() is idempotent');

directory_test(async (t, root_dir) => {
  const observer = new FileSystemObserver(() => {});

  // Create a `FileSystemFileHandle` and delete its underlying file entry.
  const file = await root_dir.getFileHandle(getUniqueName(), {create: true});
  await file.remove();

  await promise_rejects_dom(t, 'NotFoundError', observer.observe(file));
}, 'observe() fails when file does not exist');

directory_test(async (t, root_dir) => {
  const observer = new FileSystemObserver(() => {});

  // Create a `FileSystemDirectoryHandle` and delete its underlying file entry.
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});
  await dir.remove();

  await promise_rejects_dom(t, 'NotFoundError', observer.observe(dir));
}, 'observe() fails when directory does not exist');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const path of scope_test.in_scope_paths(recursive)) {
      const observer = new CollectingFileSystemObserver(t, root_dir);
      await observer.observe([watched_handle], {recursive});

      // Create `file`.
      const file = await path.createHandle();

      // Expect one "appeared" event to happen on `file`.
      const records = await observer.getRecords();
      await assert_records_equal(
          watched_handle, records,
          [appearedEvent(file, path.relativePathComponents())]);

      observer.disconnect();
    }
  }
}, 'Creating a file through FileSystemDirectoryHandle.getFileHandle is reported as an "appeared" event if in scope');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const path of scope_test.in_scope_paths(recursive)) {
      const file = await path.createHandle();

      const observer = new CollectingFileSystemObserver(t, root_dir);
      await observer.observe([watched_handle], {recursive});

      // Remove `file`.
      await file.remove();

      // Expect one "disappeared" event to happen on `file`.
      const records = await observer.getRecords();
      await assert_records_equal(
          watched_handle, records,
          [disappearedEvent(path.relativePathComponents())]);

      observer.disconnect();
    }
  }
}, 'Removing a file through FileSystemFileHandle.remove is reported as an "disappeared" event if in scope');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const path of scope_test.out_of_scope_paths(recursive)) {
      const observer = new CollectingFileSystemObserver(t, root_dir);
      await observer.observe([watched_handle], {recursive});

      // Create and remove `file`.
      const file = await path.createHandle();
      await file.remove();

      // Expect the observer to receive no events.
      const records = await observer.getRecords();
      await assert_records_equal(watched_handle, records, []);

      observer.disconnect();
    }
  }
}, 'Events outside the watch scope are not sent to the observer\'s callback');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const src of scope_test.in_scope_paths(recursive)) {
      for await (const dest of scope_test.in_scope_paths(recursive)) {
        const file = await src.createHandle();

        const observer = new CollectingFileSystemObserver(t, root_dir);
        await observer.observe([watched_handle], {recursive});

        // Move `file`.
        await file.move(dest.parentHandle(), dest.fileName());

        // Expect one "moved" event to happen on `file`.
        const records = await observer.getRecords();
        await assert_records_equal(
            watched_handle, records, [movedEvent(
                                         file, dest.relativePathComponents(),
                                         src.relativePathComponents())]);

        observer.disconnect();
      }
    }
  }
}, 'Moving a file through FileSystemFileHandle.move is reported as a "moved" event if destination and source are in scope');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const src of scope_test.out_of_scope_paths(recursive)) {
      for await (const dest of scope_test.out_of_scope_paths(recursive)) {
        const file = await src.createHandle();

        const observer = new CollectingFileSystemObserver(t, root_dir);
        await observer.observe([watched_handle], {recursive});

        // Move `file`.
        await file.move(dest.parentHandle(), dest.fileName());

        // Expect the observer to not receive any events.
        const records = await observer.getRecords();
        await assert_records_equal(watched_handle, records, []);
      }
    }
  }
}, 'Moving a file through FileSystemFileHandle.move is not reported if destination and source are not in scope');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const src of scope_test.out_of_scope_paths(recursive)) {
      for await (const dest of scope_test.in_scope_paths(recursive)) {
        const file = await src.createHandle();

        const observer = new CollectingFileSystemObserver(t, root_dir);
        await observer.observe([watched_handle], {recursive});

        // Move `file`.
        await file.move(dest.parentHandle(), dest.fileName());

        // Expect one "appeared" event to happen on `file`.
        const records = await observer.getRecords();
        await assert_records_equal(
            watched_handle, records,
            [appearedEvent(file, dest.relativePathComponents())]);
      }
    }
  }
}, 'Moving a file through FileSystemFileHandle.move is reported as a "appeared" event if only destination is in scope');

directory_test(async (t, root_dir) => {
  const dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  const scope_test = new ScopeTest(t, dir);
  const watched_handle = await scope_test.watched_handle();

  for (const recursive of [false, true]) {
    for await (const src of scope_test.in_scope_paths(recursive)) {
      for await (const dest of scope_test.out_of_scope_paths(recursive)) {
        // These both point to the same underlying file entry initially until
        // move is called on `fileToMove`. `file` is kept so that we have a
        // handle that still points at the source file entry.
        const file = await src.createHandle();
        const fileToMove = await src.createHandle();

        const observer = new CollectingFileSystemObserver(t, root_dir);
        await observer.observe([watched_handle], {recursive});

        // Move `fileToMove`.
        await fileToMove.move(dest.parentHandle(), dest.fileName());

        // Expect one "disappeared" event to happen on `file`.
        const records = await observer.getRecords();
        await assert_records_equal(
            watched_handle, records,
            [disappearedEvent(src.relativePathComponents())]);
      }
    }
  }
}, 'Moving a file through FileSystemFileHandle.move is reported as a "disappeared" event if only source is in scope');

// Wraps a `CollectingFileSystemObserver` and disconnects the observer after it's
// received `num_of_records_to_observe`.
class DisconnectingFileSystemObserver {
  #collectingObserver;

  #num_of_records_to_observe;

  #called_disconnect = false;
  #records_observed_count = 0;

  constructor(test, root_dir, num_of_records_to_observe) {
    this.#collectingObserver = new CollectingFileSystemObserver(
        test, root_dir, this.#callback.bind(this));
    this.#num_of_records_to_observe = num_of_records_to_observe;
  }

  #callback(records, observer) {
    this.#records_observed_count += records.length;

    const called_disconnect = this.#called_disconnect;

    // Call `disconnect` once after we've received `num_of_records_to_observe`.
    if (!called_disconnect &&
        this.#records_observed_count >= this.#num_of_records_to_observe) {
      observer.disconnect();
      this.#called_disconnect = true;
    }

    return {called_disconnect};
  }

  getRecordsWithCallbackInfo() {
    return this.#collectingObserver.getRecordsWithCallbackInfo();
  }

  observe(handles) {
    return this.#collectingObserver.observe(handles);
  }
}


directory_test(async (t, root_dir) => {
  const total_files_to_create = 100;

  const child_dir =
      await root_dir.getDirectoryHandle(getUniqueName(), {create: true});

  // Create a `FileSystemObserver` that will disconnect after its
  // received half of the total files we're going to create.
  const observer = new DisconnectingFileSystemObserver(
      t, root_dir, total_files_to_create / 2);

  // Observe the child directory and create files in it.
  await observer.observe([child_dir]);
  for (let i = 0; i < total_files_to_create; i++) {
    child_dir.getFileHandle(`file${i}`, {create: true});
  }

  // Wait for `disconnect` to be called.
  const records_with_disconnect_state =
      await observer.getRecordsWithCallbackInfo();

  // No observations should have been received after disconnected has been
  // called.
  assert_false(
      records_with_disconnect_state.some(
          ({called_disconnect}) => called_disconnect),
      'Received records after disconnect.');
}, 'Observations stop after disconnect()');

directory_test(async (t, root_dir) => {
  const num_of_child_dirs = 5;
  const num_files_to_create_per_directory = 100;
  const total_files_to_create =
      num_files_to_create_per_directory * num_of_child_dirs;

  const child_dirs = await createDirectoryHandles(
      root_dir, getUniqueName(), getUniqueName(), getUniqueName());

  // Create a `FileSystemObserver` that will disconnect after its received half
  // of the total files we're going to create.
  const observer = new DisconnectingFileSystemObserver(
      t, root_dir, total_files_to_create / 2);

  // Observe the child directories and create files in them.
  await observer.observe(child_dirs);
  for (let i = 0; i < num_files_to_create_per_directory; i++) {
    child_dirs.forEach(
        child_dir => child_dir.getFileHandle(`file${i}`, {create: true}));
  }

  // Wait for `disconnect` to be called.
  const records_with_disconnect_state =
      await observer.getRecordsWithCallbackInfo();

  // No observations should have been received after disconnected has been
  // called.
  assert_false(
      records_with_disconnect_state.some(
          ({called_disconnect}) => called_disconnect),
      'Received records after disconnect.');
}, 'Observations stop for all observed handles after disconnect()');
