// Copyright (c) 2025, 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 'package:test/test.dart';

import '../utils.dart';

void linkTests({required bool isNative}) {
  for (var i = 0; i != runsPerTest; ++i) {
    _linkTests(isNative: isNative);
  }
}

void _linkTests({required bool isNative}) {
  test('notifies when a link is added', () async {
    createDir('targets');
    createDir('links');
    writeFile('targets/a.target');
    await startWatcher(path: 'links');

    writeLink(
        link: 'links/a.link', target: 'targets/a.target', unawaitedAsync: true);

    await expectAddEvent('links/a.link');
  });

  test(
      'notifies when a link is replaced with a link to a different target '
      'with the same contents', () async {
    createDir('targets');
    createDir('links');
    writeFile('targets/a.target');
    sleepUntilNewModificationTime();
    writeFile('targets/b.target');
    writeLink(link: 'links/a.link', target: 'targets/a.target');
    await startWatcher(path: 'links');

    deleteLink('links/a.link');
    writeLink(link: 'links/a.link', target: 'targets/b.target');

    await expectModifyEvent('links/a.link');
  });

  test(
      'notifies when a link is replaced with a link to a different target '
      'with different contents', () async {
    writeFile('targets/a.target', contents: 'a');
    writeFile('targets/b.target', contents: 'ab');
    writeLink(link: 'links/a.link', target: 'targets/a.target');
    await startWatcher(path: 'links');

    deleteLink('links/a.link');
    writeLink(link: 'links/a.link', target: 'targets/b.target');

    await expectModifyEvent('links/a.link');
  });

  test('does not notify when a link target is modified', () async {
    createDir('targets');
    createDir('links');
    writeFile('targets/a.target');
    writeLink(link: 'links/a.link', target: 'targets/a.target');
    await startWatcher(path: 'links');
    writeFile('targets/a.target', contents: 'modified');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectNoEvents();
    } else {
      await expectModifyEvent('links/a.link');
    }
  });

  test('does not notify when a link target is removed', () async {
    createDir('targets');
    createDir('links');
    writeFile('targets/a.target');
    writeLink(link: 'links/a.link', target: 'targets/a.target');
    await startWatcher(path: 'links');

    deleteFile('targets/a.target');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectNoEvents();
    } else {
      await expectRemoveEvent('links/a.link');
    }
  });

  test('notifies when a link is moved within the watched directory', () async {
    createDir('targets');
    createDir('links');
    writeFile('targets/a.target');
    writeLink(link: 'links/a.link', target: 'targets/a.target');
    await startWatcher(path: 'links');

    renameLink('links/a.link', 'links/b.link');

    await inAnyOrder(
        [isAddEvent('links/b.link'), isRemoveEvent('links/a.link')]);
  });

  test('notifies when a link to an empty directory is added', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    await startWatcher(path: 'links');

    writeLink(
        link: 'links/a.link',
        target: 'targets/a.targetdir',
        unawaitedAsync: true);

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectAddEvent('links/a.link');
    } else {
      await expectNoEvents();
    }
  });

  test(
      'does not notify about directory contents '
      'when a link to a directory is added', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    writeFile('targets/a.targetdir/a.target');
    await startWatcher(path: 'links');

    writeLink(
        link: 'links/a.link',
        target: 'targets/a.targetdir',
        unawaitedAsync: true);

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectAddEvent('links/a.link');
    } else {
      await expectAddEvent('links/a.link/a.target');
    }
  });

  test('notifies when a file is added to a linked directory', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    writeLink(link: 'links/a.link', target: 'targets/a.targetdir');
    await startWatcher(path: 'links');

    writeFile('targets/a.targetdir/a.txt');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectNoEvents();
    } else {
      await expectAddEvent('links/a.link/a.txt');
    }
  });

  test('notifies when a file is added to a newly linked directory', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    await startWatcher(path: 'links');

    writeLink(link: 'links/a.link', target: 'targets/a.targetdir');
    writeFile('targets/a.targetdir/a.txt');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectAddEvent('links/a.link');
    } else {
      await expectAddEvent('links/a.link/a.txt');
    }
  });

  test(
      'notifies about linked directory contents when a directory with a linked '
      'subdirectory is moved in', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    createDir('watched');
    writeFile('targets/a.targetdir/a.txt');
    writeLink(link: 'links/a.link', target: 'targets/a.targetdir');
    await startWatcher(path: 'watched');

    renameDir('links', 'watched/links');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectAddEvent('watched/links/a.link');
    } else {
      await expectAddEvent('watched/links/a.link/a.txt');
    }
  });

  test(
      'notifies about linked directory contents when a directory with a linked '
      'subdirectory containing a link loop is moved in', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    createDir('watched');
    writeFile('targets/a.targetdir/a.txt');
    writeLink(link: 'links/a.link', target: 'targets/a.targetdir');
    writeLink(
        link: 'targets/a.targetdir/cycle.link', target: 'targets/a.targetdir');
    await startWatcher(path: 'watched');

    renameDir('links', 'watched/links');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectAddEvent('watched/links/a.link');
    } else {
      await expectAddEvent('watched/links/a.link/a.txt');
    }
    await expectNoEvents();
  });

  test(
      'notifies about linked directory contents when a directory with a linked '
      'subdirectory containing two link loops is moved in', () async {
    createDir('targets');
    createDir('links');
    createDir('targets/a.targetdir');
    createDir('watched');
    writeFile('targets/a.targetdir/a.txt');
    writeLink(link: 'links/a.link', target: 'targets/a.targetdir');
    writeLink(
        link: 'targets/a.targetdir/cycle1.link', target: 'targets/a.targetdir');
    writeLink(
        link: 'targets/a.targetdir/cycle2.link', target: 'targets/a.targetdir');
    await startWatcher(path: 'watched');

    renameDir('links', 'watched/links');

    // Native watchers treat links as files, polling watcher polls through them.
    if (isNative) {
      await expectAddEvent('watched/links/a.link');
    } else {
      await expectAddEvent('watched/links/a.link/a.txt');
    }
    await expectNoEvents();
  });

  test('is not slow in a directory with many link loops', () async {
    createDir('links');
    writeLink(link: 'links/a/cycle1.link', target: 'links/a');
    writeLink(link: 'links/a/cycle2.link', target: 'links/a');
    writeLink(link: 'links/a/cycle3.link', target: 'links/a');
    writeLink(link: 'links/a/cycle4.link', target: 'links/a');
    writeLink(link: 'links/a/cycle5.link', target: 'links/a');
    writeLink(link: 'links/a/cycle6.link', target: 'links/a');
    writeLink(link: 'links/a/cycle7.link', target: 'links/a');

    final stopwatch = Stopwatch()..start();
    await startWatcher(path: 'links');
    expect(stopwatch.elapsedMilliseconds, lessThan(500));
  });
}
