// Copyright (c) 2019, 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:convert';
import 'dart:typed_data';

import 'package:package_config/package_config_types.dart';
import 'package:package_config/src/errors.dart';
import 'package:package_config/src/package_config_json.dart';
import 'package:package_config/src/packages_file.dart' as packages;
import 'package:test/test.dart';

import 'src/util.dart';

void main() {
  group('.packages', () {
    test('valid', () {
      var packagesFile = '# Generated by pub yadda yadda\n'
          'foo:file:///foo/lib/\n'
          'bar:/bar/lib/\n'
          'baz:lib/\n';
      var result = packages.parse(utf8.encode(packagesFile),
          Uri.parse('file:///tmp/file.dart'), throwError);
      expect(result.version, 1);
      expect({for (var p in result.packages) p.name}, {'foo', 'bar', 'baz'});
      expect(result.resolve(pkg('foo', 'foo.dart')),
          Uri.parse('file:///foo/lib/foo.dart'));
      expect(result.resolve(pkg('bar', 'bar.dart')),
          Uri.parse('file:///bar/lib/bar.dart'));
      expect(result.resolve(pkg('baz', 'baz.dart')),
          Uri.parse('file:///tmp/lib/baz.dart'));

      var foo = result['foo']!;
      expect(foo, isNotNull);
      expect(foo.root, Uri.parse('file:///foo/'));
      expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
      expect(foo.languageVersion, LanguageVersion(2, 7));
      expect(foo.relativeRoot, false);
    });

    test('valid empty', () {
      var packagesFile = '# Generated by pub yadda yadda\n';
      var result = packages.parse(
          utf8.encode(packagesFile), Uri.file('/tmp/file.dart'), throwError);
      expect(result.version, 1);
      expect({for (var p in result.packages) p.name}, <String>{});
    });

    group('invalid', () {
      var baseFile = Uri.file('/tmp/file.dart');
      void testThrows(String name, String content) {
        test(name, () {
          expect(
              () => packages.parse(utf8.encode(content), baseFile, throwError),
              throwsA(isA<FormatException>()));
        });
        test('$name, handle error', () {
          var hadError = false;
          packages.parse(utf8.encode(content), baseFile, (error) {
            hadError = true;
            expect(error, isA<FormatException>());
          });
          expect(hadError, true);
        });
      }

      testThrows('repeated package name', 'foo:lib/\nfoo:lib\n');
      testThrows('no colon', 'foo\n');
      testThrows('empty package name', ':lib/\n');
      testThrows('dot only package name', '.:lib/\n');
      testThrows('dot only package name', '..:lib/\n');
      testThrows('invalid package name character', 'f\\o:lib/\n');
      testThrows('package URI', 'foo:package:bar/lib/');
      testThrows('location with query', 'f\\o:lib/?\n');
      testThrows('location with fragment', 'f\\o:lib/#\n');
    });
  });

  group('package_config.json', () {
    test('valid', () {
      var packageConfigFile = '''
        {
          "configVersion": 2,
          "packages": [
            {
              "name": "foo",
              "rootUri": "file:///foo/",
              "packageUri": "lib/",
              "languageVersion": "2.5",
              "nonstandard": true
            },
            {
              "name": "bar",
              "rootUri": "/bar/",
              "packageUri": "lib/",
              "languageVersion": "9999.9999"
            },
            {
              "name": "baz",
              "rootUri": "../",
              "packageUri": "lib/"
            },
            {
              "name": "noslash",
              "rootUri": "../noslash",
              "packageUri": "lib"
            }
          ],
          "generator": "pub",
          "other": [42]
        }
        ''';
      var config = parsePackageConfigBytes(
          // ignore: unnecessary_cast
          utf8.encode(packageConfigFile) as Uint8List,
          Uri.parse('file:///tmp/.dart_tool/file.dart'),
          throwError);
      expect(config.version, 2);
      expect({for (var p in config.packages) p.name},
          {'foo', 'bar', 'baz', 'noslash'});

      expect(config.resolve(pkg('foo', 'foo.dart')),
          Uri.parse('file:///foo/lib/foo.dart'));
      expect(config.resolve(pkg('bar', 'bar.dart')),
          Uri.parse('file:///bar/lib/bar.dart'));
      expect(config.resolve(pkg('baz', 'baz.dart')),
          Uri.parse('file:///tmp/lib/baz.dart'));

      var foo = config['foo']!;
      expect(foo, isNotNull);
      expect(foo.root, Uri.parse('file:///foo/'));
      expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
      expect(foo.languageVersion, LanguageVersion(2, 5));
      expect(foo.extraData, {'nonstandard': true});
      expect(foo.relativeRoot, false);

      var bar = config['bar']!;
      expect(bar, isNotNull);
      expect(bar.root, Uri.parse('file:///bar/'));
      expect(bar.packageUriRoot, Uri.parse('file:///bar/lib/'));
      expect(bar.languageVersion, LanguageVersion(9999, 9999));
      expect(bar.extraData, null);
      expect(bar.relativeRoot, false);

      var baz = config['baz']!;
      expect(baz, isNotNull);
      expect(baz.root, Uri.parse('file:///tmp/'));
      expect(baz.packageUriRoot, Uri.parse('file:///tmp/lib/'));
      expect(baz.languageVersion, null);
      expect(baz.relativeRoot, true);

      // No slash after root or package root. One is inserted.
      var noslash = config['noslash']!;
      expect(noslash, isNotNull);
      expect(noslash.root, Uri.parse('file:///tmp/noslash/'));
      expect(noslash.packageUriRoot, Uri.parse('file:///tmp/noslash/lib/'));
      expect(noslash.languageVersion, null);
      expect(noslash.relativeRoot, true);

      expect(config.extraData, {
        'generator': 'pub',
        'other': [42]
      });
    });

    test('valid other order', () {
      // The ordering in the file is not important.
      var packageConfigFile = '''
        {
          "generator": "pub",
          "other": [42],
          "packages": [
            {
              "languageVersion": "2.5",
              "packageUri": "lib/",
              "rootUri": "file:///foo/",
              "name": "foo"
            },
            {
              "packageUri": "lib/",
              "languageVersion": "9999.9999",
              "rootUri": "/bar/",
              "name": "bar"
            },
            {
              "packageUri": "lib/",
              "name": "baz",
              "rootUri": "../"
            }
          ],
          "configVersion": 2
        }
        ''';
      var config = parsePackageConfigBytes(
          // ignore: unnecessary_cast
          utf8.encode(packageConfigFile) as Uint8List,
          Uri.parse('file:///tmp/.dart_tool/file.dart'),
          throwError);
      expect(config.version, 2);
      expect({for (var p in config.packages) p.name}, {'foo', 'bar', 'baz'});

      expect(config.resolve(pkg('foo', 'foo.dart')),
          Uri.parse('file:///foo/lib/foo.dart'));
      expect(config.resolve(pkg('bar', 'bar.dart')),
          Uri.parse('file:///bar/lib/bar.dart'));
      expect(config.resolve(pkg('baz', 'baz.dart')),
          Uri.parse('file:///tmp/lib/baz.dart'));
      expect(config.extraData, {
        'generator': 'pub',
        'other': [42]
      });
    });

    // Check that a few minimal configurations are valid.
    // These form the basis of invalid tests below.
    var cfg = '"configVersion":2';
    var pkgs = '"packages":[]';
    var name = '"name":"foo"';
    var root = '"rootUri":"/foo/"';
    test('minimal', () {
      var config = parsePackageConfigBytes(
          // ignore: unnecessary_cast
          utf8.encode('{$cfg,$pkgs}') as Uint8List,
          Uri.parse('file:///tmp/.dart_tool/file.dart'),
          throwError);
      expect(config.version, 2);
      expect(config.packages, isEmpty);
    });
    test('minimal package', () {
      // A package must have a name and a rootUri, the remaining properties
      // are optional.
      var config = parsePackageConfigBytes(
          // ignore: unnecessary_cast
          utf8.encode('{$cfg,"packages":[{$name,$root}]}') as Uint8List,
          Uri.parse('file:///tmp/.dart_tool/file.dart'),
          throwError);
      expect(config.version, 2);
      expect(config.packages.first.name, 'foo');
    });

    test('nested packages', () {
      var configBytes = utf8.encode(json.encode({
        'configVersion': 2,
        'packages': [
          {'name': 'foo', 'rootUri': '/foo/', 'packageUri': 'lib/'},
          {'name': 'bar', 'rootUri': '/foo/bar/', 'packageUri': 'lib/'},
          {'name': 'baz', 'rootUri': '/foo/bar/baz/', 'packageUri': 'lib/'},
          {'name': 'qux', 'rootUri': '/foo/qux/', 'packageUri': 'lib/'},
        ]
      }));
      // ignore: unnecessary_cast
      var config = parsePackageConfigBytes(configBytes as Uint8List,
          Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
      expect(config.version, 2);
      expect(config.packageOf(Uri.parse('file:///foo/lala/lala.dart'))!.name,
          'foo');
      expect(config.packageOf(Uri.parse('file:///foo/bar/lala.dart'))!.name,
          'bar');
      expect(config.packageOf(Uri.parse('file:///foo/bar/baz/lala.dart'))!.name,
          'baz');
      expect(config.packageOf(Uri.parse('file:///foo/qux/lala.dart'))!.name,
          'qux');
      expect(config.toPackageUri(Uri.parse('file:///foo/lib/diz')),
          Uri.parse('package:foo/diz'));
      expect(config.toPackageUri(Uri.parse('file:///foo/bar/lib/diz')),
          Uri.parse('package:bar/diz'));
      expect(config.toPackageUri(Uri.parse('file:///foo/bar/baz/lib/diz')),
          Uri.parse('package:baz/diz'));
      expect(config.toPackageUri(Uri.parse('file:///foo/qux/lib/diz')),
          Uri.parse('package:qux/diz'));
    });

    test('nested packages 2', () {
      var configBytes = utf8.encode(json.encode({
        'configVersion': 2,
        'packages': [
          {'name': 'foo', 'rootUri': '/', 'packageUri': 'lib/'},
          {'name': 'bar', 'rootUri': '/bar/', 'packageUri': 'lib/'},
          {'name': 'baz', 'rootUri': '/bar/baz/', 'packageUri': 'lib/'},
          {'name': 'qux', 'rootUri': '/qux/', 'packageUri': 'lib/'},
        ]
      }));
      // ignore: unnecessary_cast
      var config = parsePackageConfigBytes(configBytes as Uint8List,
          Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
      expect(config.version, 2);
      expect(
          config.packageOf(Uri.parse('file:///lala/lala.dart'))!.name, 'foo');
      expect(config.packageOf(Uri.parse('file:///bar/lala.dart'))!.name, 'bar');
      expect(config.packageOf(Uri.parse('file:///bar/baz/lala.dart'))!.name,
          'baz');
      expect(config.packageOf(Uri.parse('file:///qux/lala.dart'))!.name, 'qux');
      expect(config.toPackageUri(Uri.parse('file:///lib/diz')),
          Uri.parse('package:foo/diz'));
      expect(config.toPackageUri(Uri.parse('file:///bar/lib/diz')),
          Uri.parse('package:bar/diz'));
      expect(config.toPackageUri(Uri.parse('file:///bar/baz/lib/diz')),
          Uri.parse('package:baz/diz'));
      expect(config.toPackageUri(Uri.parse('file:///qux/lib/diz')),
          Uri.parse('package:qux/diz'));
    });

    test('packageOf is case sensitive on windows', () {
      var configBytes = utf8.encode(json.encode({
        'configVersion': 2,
        'packages': [
          {'name': 'foo', 'rootUri': 'file:///C:/Foo/', 'packageUri': 'lib/'},
        ]
      }));
      var config = parsePackageConfigBytes(
          // ignore: unnecessary_cast
          configBytes as Uint8List,
          Uri.parse('file:///C:/tmp/.dart_tool/file.dart'),
          throwError);
      expect(config.version, 2);
      expect(
          config.packageOf(Uri.parse('file:///C:/foo/lala/lala.dart')), null);
      expect(config.packageOf(Uri.parse('file:///C:/Foo/lala/lala.dart'))!.name,
          'foo');
    });

    group('invalid', () {
      void testThrows(String name, String source) {
        test(name, () {
          expect(
              // ignore: unnecessary_cast
              () => parsePackageConfigBytes(utf8.encode(source) as Uint8List,
                  Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError),
              throwsA(isA<FormatException>()));
        });
      }

      void testThrowsContains(
          String name, String source, String containsString) {
        test(name, () {
          dynamic exception;
          try {
            parsePackageConfigBytes(
              // ignore: unnecessary_cast
              utf8.encode(source) as Uint8List,
              Uri.parse('file:///tmp/.dart_tool/file.dart'),
              throwError,
            );
          } catch (e) {
            exception = e;
          }
          if (exception == null) fail("Didn't get exception");
          expect('$exception', contains(containsString));
        });
      }

      testThrows('comment', '# comment\n {$cfg,$pkgs}');
      testThrows('.packages file', 'foo:/foo\n');
      testThrows('no configVersion', '{$pkgs}');
      testThrows('no packages', '{$cfg}');
      group('config version:', () {
        testThrows('null', '{"configVersion":null,$pkgs}');
        testThrows('string', '{"configVersion":"2",$pkgs}');
        testThrows('array', '{"configVersion":[2],$pkgs}');
      });
      group('packages:', () {
        testThrows('null', '{$cfg,"packages":null}');
        testThrows('string', '{$cfg,"packages":"foo"}');
        testThrows('object', '{$cfg,"packages":{}}');
      });
      group('packages entry:', () {
        testThrows('null', '{$cfg,"packages":[null]}');
        testThrows('string', '{$cfg,"packages":["foo"]}');
        testThrows('array', '{$cfg,"packages":[[]]}');
      });
      group('package', () {
        testThrows('no name', '{$cfg,"packages":[{$root}]}');
        group('name:', () {
          testThrows('null', '{$cfg,"packages":[{"name":null,$root}]}');
          testThrows('num', '{$cfg,"packages":[{"name":1,$root}]}');
          testThrows('object', '{$cfg,"packages":[{"name":{},$root}]}');
          testThrows('empty', '{$cfg,"packages":[{"name":"",$root}]}');
          testThrows('one-dot', '{$cfg,"packages":[{"name":".",$root}]}');
          testThrows('two-dot', '{$cfg,"packages":[{"name":"..",$root}]}');
          testThrows(
              "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}');
          testThrows(
              "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}');
          testThrows(
              "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}');
        });

        testThrows('no root', '{$cfg,"packages":[{$name}]}');
        group('root:', () {
          testThrows('null', '{$cfg,"packages":[{$name,"rootUri":null}]}');
          testThrows('num', '{$cfg,"packages":[{$name,"rootUri":1}]}');
          testThrows('object', '{$cfg,"packages":[{$name,"rootUri":{}}]}');
          testThrows('fragment', '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}');
          testThrows('query', '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}');
          testThrows('package-URI',
              '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}');
        });
        group('package-URI root:', () {
          testThrows(
              'null', '{$cfg,"packages":[{$name,$root,"packageUri":null}]}');
          testThrows('num', '{$cfg,"packages":[{$name,$root,"packageUri":1}]}');
          testThrows(
              'object', '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}');
          testThrows('fragment',
              '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}');
          testThrows(
              'query', '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}');
          testThrows('package: URI',
              '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}');
          testThrows('not inside root',
              '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}');
        });
        group('language version', () {
          testThrows('null',
              '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}');
          testThrows(
              'num', '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}');
          testThrows('object',
              '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}');
          testThrows('empty',
              '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}');
          testThrows('non number.number',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}');
          testThrows('number.non number',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}');
          testThrows('non number',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}');
          testThrows('one number',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}');
          testThrows('three numbers',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}');
          testThrows('leading zero first',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}');
          testThrows('leading zero second',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}');
          testThrows('trailing-',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}');
          testThrows('trailing+',
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}');
        });
      });
      testThrows('duplicate package name',
          '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}');
      testThrowsContains(
          // The roots of foo and bar are the same.
          'same roots',
          '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}',
          'the same root directory');
      testThrowsContains(
          // The roots of foo and bar are the same.
          'same roots 2',
          '{$cfg,"packages":[{$name,"rootUri":"/"},{"name":"bar","rootUri":"/"}]}',
          'the same root directory');
      testThrowsContains(
          // The root of bar is inside the root of foo,
          // but the package root of foo is inside the root of bar.
          'between root and lib',
          '{$cfg,"packages":['
              '{"name":"foo","rootUri":"/foo/","packageUri":"bar/lib/"},'
              '{"name":"bar","rootUri":"/foo/bar/","packageUri":"baz/lib"}]}',
          'package root of foo is inside the root of bar');

      // This shouldn't be allowed, but for internal reasons it is.
      test('package inside package root', () {
        var config = parsePackageConfigBytes(
            // ignore: unnecessary_cast
            utf8.encode(
              '{$cfg,"packages":['
              '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},'
              '{"name":"bar","rootUri":"/foo/lib/bar/","packageUri":"lib"}]}',
            ) as Uint8List,
            Uri.parse('file:///tmp/.dart_tool/file.dart'),
            throwError);
        expect(
            config
                .packageOf(Uri.parse('file:///foo/lib/bar/lib/lala.dart'))!
                .name,
            'foo'); // why not bar?
        expect(config.toPackageUri(Uri.parse('file:///foo/lib/bar/lib/diz')),
            Uri.parse('package:foo/bar/lib/diz')); // why not package:bar/diz?
      });
    });
  });

  group('factories', () {
    void testConfig(String name, PackageConfig config, PackageConfig expected) {
      group(name, () {
        test('structure', () {
          expect(config.version, expected.version);
          var expectedPackages = {for (var p in expected.packages) p.name};
          var actualPackages = {for (var p in config.packages) p.name};
          expect(actualPackages, expectedPackages);
        });
        for (var package in config.packages) {
          var name = package.name;
          test('package $name', () {
            var expectedPackage = expected[name]!;
            expect(expectedPackage, isNotNull);
            expect(package.root, expectedPackage.root, reason: 'root');
            expect(package.packageUriRoot, expectedPackage.packageUriRoot,
                reason: 'package root');
            expect(package.languageVersion, expectedPackage.languageVersion,
                reason: 'languageVersion');
          });
        }
      });
    }

    var configText = '''
     {"configVersion": 2, "packages": [
       {
         "name": "foo",
         "rootUri": "foo/",
         "packageUri": "bar/",
         "languageVersion": "1.2"
       }
     ]}
    ''';
    var baseUri = Uri.parse('file:///start/');
    var config = PackageConfig([
      Package('foo', Uri.parse('file:///start/foo/'),
          packageUriRoot: Uri.parse('file:///start/foo/bar/'),
          languageVersion: LanguageVersion(1, 2))
    ]);
    testConfig(
        'string', PackageConfig.parseString(configText, baseUri), config);
    testConfig(
        'bytes',
        PackageConfig.parseBytes(
            Uint8List.fromList(configText.codeUnits), baseUri),
        config);
    testConfig('json', PackageConfig.parseJson(jsonDecode(configText), baseUri),
        config);

    baseUri = Uri.parse('file:///start2/');
    config = PackageConfig([
      Package('foo', Uri.parse('file:///start2/foo/'),
          packageUriRoot: Uri.parse('file:///start2/foo/bar/'),
          languageVersion: LanguageVersion(1, 2))
    ]);
    testConfig(
        'string2', PackageConfig.parseString(configText, baseUri), config);
    testConfig(
        'bytes2',
        PackageConfig.parseBytes(
            Uint8List.fromList(configText.codeUnits), baseUri),
        config);
    testConfig('json2',
        PackageConfig.parseJson(jsonDecode(configText), baseUri), config);
  });
}
