// Copyright (c) 2015, 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:path/path.dart' as p;
import 'package:source_map_stack_trace/source_map_stack_trace.dart';
import 'package:source_maps/source_maps.dart';
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:test/test.dart';

/// A simple [Mapping] for tests that don't need anything special.
final _simpleMapping = parseJson((SourceMapBuilder()
      ..addSpan(
          SourceMapSpan.identifier(
              SourceLocation(1, line: 1, column: 3, sourceUrl: 'foo.dart'),
              'qux'),
          SourceSpan(SourceLocation(8, line: 5, column: 0),
              SourceLocation(18, line: 15, column: 0), '\n' * 10)))
    .build('foo.dart.js.map'));

final _packageMap = {
  'bar': Uri.parse('packages/bar'),
  'foo': Uri.parse('packages/foo'),
};

void main() {
  test('maps a JS line and column to a Dart line and span', () {
    var trace = Trace.parse('foo.dart.js 10:11  foo');
    var frame = _mapTrace(_simpleMapping, trace).frames.first;
    expect(frame.uri, equals(Uri.parse('foo.dart')));

    // These are +1 because stack_trace is 1-based whereas source_span is
    // 0-basd.
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));
  });

  test('ignores JS frames without line info', () {
    var trace = Trace.parse('''
foo.dart.js 10:11  foo
foo.dart.js        bar
foo.dart.js 10:11  baz
''');
    var frames = _mapTrace(_simpleMapping, trace).frames;

    expect(frames.length, equals(2));
    expect(frames.first.member, equals('foo'));
    expect(frames.last.member, equals('baz'));
  });

  test('ignores JS frames without corresponding spans', () {
    var trace = Trace.parse('''
foo.dart.js 10:11  foo
foo.dart.js 1:1   bar
foo.dart.js 10:11  baz
''');

    var frames = _mapTrace(_simpleMapping, trace).frames;

    expect(frames.length, equals(2));
    expect(frames.first.member, equals('foo'));
    expect(frames.last.member, equals('baz'));
  });

  test('include frames from JS files not covered by the source map bundle', () {
    var trace = Trace.parse('''
foo.dart.js 10:11  foo
jquery.js 10:1  foo
bar.dart.js 10:11  foo
''');
    var builder = SourceMapBuilder()
      ..addSpan(
          SourceMapSpan.identifier(
              SourceLocation(1,
                  line: 1, column: 3, sourceUrl: 'packages/foo/foo.dart'),
              'qux'),
          SourceSpan(SourceLocation(8, line: 5, column: 0),
              SourceLocation(12, line: 9, column: 1), '\n' * 4));
    var sourceMapJson1 = builder.build('foo.dart.js.map');
    sourceMapJson1['file'] = 'foo.dart.js';

    builder = SourceMapBuilder()
      ..addSpan(
          SourceMapSpan.identifier(
              SourceLocation(1,
                  line: 1, column: 3, sourceUrl: 'packages/bar/bar.dart'),
              'qux'),
          SourceSpan(SourceLocation(8, line: 5, column: 0),
              SourceLocation(12, line: 9, column: 1), '\n' * 4));
    var sourceMapJson2 = builder.build('bar.dart.js.map');
    sourceMapJson2['file'] = 'bar.dart.js';

    var bundle = [sourceMapJson1, sourceMapJson2];
    var mapping = parseJsonExtended(bundle);
    var frames = _mapTrace(mapping, trace, packageMap: _packageMap).frames;

    expect(frames.length, equals(3));

    var frame = frames[0];
    expect(frame.uri, equals(Uri.parse('package:foo/foo.dart')));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));

    frame = frames[1];
    expect(p.basename(frame.uri.toString()), equals('jquery.js'));
    expect(frame.line, equals(10));

    frame = frames[2];
    expect(frame.uri, equals(Uri.parse('package:bar/bar.dart')));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));
  });

  test('falls back to column 0 for unlisted column', () {
    var trace = Trace.parse('foo.dart.js 10  foo');
    var builder = SourceMapBuilder()
      ..addSpan(
          SourceMapSpan.identifier(
              SourceLocation(1, line: 1, column: 3, sourceUrl: 'foo.dart'),
              'qux'),
          SourceSpan(SourceLocation(8, line: 5, column: 0),
              SourceLocation(12, line: 9, column: 1), '\n' * 4));

    var mapping = parseJson(builder.build('foo.dart.js.map'));
    var frame = _mapTrace(mapping, trace).frames.first;
    expect(frame.uri, equals(Uri.parse('foo.dart')));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));
  });

  test('uses package: URIs for frames within a packageResolver.packageMap URL',
      () {
    var trace = Trace.parse('foo.dart.js 10  foo');
    var builder = SourceMapBuilder()
      ..addSpan(
          SourceMapSpan.identifier(
              SourceLocation(1,
                  line: 1, column: 3, sourceUrl: 'packages/foo/foo.dart'),
              'qux'),
          SourceSpan(SourceLocation(8, line: 5, column: 0),
              SourceLocation(12, line: 9, column: 1), '\n' * 4));

    var mapping = parseJson(builder.build('foo.dart.js.map'));
    var mappedTrace = _mapTrace(mapping, trace, packageMap: _packageMap);
    var frame = mappedTrace.frames.first;
    expect(frame.uri, equals(Uri.parse('package:foo/foo.dart')));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));
  });

  test('uses dart: URIs for frames within sdkRoot', () {
    var trace = Trace.parse('foo.dart.js 10  foo');
    var builder = SourceMapBuilder()
      ..addSpan(
          SourceMapSpan.identifier(
              SourceLocation(1,
                  line: 1, column: 3, sourceUrl: 'sdk/lib/async/foo.dart'),
              'qux'),
          SourceSpan(SourceLocation(8, line: 5, column: 0),
              SourceLocation(12, line: 9, column: 1), '\n' * 4));

    var mapping = parseJson(builder.build('foo.dart.js.map'));
    var frame =
        _mapTrace(mapping, trace, sdkRoot: Uri.parse('sdk/')).frames.first;
    expect(frame.uri, equals(Uri.parse('dart:async/foo.dart')));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));
  });

  test('converts a stack chain', () {
    var trace = Chain([
      Trace.parse('foo.dart.js 10:11  foo'),
      Trace.parse('foo.dart.js 10:11  bar')
    ]);
    var traces = _mapChain(_simpleMapping, trace).traces;

    var frame = traces.first.frames.single;
    expect(frame.uri, equals(Uri.parse('foo.dart')));
    expect(frame.member, equals('foo'));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));

    frame = traces.last.frames.single;
    expect(frame.uri, equals(Uri.parse('foo.dart')));
    expect(frame.member, equals('bar'));
    expect(frame.line, equals(2));
    expect(frame.column, equals(4));
  });

  group('cleans up', () {
    test('Firefox junk', () {
      expect(_prettify('foo/<'), equals('foo'));
      expect(_prettify('foo<'), equals('foo'));
    });

    test('arity indicators', () {
      expect(_prettify(r'foo$1'), equals('foo'));
      expect(_prettify(r'foo$1234'), equals('foo'));
    });

    test('named arguments', () {
      expect(_prettify(r'foo$1$bar'), equals('foo'));
      expect(_prettify(r'foo$123$bar$bang$qux'), equals('foo'));
    });

    test('closures', () {
      expect(_prettify('foo_closure.call'), equals('foo.<fn>'));
    });

    test('nested closures', () {
      expect(_prettify('foo__closure.call'), equals('foo.<fn>.<fn>'));
      expect(
          _prettify('foo____closure.call'), equals('foo.<fn>.<fn>.<fn>.<fn>'));
    });

    test('.call', () {
      expect(_prettify('foo.call'), equals('foo'));
    });

    test('top-level functions', () {
      expect(_prettify('dart.foo'), equals('foo'));
    });

    test('library namespaces', () {
      expect(_prettify(r'my_library$foo'), equals('foo'));
    });

    test('static methods', () {
      expect(_prettify(r'Foo.static.foo'), equals('foo'));
    });

    test('instance methods', () {
      expect(_prettify(r'Foo_bar__baz'), equals('Foo.bar._baz'));
    });

    test('lots of stuff', () {
      expect(_prettify(r'lib$Foo.static.lib$Foo_closure.call$0/<'),
          equals('Foo.<fn>'));
    });
  });
}

/// Like [mapStackTrace], but is guaranteed to return a [Trace] so it can be
/// inspected.
Trace _mapTrace(Mapping sourceMap, StackTrace stackTrace,
    {bool minified = false, Map<String, Uri>? packageMap, Uri? sdkRoot}) {
  return Trace.from(mapStackTrace(sourceMap, stackTrace,
      minified: minified, packageMap: packageMap, sdkRoot: sdkRoot));
}

/// Like [mapStackTrace], but is guaranteed to return a [Chain] so it can be
/// inspected.
Chain _mapChain(Mapping sourceMap, StackTrace stackTrace,
    {bool minified = false, Map<String, Uri>? packageMap, Uri? sdkRoot}) {
  return Chain.forTrace(mapStackTrace(sourceMap, stackTrace,
      minified: minified, packageMap: packageMap, sdkRoot: sdkRoot));
}

/// Runs the mapper's prettification logic on [member] and returns the result.
String? _prettify(String member) {
  var trace = Trace([Frame(Uri.parse('foo.dart.js'), 10, 11, member)]);
  return _mapTrace(_simpleMapping, trace).frames.first.member;
}
