// Copyright 2017 The Chromium Authors. 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:async';
import 'dart:convert';

import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';

import 'src/common.dart';
import 'src/context.dart';

void main() {
  group('batch compile', () {
    ProcessManager mockProcessManager;
    MockProcess mockFrontendServer;
    MockStdIn mockFrontendServerStdIn;
    MockStream mockFrontendServerStdErr;
    setUp(() {
      mockProcessManager = MockProcessManager();
      mockFrontendServer = MockProcess();
      mockFrontendServerStdIn = MockStdIn();
      mockFrontendServerStdErr = MockStream();

      when(mockFrontendServer.stderr)
          .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
      final StreamController<String> stdErrStreamController = StreamController<String>();
      when(mockFrontendServerStdErr.transform<String>(any)).thenAnswer((_) => stdErrStreamController.stream);
      when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
      when(mockProcessManager.canRun(any)).thenReturn(true);
      when(mockProcessManager.start(any)).thenAnswer(
          (Invocation invocation) => Future<Process>.value(mockFrontendServer));
      when(mockFrontendServer.exitCode).thenAnswer((_) async => 0);
    });

    testUsingContext('single dart successful compilation', () async {
      final BufferLogger logger = context[Logger];
      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
            Future<List<int>>.value(utf8.encode(
              'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0'
            ))
          ));
      final CompilerOutput output = await kernelCompiler.compile(sdkRoot: '/path/to/sdkroot',
        mainPath: '/path/to/main.dart',
        trackWidgetCreation: false,
      );
      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
      expect(output.outputFilename, equals('/path/to/main.dart.dill'));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });

    testUsingContext('single dart failed compilation', () async {
      final BufferLogger logger = context[Logger];

      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
            Future<List<int>>.value(utf8.encode(
              'result abc\nline1\nline2\nabc'
            ))
          ));

      final CompilerOutput output = await kernelCompiler.compile(sdkRoot: '/path/to/sdkroot',
        mainPath: '/path/to/main.dart',
        trackWidgetCreation: false,
      );
      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
      expect(output, equals(null));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });

    testUsingContext('single dart abnormal compiler termination', () async {
      when(mockFrontendServer.exitCode).thenAnswer((_) async => 255);

      final BufferLogger logger = context[Logger];

      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
          Future<List<int>>.value(utf8.encode(
              'result abc\nline1\nline2\nabc'
          ))
      ));

      final CompilerOutput output = await kernelCompiler.compile(
        sdkRoot: '/path/to/sdkroot',
        mainPath: '/path/to/main.dart',
        trackWidgetCreation: false,
      );
      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
      expect(output, equals(null));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });
  });

  group('incremental compile', () {
    ProcessManager mockProcessManager;
    ResidentCompiler generator;
    MockProcess mockFrontendServer;
    MockStdIn mockFrontendServerStdIn;
    MockStream mockFrontendServerStdErr;
    StreamController<String> stdErrStreamController;

    setUp(() {
      generator = ResidentCompiler('sdkroot');
      mockProcessManager = MockProcessManager();
      mockFrontendServer = MockProcess();
      mockFrontendServerStdIn = MockStdIn();
      mockFrontendServerStdErr = MockStream();

      when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
      when(mockFrontendServer.stderr)
          .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
      stdErrStreamController = StreamController<String>();
      when(mockFrontendServerStdErr.transform<String>(any))
          .thenAnswer((Invocation invocation) => stdErrStreamController.stream);

      when(mockProcessManager.canRun(any)).thenReturn(true);
      when(mockProcessManager.start(any)).thenAnswer(
          (Invocation invocation) => Future<Process>.value(mockFrontendServer)
      );
    });

    tearDown(() {
      verifyNever(mockFrontendServer.exitCode);
    });

    testUsingContext('single dart compile', () async {
      final BufferLogger logger = context[Logger];

      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
            Future<List<int>>.value(utf8.encode(
              'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0'
            ))
          ));

      final CompilerOutput output = await generator.recompile(
        '/path/to/main.dart',
          null /* invalidatedFiles */,
        outputPath: '/build/',
      );
      expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
      verifyNoMoreInteractions(mockFrontendServerStdIn);
      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
      expect(output.outputFilename, equals('/path/to/main.dart.dill'));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });

    testUsingContext('single dart compile abnormally terminates', () async {
      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => const Stream<List<int>>.empty()
      );

      final CompilerOutput output = await generator.recompile(
        '/path/to/main.dart',
        null, /* invalidatedFiles */
        outputPath: '/build/',
      );
      expect(output, equals(null));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });

    testUsingContext('compile and recompile', () async {
      final BufferLogger logger = context[Logger];

      final StreamController<List<int>> streamController = StreamController<List<int>>();
      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => streamController.stream);
      streamController.add(utf8.encode('result abc\nline0\nline1\nabc /path/to/main.dart.dill 0\n'));
      await generator.recompile(
        '/path/to/main.dart',
        null, /* invalidatedFiles */
        outputPath: '/build/',
      );
      expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');

      await _recompile(streamController, generator, mockFrontendServerStdIn,
        'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0\n');

      verifyNoMoreInteractions(mockFrontendServerStdIn);
      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
      expect(logger.errorText, equals(
        '\nCompiler message:\nline0\nline1\n'
        '\nCompiler message:\nline1\nline2\n'
      ));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });

    testUsingContext('compile and recompile twice', () async {
      final BufferLogger logger = context[Logger];

      final StreamController<List<int>> streamController = StreamController<List<int>>();
      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) => streamController.stream);
      streamController.add(utf8.encode(
        'result abc\nline0\nline1\nabc /path/to/main.dart.dill 0\n'
      ));
      await generator.recompile('/path/to/main.dart', null /* invalidatedFiles */, outputPath: '/build/');
      expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');

      await _recompile(streamController, generator, mockFrontendServerStdIn,
        'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0\n');
      await _recompile(streamController, generator, mockFrontendServerStdIn,
        'result abc\nline2\nline3\nabc /path/to/main.dart.dill 0\n');

      verifyNoMoreInteractions(mockFrontendServerStdIn);
      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
      expect(logger.errorText, equals(
        '\nCompiler message:\nline0\nline1\n'
        '\nCompiler message:\nline1\nline2\n'
        '\nCompiler message:\nline2\nline3\n'
      ));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });
  });

  group('compile expression', ()
  {
    ProcessManager mockProcessManager;
    ResidentCompiler generator;
    MockProcess mockFrontendServer;
    MockStdIn mockFrontendServerStdIn;
    MockStream mockFrontendServerStdErr;
    StreamController<String> stdErrStreamController;

    setUp(() {
      generator = ResidentCompiler('sdkroot');
      mockProcessManager = MockProcessManager();
      mockFrontendServer = MockProcess();
      mockFrontendServerStdIn = MockStdIn();
      mockFrontendServerStdErr = MockStream();

      when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
      when(mockFrontendServer.stderr)
          .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
      stdErrStreamController = StreamController<String>();
      when(mockFrontendServerStdErr.transform<String>(any))
          .thenAnswer((Invocation invocation) => stdErrStreamController.stream);

      when(mockProcessManager.canRun(any)).thenReturn(true);
      when(mockProcessManager.start(any)).thenAnswer(
              (Invocation invocation) =>
          Future<Process>.value(mockFrontendServer)
      );
    });

    tearDown(() {
      verifyNever(mockFrontendServer.exitCode);
    });

    testUsingContext('fails if not previously compiled', () async {
      final CompilerOutput result = await generator.compileExpression(
          '2+2', null, null, null, null, false);
      expect(result, isNull);
    });

    testUsingContext('compile single expression', () async {
      final BufferLogger logger = context[Logger];

      final Completer<List<int>> compileResponseCompleter =
          Completer<List<int>>();
      final Completer<List<int>> compileExpressionResponseCompleter =
          Completer<List<int>>();

      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) =>
      Stream<List<int>>.fromFutures(
        <Future<List<int>>>[
          compileResponseCompleter.future,
          compileExpressionResponseCompleter.future]));

      compileResponseCompleter.complete(Future<List<int>>.value(utf8.encode(
        'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0\n'
      )));

      await generator.recompile(
        '/path/to/main.dart',
        null, /* invalidatedFiles */
        outputPath: '/build/',
      ).then((CompilerOutput output) {
        expect(mockFrontendServerStdIn.getAndClear(),
            'compile /path/to/main.dart\n');
        verifyNoMoreInteractions(mockFrontendServerStdIn);
        expect(logger.errorText,
            equals('\nCompiler message:\nline1\nline2\n'));
        expect(output.outputFilename, equals('/path/to/main.dart.dill'));

        compileExpressionResponseCompleter.complete(
            Future<List<int>>.value(utf8.encode(
                'result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'
            )));
        generator.compileExpression(
            '2+2', null, null, null, null, false).then(
                (CompilerOutput outputExpression) {
                  expect(outputExpression, isNotNull);
                  expect(outputExpression.outputFilename, equals('/path/to/main.dart.dill.incremental'));
                  expect(outputExpression.errorCount, 0);
                }
        );
      });

    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });

    testUsingContext('compile expressions without awaiting', () async {
      final BufferLogger logger = context[Logger];

      final Completer<List<int>> compileResponseCompleter = Completer<List<int>>();
      final Completer<List<int>> compileExpressionResponseCompleter1 = Completer<List<int>>();
      final Completer<List<int>> compileExpressionResponseCompleter2 = Completer<List<int>>();

      when(mockFrontendServer.stdout)
          .thenAnswer((Invocation invocation) =>
      Stream<List<int>>.fromFutures(
          <Future<List<int>>>[
            compileResponseCompleter.future,
            compileExpressionResponseCompleter1.future,
            compileExpressionResponseCompleter2.future,
          ]));

      // The test manages timing via completers.
      generator.recompile( // ignore: unawaited_futures
        '/path/to/main.dart',
        null, /* invalidatedFiles */
        outputPath: '/build/',
      ).then((CompilerOutput outputCompile) {
        expect(logger.errorText,
            equals('\nCompiler message:\nline1\nline2\n'));
        expect(outputCompile.outputFilename, equals('/path/to/main.dart.dill'));

        compileExpressionResponseCompleter1.complete(Future<List<int>>.value(utf8.encode(
            'result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'
        )));
      });

      // The test manages timing via completers.
      final Completer<bool> lastExpressionCompleted = Completer<bool>();
      generator.compileExpression('0+1', null, null, null, null, false).then( // ignore: unawaited_futures
          (CompilerOutput outputExpression) {
            expect(outputExpression, isNotNull);
            expect(outputExpression.outputFilename,
                equals('/path/to/main.dart.dill.incremental'));
            expect(outputExpression.errorCount, 0);
            compileExpressionResponseCompleter2.complete(Future<List<int>>.value(utf8.encode(
                'result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'
            )));
          });

      // The test manages timing via completers.
      generator.compileExpression('1+1', null, null, null, null, false).then( // ignore: unawaited_futures
          (CompilerOutput outputExpression) {
            expect(outputExpression, isNotNull);
            expect(outputExpression.outputFilename,
                equals('/path/to/main.dart.dill.incremental'));
            expect(outputExpression.errorCount, 0);
            lastExpressionCompleted.complete(true);
          });

      compileResponseCompleter.complete(Future<List<int>>.value(utf8.encode(
          'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0\n'
      )));

      expect(await lastExpressionCompleted.future, isTrue);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
      OutputPreferences: () => OutputPreferences(showColor: false),
      Logger: () => BufferLogger(),
    });
  });
}

Future<void> _recompile(StreamController<List<int>> streamController,
  ResidentCompiler generator, MockStdIn mockFrontendServerStdIn,
  String mockCompilerOutput) async {
  // Put content into the output stream after generator.recompile gets
  // going few lines below, resets completer.
  scheduleMicrotask(() {
    streamController.add(utf8.encode(mockCompilerOutput));
  });
  final CompilerOutput output = await generator.recompile(
    null /* mainPath */,
    <String>['/path/to/main.dart'],
    outputPath: '/build/',
  );
  expect(output.outputFilename, equals('/path/to/main.dart.dill'));
  final String commands = mockFrontendServerStdIn.getAndClear();
  final RegExp re = RegExp('^recompile (.*)\\n/path/to/main.dart\\n(.*)\\n\$');
  expect(commands, matches(re));
  final Match match = re.firstMatch(commands);
  expect(match[1] == match[2], isTrue);
  mockFrontendServerStdIn._stdInWrites.clear();
}

class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {}
class MockStream extends Mock implements Stream<List<int>> {}
class MockStdIn extends Mock implements IOSink {
  final StringBuffer _stdInWrites = StringBuffer();

  String getAndClear() {
    final String result = _stdInWrites.toString();
    _stdInWrites.clear();
    return result;
  }

  @override
  void write([Object o = '']) {
    _stdInWrites.write(o);
  }

  @override
  void writeln([Object o = '']) {
    _stdInWrites.writeln(o);
  }
}