// Copyright (c) 2018, 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:io';

import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test/test.dart';

import 'test_utils.dart';

void main() {
  group('hosted', _hostedDependency);
  group('git', _gitDependency);
  group('sdk', _sdkDependency);
  group('path', _pathDependency);

  group('errors', () {
    test('List', () {
      _expectThrows(
        [],
        r'''
line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
  ╷
4 │   "dep": []
  │          ^^
  ╵''',
      );
    });

    test('int', () {
      _expectThrows(
        42,
        r'''
line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
  ╷
4 │     "dep": 42
  │ ┌──────────^
5 │ │  }
  │ └─^
  ╵''',
      );
    });

    test('map with too many keys', () {
      _expectThrows(
        {'path': 'a', 'git': 'b'},
        r'''
line 6, column 11: Unsupported value for "git". A dependency may only have one source.
  ╷
6 │    "git": "b"
  │           ^^^
  ╵''',
      );
    });

    test('map with unsupported keys', () {
      _expectThrows(
        {'bob': 'a', 'jones': 'b'},
        r'''
line 5, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
  ╷
5 │    "bob": "a",
  │    ^^^^^
  ╵''',
      );
    });
  });
}

void _hostedDependency() {
  test('null', () async {
    final dep = await _dependency<HostedDependency>(null);
    expect(dep.version.toString(), 'any');
    expect(dep.hosted, isNull);
    expect(dep.toString(), 'HostedDependency: any');
  });

  test('empty map', () async {
    final dep = await _dependency<HostedDependency>({});
    expect(dep.hosted, isNull);
    expect(dep.toString(), 'HostedDependency: any');
  });

  test('string version', () async {
    final dep = await _dependency<HostedDependency>('^1.0.0');
    expect(dep.version.toString(), '^1.0.0');
    expect(dep.hosted, isNull);
    expect(dep.toString(), 'HostedDependency: ^1.0.0');
  });

  test('bad string version', () {
    _expectThrows(
      'not a version',
      r'''
line 4, column 10: Unsupported value for "dep". Could not parse version "not a version". Unknown text at "not a version".
  ╷
4 │   "dep": "not a version"
  │          ^^^^^^^^^^^^^^^
  ╵''',
    );
  });

  test('map w/ just version', () async {
    final dep = await _dependency<HostedDependency>({'version': '^1.0.0'});
    expect(dep.version.toString(), '^1.0.0');
    expect(dep.hosted, isNull);
    expect(dep.toString(), 'HostedDependency: ^1.0.0');
  });

  test('map w/ version and hosted as Map', () async {
    final dep = await _dependency<HostedDependency>({
      'version': '^1.0.0',
      'hosted': {'name': 'hosted_name', 'url': 'https://hosted_url'},
    });
    expect(dep.version.toString(), '^1.0.0');
    expect(dep.hosted!.name, 'hosted_name');
    expect(dep.hosted!.url.toString(), 'https://hosted_url');
    expect(dep.toString(), 'HostedDependency: ^1.0.0');
  });

  test('map /w hosted as a map without name', () async {
    final dep = await _dependency<HostedDependency>(
      {
        'version': '^1.0.0',
        'hosted': {'url': 'https://hosted_url'},
      },
      skipTryPub: true, // todo: Unskip once pub supports this syntax
    );
    expect(dep.version.toString(), '^1.0.0');
    expect(dep.hosted!.declaredName, isNull);
    expect(dep.hosted!.name, 'dep');
    expect(dep.hosted!.url.toString(), 'https://hosted_url');
    expect(dep.toString(), 'HostedDependency: ^1.0.0');
  });

  test('map w/ bad version value', () {
    _expectThrows(
      {
        'version': 'not a version',
        'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
      },
      r'''
line 5, column 15: Unsupported value for "version". Could not parse version "not a version". Unknown text at "not a version".
  ╷
5 │    "version": "not a version",
  │               ^^^^^^^^^^^^^^^
  ╵''',
    );
  });

  test('map w/ extra keys should fail', () {
    _expectThrows(
      {
        'version': '^1.0.0',
        'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
        'not_supported': null,
      },
      r'''
line 10, column 4: Unrecognized keys: [not_supported]; supported keys: [sdk, git, path, hosted]
   ╷
10 │    "not_supported": null
   │    ^^^^^^^^^^^^^^^
   ╵''',
    );
  });

  test('map w/ version and hosted as String', () async {
    final dep = await _dependency<HostedDependency>(
      {'version': '^1.0.0', 'hosted': 'hosted_url'},
      skipTryPub: true, // todo: Unskip once put supports this
    );
    expect(dep.version.toString(), '^1.0.0');
    expect(dep.hosted!.declaredName, isNull);
    expect(dep.hosted!.name, 'dep');
    expect(dep.hosted!.url, Uri.parse('hosted_url'));
    expect(dep.toString(), 'HostedDependency: ^1.0.0');
  });

  test('map w/ hosted as String', () async {
    final dep = await _dependency<HostedDependency>({'hosted': 'hosted_url'});
    expect(dep.version, VersionConstraint.any);
    expect(dep.hosted!.declaredName, isNull);
    expect(dep.hosted!.name, 'dep');
    expect(dep.hosted!.url, Uri.parse('hosted_url'));
    expect(dep.toString(), 'HostedDependency: any');
  });

  test('map w/ null hosted should error', () {
    _expectThrows(
      {'hosted': null},
      r'''
line 5, column 4: These keys had `null` values, which is not allowed: [hosted]
  ╷
5 │    "hosted": null
  │    ^^^^^^^^
  ╵''',
    );
  });

  test('map w/ null version is fine', () async {
    final dep = await _dependency<HostedDependency>({'version': null});
    expect(dep.version, VersionConstraint.any);
    expect(dep.hosted, isNull);
    expect(dep.toString(), 'HostedDependency: any');
  });
}

void _sdkDependency() {
  test('without version', () async {
    final dep = await _dependency<SdkDependency>({'sdk': 'flutter'});
    expect(dep.sdk, 'flutter');
    expect(dep.version, VersionConstraint.any);
    expect(dep.toString(), 'SdkDependency: flutter');
  });

  test('with version', () async {
    final dep = await _dependency<SdkDependency>(
      {'sdk': 'flutter', 'version': '>=1.2.3 <2.0.0'},
    );
    expect(dep.sdk, 'flutter');
    expect(dep.version.toString(), '>=1.2.3 <2.0.0');
    expect(dep.toString(), 'SdkDependency: flutter');
  });

  test('null content', () {
    _expectThrowsContaining(
      {'sdk': null},
      r"type 'Null' is not a subtype of type 'String'",
    );
  });

  test('number content', () {
    _expectThrowsContaining(
      {'sdk': 42},
      r"type 'int' is not a subtype of type 'String'",
    );
  });
}

void _gitDependency() {
  test('string', () async {
    final dep = await _dependency<GitDependency>({'git': 'url'});
    expect(dep.url.toString(), 'url');
    expect(dep.path, isNull);
    expect(dep.ref, isNull);
    expect(dep.toString(), 'GitDependency: url@url');
  });

  test('string with version key is ignored', () async {
    // Regression test for https://github.com/dart-lang/pubspec_parse/issues/13
    final dep =
        await _dependency<GitDependency>({'git': 'url', 'version': '^1.2.3'});
    expect(dep.url.toString(), 'url');
    expect(dep.path, isNull);
    expect(dep.ref, isNull);
    expect(dep.toString(), 'GitDependency: url@url');
  });

  test('string with user@ URL', () async {
    final skipTryParse = Platform.environment.containsKey('TRAVIS');
    if (skipTryParse) {
      print('FYI: not validating git@ URI on travis due to failure');
    }
    final dep = await _dependency<GitDependency>(
      {'git': 'git@localhost:dep.git'},
      skipTryPub: skipTryParse,
    );
    expect(dep.url.toString(), 'ssh://git@localhost/dep.git');
    expect(dep.path, isNull);
    expect(dep.ref, isNull);
    expect(dep.toString(), 'GitDependency: url@ssh://git@localhost/dep.git');
  });

  test('string with random extra key fails', () {
    _expectThrows(
      {'git': 'url', 'bob': '^1.2.3'},
      r'''
line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
  ╷
6 │    "bob": "^1.2.3"
  │    ^^^^^
  ╵''',
    );
  });

  test('map', () async {
    final dep = await _dependency<GitDependency>({
      'git': {'url': 'url', 'path': 'path', 'ref': 'ref'},
    });
    expect(dep.url.toString(), 'url');
    expect(dep.path, 'path');
    expect(dep.ref, 'ref');
    expect(dep.toString(), 'GitDependency: url@url');
  });

  test('git - null content', () {
    _expectThrows(
      {'git': null},
      r'''
line 5, column 11: Unsupported value for "git". Must be a String or a Map.
  ╷
5 │      "git": null
  │ ┌───────────^
6 │ │   }
  │ └──^
  ╵''',
    );
  });

  test('git - int content', () {
    _expectThrows(
      {'git': 42},
      r'''
line 5, column 11: Unsupported value for "git". Must be a String or a Map.
  ╷
5 │      "git": 42
  │ ┌───────────^
6 │ │   }
  │ └──^
  ╵''',
    );
  });

  test('git - empty map', () {
    _expectThrowsContaining(
      {'git': <String, dynamic>{}},
      r"type 'Null' is not a subtype of type 'String'",
    );
  });

  test('git - null url', () {
    _expectThrowsContaining(
      {
        'git': {'url': null},
      },
      r"type 'Null' is not a subtype of type 'String'",
    );
  });

  test('git - int url', () {
    _expectThrowsContaining(
      {
        'git': {'url': 42},
      },
      r"type 'int' is not a subtype of type 'String'",
    );
  });
}

void _pathDependency() {
  test('valid', () async {
    final dep = await _dependency<PathDependency>({'path': '../path'});
    expect(dep.path, '../path');
    expect(dep.toString(), 'PathDependency: path@../path');
  });

  test('valid with version key is ignored', () async {
    final dep = await _dependency<PathDependency>(
      {'path': '../path', 'version': '^1.2.3'},
    );
    expect(dep.path, '../path');
    expect(dep.toString(), 'PathDependency: path@../path');
  });

  test('valid with random extra key fails', () {
    _expectThrows(
      {'path': '../path', 'bob': '^1.2.3'},
      r'''
line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
  ╷
6 │    "bob": "^1.2.3"
  │    ^^^^^
  ╵''',
    );
  });

  test('null content', () {
    _expectThrows(
      {'path': null},
      r'''
line 5, column 12: Unsupported value for "path". Must be a String.
  ╷
5 │      "path": null
  │ ┌────────────^
6 │ │   }
  │ └──^
  ╵''',
    );
  });

  test('int content', () {
    _expectThrows(
      {'path': 42},
      r'''
line 5, column 12: Unsupported value for "path". Must be a String.
  ╷
5 │      "path": 42
  │ ┌────────────^
6 │ │   }
  │ └──^
  ╵''',
    );
  });
}

void _expectThrows(Object content, String expectedError) {
  expectParseThrows(
    {
      'name': 'sample',
      'dependencies': {'dep': content},
    },
    expectedError,
  );
}

void _expectThrowsContaining(Object content, String errorText) {
  expectParseThrowsContaining(
    {
      'name': 'sample',
      'dependencies': {'dep': content},
    },
    errorText,
  );
}

Future<T> _dependency<T extends Dependency>(
  Object? content, {
  bool skipTryPub = false,
}) async {
  final value = await parse(
    {
      ...defaultPubspec,
      'dependencies': {'dep': content},
    },
    skipTryPub: skipTryPub,
  );
  expect(value.name, 'sample');
  expect(value.dependencies, hasLength(1));

  final entry = value.dependencies.entries.single;
  expect(entry.key, 'dep');

  return entry.value as T;
}
