// 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')
import 'dart:async';
import 'dart:io';

import 'package:glob/glob.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(new Glob("*", context: p.url).list, throwsStateError);
    });

    test("reports exceptions for non-existent case-sensitive directories", () {
      expect(new Glob("non/existent/**", caseSensitive: true).list().toList(),
          throwsA(new isInstanceOf<FileSystemException>()));
    });

    test("reports exceptions for non-existent case-insensitive directories",
        () {
      expect(new Glob("non/existent/**", caseSensitive: false).list().toList(),
          throwsA(new isInstanceOf<FileSystemException>()));
    });
  });

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

    test("reports exceptions for non-existent case-sensitive directories", () {
      expect(new Glob("non/existent/**", caseSensitive: true).listSync,
          throwsA(new isInstanceOf<FileSystemException>()));
    });

    test("reports exceptions for non-existent case-insensitive directories",
        () {
      expect(new Glob("non/existent/**", caseSensitive: false).listSync,
          throwsA(new isInstanceOf<FileSystemException>()));
    });
  });

  group("when case-sensitive", () {
    test("lists literals case-sensitively", () {
      expect(new Glob("foo/BAZ/qux", caseSensitive: true).listSync,
          throwsA(new isInstanceOf<FileSystemException>()));
    });

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

    test("options preserve case-sensitivity", () {
      expect(new Glob("foo/{BAZ,ZAP}/qux", caseSensitive: true).listSync,
          throwsA(new isInstanceOf<FileSystemException>()));
    });
  });

  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 new 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 new 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 FutureOr<List<String>> ListFn(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 callback(ListFn listFn)) {
  group("async", () {
    callback((pattern, {recursive: false, followLinks: true, caseSensitive}) {
      var glob =
          new 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 =
          new Glob(pattern, recursive: recursive, caseSensitive: caseSensitive);

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