// Copyright (c) 2014, 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.

@TestOn('vm')
library;

import 'dart:async';
import 'dart:io';

import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:glob/src/utils.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

void main() {
  setUp(() async {
    await d.dir('foo', [
      d.file('bar'),
      d.dir('baz', [d.file('bang'), d.file('qux')])
    ]).create();
  });

  group('list()', () {
    test("fails if the context doesn't match the system context", () {
      expect(Glob('*', context: p.url).list, throwsStateError);
    });

    test('returns empty list for non-existent case-sensitive directories',
        () async {
      expect(await Glob('non/existent/**', caseSensitive: true).list().toList(),
          <Never>[]);
    });

    test('returns empty list for non-existent case-insensitive directories',
        () async {
      expect(
          await Glob('non/existent/**', caseSensitive: false).list().toList(),
          <Never>[]);
    });
  });

  group('listSync()', () {
    test("fails if the context doesn't match the system context", () {
      expect(Glob('*', context: p.url).listSync, throwsStateError);
    });

    test('returns empty list for non-existent case-sensitive directories', () {
      expect(
          Glob('non/existent/**', caseSensitive: true).listSync(), <Never>[]);
    });

    test('returns empty list for non-existent case-insensitive directories',
        () {
      expect(
          Glob('non/existent/**', caseSensitive: false).listSync(), <Never>[]);
    });
  });

  group('when case-sensitive', () {
    test('lists literals case-sensitively', () {
      expect(Glob('foo/BAZ/qux', caseSensitive: true).listSync(), <Never>[]);
    });

    test('lists ranges case-sensitively', () {
      expect(Glob('foo/[BX][A-Z]z/qux', caseSensitive: true).listSync(),
          <Never>[]);
    });

    test('options preserve case-sensitivity', () {
      expect(
          Glob('foo/{BAZ,ZAP}/qux', caseSensitive: true).listSync(), <Never>[]);
    });
  });

  syncAndAsync((ListFn list) {
    group('literals', () {
      test('lists a single literal', () async {
        expect(
            await list('foo/baz/qux'), equals([p.join('foo', 'baz', 'qux')]));
      });

      test('lists a non-matching literal', () async {
        expect(await list('foo/baz/nothing'), isEmpty);
      });
    });

    group('star', () {
      test('lists within filenames but not across directories', () async {
        expect(await list('foo/b*'),
            unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
      });

      test('lists the empy string', () async {
        expect(await list('foo/bar*'), equals([p.join('foo', 'bar')]));
      });
    });

    group('double star', () {
      test('lists within filenames', () async {
        expect(
            await list('foo/baz/**'),
            unorderedEquals(
                [p.join('foo', 'baz', 'qux'), p.join('foo', 'baz', 'bang')]));
      });

      test('lists the empty string', () async {
        expect(await list('foo/bar**'), equals([p.join('foo', 'bar')]));
      });

      test('lists recursively', () async {
        expect(
            await list('foo/**'),
            unorderedEquals([
              p.join('foo', 'bar'),
              p.join('foo', 'baz'),
              p.join('foo', 'baz', 'qux'),
              p.join('foo', 'baz', 'bang')
            ]));
      });

      test('combines with literals', () async {
        expect(
            await list('foo/ba**'),
            unorderedEquals([
              p.join('foo', 'bar'),
              p.join('foo', 'baz'),
              p.join('foo', 'baz', 'qux'),
              p.join('foo', 'baz', 'bang')
            ]));
      });

      test('lists recursively in the middle of a glob', () async {
        await d.dir('deep', [
          d.dir('a', [
            d.dir('b', [
              d.dir('c', [d.file('d'), d.file('long-file')]),
              d.dir('long-dir', [d.file('x')])
            ])
          ])
        ]).create();

        expect(
            await list('deep/**/?/?'),
            unorderedEquals([
              p.join('deep', 'a', 'b', 'c'),
              p.join('deep', 'a', 'b', 'c', 'd')
            ]));
      });
    });

    group('any char', () {
      test('matches a character', () async {
        expect(await list('foo/ba?'),
            unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
      });

      test("doesn't match a separator", () async {
        expect(await list('foo?bar'), isEmpty);
      });
    });

    group('range', () {
      test('matches a range of characters', () async {
        expect(await list('foo/ba[a-z]'),
            unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
      });

      test('matches a specific list of characters', () async {
        expect(await list('foo/ba[rz]'),
            unorderedEquals([p.join('foo', 'bar'), p.join('foo', 'baz')]));
      });

      test("doesn't match outside its range", () async {
        expect(
            await list('foo/ba[a-x]'), unorderedEquals([p.join('foo', 'bar')]));
      });

      test("doesn't match outside its specific list", () async {
        expect(
            await list('foo/ba[rx]'), unorderedEquals([p.join('foo', 'bar')]));
      });
    });

    test("the same file shouldn't be non-recursively listed multiple times",
        () async {
      await d.dir('multi', [
        d.dir('start-end', [d.file('file')])
      ]).create();

      expect(await list('multi/{start-*/f*,*-end/*e}'),
          equals([p.join('multi', 'start-end', 'file')]));
    });

    test("the same file shouldn't be recursively listed multiple times",
        () async {
      await d.dir('multi', [
        d.dir('a', [
          d.dir('b', [
            d.file('file'),
            d.dir('c', [d.file('file')])
          ]),
          d.dir('x', [
            d.dir('y', [d.file('file')])
          ])
        ])
      ]).create();

      expect(
          await list('multi/{*/*/*/file,a/**/file}'),
          unorderedEquals([
            p.join('multi', 'a', 'b', 'file'),
            p.join('multi', 'a', 'b', 'c', 'file'),
            p.join('multi', 'a', 'x', 'y', 'file')
          ]));
    });

    group('with symlinks', () {
      setUp(() async {
        await Link(p.join(d.sandbox, 'dir', 'link'))
            .create(p.join(d.sandbox, 'foo', 'baz'), recursive: true);
      });

      test('follows symlinks by default', () async {
        expect(
            await list('dir/**'),
            unorderedEquals([
              p.join('dir', 'link'),
              p.join('dir', 'link', 'bang'),
              p.join('dir', 'link', 'qux')
            ]));
      });

      test("doesn't follow symlinks with followLinks: false", () async {
        expect(await list('dir/**', followLinks: false),
            equals([p.join('dir', 'link')]));
      });

      test("shouldn't crash on broken symlinks", () async {
        await Directory(p.join(d.sandbox, 'foo')).delete(recursive: true);

        expect(await list('dir/**'), equals([p.join('dir', 'link')]));
      });
    });

    test('always lists recursively with recursive: true', () async {
      expect(
          await list('foo', recursive: true),
          unorderedEquals([
            'foo',
            p.join('foo', 'bar'),
            p.join('foo', 'baz'),
            p.join('foo', 'baz', 'qux'),
            p.join('foo', 'baz', 'bang')
          ]));
    });

    test('lists an absolute glob', () async {
      var pattern =
          separatorToForwardSlash(p.absolute(p.join(d.sandbox, 'foo/baz/**')));

      var result = await list(pattern);

      expect(
          result,
          unorderedEquals(
              [p.join('foo', 'baz', 'bang'), p.join('foo', 'baz', 'qux')]));
    });

    // Regression test for #4.
    test('lists an absolute case-insensitive glob', () async {
      var pattern =
          separatorToForwardSlash(p.absolute(p.join(d.sandbox, 'foo/Baz/**')));

      expect(
          await list(pattern, caseSensitive: false),
          unorderedEquals(
              [p.join('foo', 'baz', 'bang'), p.join('foo', 'baz', 'qux')]));
    });

    test('lists a subdirectory that sometimes exists', () async {
      await d.dir('top', [
        d.dir('dir1', [
          d.dir('subdir', [d.file('file')])
        ]),
        d.dir('dir2', [])
      ]).create();

      expect(await list('top/*/subdir/**'),
          equals([p.join('top', 'dir1', 'subdir', 'file')]));
    });

    group('when case-insensitive', () {
      test('lists literals case-insensitively', () async {
        expect(await list('foo/baz/qux', caseSensitive: false),
            equals([p.join('foo', 'baz', 'qux')]));
        expect(await list('foo/BAZ/qux', caseSensitive: false),
            equals([p.join('foo', 'baz', 'qux')]));
      });

      test('lists ranges case-insensitively', () async {
        expect(await list('foo/[bx][a-z]z/qux', caseSensitive: false),
            equals([p.join('foo', 'baz', 'qux')]));
        expect(await list('foo/[BX][A-Z]z/qux', caseSensitive: false),
            equals([p.join('foo', 'baz', 'qux')]));
      });

      test('options preserve case-insensitivity', () async {
        expect(await list('foo/{bar,baz}/qux', caseSensitive: false),
            equals([p.join('foo', 'baz', 'qux')]));
        expect(await list('foo/{BAR,BAZ}/qux', caseSensitive: false),
            equals([p.join('foo', 'baz', 'qux')]));
      });
    });
  });
}

typedef ListFn = FutureOr<List<String>> Function(String glob,
    {bool recursive, bool followLinks, bool? caseSensitive});

/// Runs [callback] in two groups with two values of [ListFn]: one that uses
/// `Glob.list`, one that uses `Glob.listSync`.
void syncAndAsync(FutureOr<void> Function(ListFn) callback) {
  group('async', () {
    callback((pattern, {recursive = false, followLinks = true, caseSensitive}) {
      var glob =
          Glob(pattern, recursive: recursive, caseSensitive: caseSensitive);

      return glob
          .list(root: d.sandbox, followLinks: followLinks)
          .map((entity) => p.relative(entity.path, from: d.sandbox))
          .toList();
    });
  });

  group('sync', () {
    callback((pattern, {recursive = false, followLinks = true, caseSensitive}) {
      var glob =
          Glob(pattern, recursive: recursive, caseSensitive: caseSensitive);

      return glob
          .listSync(root: d.sandbox, followLinks: followLinks)
          .map((entity) => p.relative(entity.path, from: d.sandbox))
          .toList();
    });
  });
}
