// Copyright 2014 The Flutter 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 'package:fake_async/fake_async.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/analyze.dart';
import 'package:flutter_tools/src/dart/analysis.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/project_validator.dart';
import 'package:process/process.dart';

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
import '../../src/test_flutter_command_runner.dart';

void main() {
  setUpAll(() {
    Cache.flutterRoot = getFlutterRoot();
  });

  late Directory tempDir;
  late FileSystem fileSystem;
  late Platform platform;
  late ProcessManager processManager;
  late AnsiTerminal terminal;
  late Logger logger;
  late FakeStdio mockStdio;

  setUp(() {
    fileSystem = globals.localFileSystem;
    platform = const LocalPlatform();
    processManager = const LocalProcessManager();
    terminal = AnsiTerminal(platform: platform, stdio: Stdio());
    logger = BufferLogger(outputPreferences: OutputPreferences.test(), terminal: terminal);
    tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_analysis_test.');
    mockStdio = FakeStdio();
  });

  tearDown(() {
    tryToDelete(tempDir);
  });


  void createSampleProject(Directory directory, { bool brokenCode = false }) {
    final File pubspecFile = fileSystem.file(fileSystem.path.join(directory.path, 'pubspec.yaml'));
    pubspecFile.writeAsStringSync('''
  name: foo_project
  environment:
    sdk: '>=3.0.0-0 <4.0.0'
  ''');

    final File dartFile = fileSystem.file(fileSystem.path.join(directory.path, 'lib', 'main.dart'));
    dartFile.parent.createSync();
    dartFile.writeAsStringSync('''
  void main() {
    print('hello world');
    ${brokenCode ? 'prints("hello world");' : ''}
  }
  ''');
  }

  group('analyze --watch', () {
    testUsingContext('AnalysisServer success', () async {
      createSampleProject(tempDir);

      final Pub pub = Pub.test(
        fileSystem: fileSystem,
        logger: logger,
        processManager: processManager,
        platform: const LocalPlatform(),
        botDetector: globals.botDetector,
        usage: globals.flutterUsage,
        stdio: mockStdio,
      );
      await pub.get(
        context: PubContext.flutterTests,
        project: FlutterProject.fromDirectoryTest(tempDir),
      );

      final AnalysisServer server = AnalysisServer(
        globals.artifacts!.getArtifactPath(Artifact.engineDartSdkPath),
        <String>[tempDir.path],
        fileSystem: fileSystem,
        platform: platform,
        processManager: processManager,
        logger: logger,
        terminal: terminal,
        suppressAnalytics: true,
      );

      int errorCount = 0;
      final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first;
      server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length);

      await server.start();
      await onDone;

      expect(errorCount, 0);

      await server.dispose();
    });
  });

  testUsingContext('AnalysisServer errors', () async {
    createSampleProject(tempDir, brokenCode: true);

    final Pub pub = Pub.test(
      fileSystem: fileSystem,
      logger: logger,
      processManager: processManager,
      platform: const LocalPlatform(),
      usage: globals.flutterUsage,
      botDetector: globals.botDetector,
      stdio: mockStdio,
    );
    await pub.get(
      context: PubContext.flutterTests,
      project: FlutterProject.fromDirectoryTest(tempDir),
    );

    final AnalysisServer server = AnalysisServer(
      globals.artifacts!.getArtifactPath(Artifact.engineDartSdkPath),
      <String>[tempDir.path],
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      logger: logger,
      terminal: terminal,
      suppressAnalytics: true,
    );

    int errorCount = 0;
    final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first;
    server.onErrors.listen((FileAnalysisErrors errors) {
      errorCount += errors.errors.length;
    });

    await server.start();
    await onDone;

    expect(errorCount, greaterThan(0));

    await server.dispose();
  });

  testUsingContext('Returns no errors when source is error-free', () async {
    const String contents = "StringBuffer bar = StringBuffer('baz');";
    tempDir.childFile('main.dart').writeAsStringSync(contents);
    final AnalysisServer server = AnalysisServer(
      globals.artifacts!.getArtifactPath(Artifact.engineDartSdkPath),
      <String>[tempDir.path],
      fileSystem: fileSystem,
      platform: platform,
      processManager: processManager,
      logger: logger,
      terminal: terminal,
      suppressAnalytics: true,
    );

    int errorCount = 0;
    final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first;
    server.onErrors.listen((FileAnalysisErrors errors) {
      errorCount += errors.errors.length;
    });
    await server.start();
    await onDone;
    expect(errorCount, 0);
    await server.dispose();
  });

  testUsingContext('Can run AnalysisService without suppressing analytics', () async {
    final StreamController<List<int>> stdin = StreamController<List<int>>();
    final FakeProcessManager processManager = FakeProcessManager.list(
      <FakeCommand>[
        FakeCommand(
          command: const <String>[
            'Artifact.engineDartSdkPath/bin/dart',
            '--disable-dart-dev',
            'Artifact.engineDartSdkPath/bin/snapshots/analysis_server.dart.snapshot',
            '--disable-server-feature-completion',
            '--disable-server-feature-search',
            '--sdk',
            'Artifact.engineDartSdkPath',
          ],
          stdin: IOSink(stdin.sink),
        ),
      ]);

    final Artifacts artifacts = Artifacts.test();
    final AnalyzeCommand command = AnalyzeCommand(
      terminal: Terminal.test(),
      artifacts: artifacts,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
      fileSystem: MemoryFileSystem.test(),
      processManager: processManager,
      allProjectValidators: <ProjectValidator>[],
      suppressAnalytics: false,
    );

    final TestFlutterCommandRunner commandRunner = TestFlutterCommandRunner();
    commandRunner.addCommand(command);
    unawaited(commandRunner.run(<String>['analyze', '--watch']));
    await stdin.stream.first;

    expect(processManager, hasNoRemainingExpectations);
  });

  testUsingContext('Can run AnalysisService with customized cache location', () async {
    final StreamController<List<int>> stdin = StreamController<List<int>>();
    final FakeProcessManager processManager = FakeProcessManager.list(
      <FakeCommand>[
        FakeCommand(
          command: const <String>[
            'Artifact.engineDartSdkPath/bin/dart',
            '--disable-dart-dev',
            'Artifact.engineDartSdkPath/bin/snapshots/analysis_server.dart.snapshot',
            '--disable-server-feature-completion',
            '--disable-server-feature-search',
            '--sdk',
            'Artifact.engineDartSdkPath',
            '--suppress-analytics',
          ],
          stdin: IOSink(stdin.sink),
        ),
      ]);

    final Artifacts artifacts = Artifacts.test();
    final AnalyzeCommand command = AnalyzeCommand(
      terminal: Terminal.test(),
      artifacts: artifacts,
      logger: BufferLogger.test(),
      platform: FakePlatform(),
      fileSystem: MemoryFileSystem.test(),
      processManager: processManager,
      allProjectValidators: <ProjectValidator>[],
      suppressAnalytics: true,
    );

    final TestFlutterCommandRunner commandRunner = TestFlutterCommandRunner();
    commandRunner.addCommand(command);
    unawaited(commandRunner.run(<String>['analyze', '--watch']));
    await stdin.stream.first;

    expect(processManager, hasNoRemainingExpectations);
  });

  testUsingContext('Can run AnalysisService with customized cache location --watch', () async {
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    fileSystem.directory('directoryA').childFile('foo').createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();

    final Completer<void> completer = Completer<void>();
    final StreamController<List<int>> stdin = StreamController<List<int>>();
    final FakeProcessManager processManager = FakeProcessManager.list(
      <FakeCommand>[
        FakeCommand(
          command: const <String>[
            'Artifact.engineDartSdkPath/bin/dart',
            '--disable-dart-dev',
            'Artifact.engineDartSdkPath/bin/snapshots/analysis_server.dart.snapshot',
            '--disable-server-feature-completion',
            '--disable-server-feature-search',
            '--sdk',
            'Artifact.engineDartSdkPath',
            '--suppress-analytics',
          ],
          stdin: IOSink(stdin.sink),
          stdout: '''
{"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
{"event":"analysis.errors","params":{"file":"/directoryA/foo","errors":[{"type":"TestError","message":"It's an error.","severity":"warning","code":"500","location":{"file":"/directoryA/foo","startLine": 100,"startColumn":5,"offset":0}}]}}
{"event":"server.status","params":{"analysis":{"isAnalyzing":false}}}
'''
        ),
      ]);

    final Artifacts artifacts = Artifacts.test();
    final AnalyzeCommand command = AnalyzeCommand(
      terminal: Terminal.test(),
      artifacts: artifacts,
      logger: logger,
      platform: FakePlatform(),
      fileSystem: fileSystem,
      processManager: processManager,
      allProjectValidators: <ProjectValidator>[],
      suppressAnalytics: true,
    );

    await FakeAsync().run((FakeAsync time) async {
      final TestFlutterCommandRunner commandRunner = TestFlutterCommandRunner();
      commandRunner.addCommand(command);
      unawaited(commandRunner.run(<String>['analyze', '--watch']));

      while (!logger.statusText.contains('analyzed 1 file')) {
        time.flushMicrotasks();
      }
      completer.complete();
      return completer.future;
    });
    expect(logger.statusText, contains("warning • It's an error • directoryA/foo:100:5 • 500"));
    expect(logger.statusText, contains('1 issue found. (1 new)'));
    expect(logger.errorText, isEmpty);
    expect(processManager, hasNoRemainingExpectations);
  });

  testUsingContext('AnalysisService --watch skips errors from non-files', () async {
    final BufferLogger logger = BufferLogger.test();
    final Completer<void> completer = Completer<void>();
    final StreamController<List<int>> stdin = StreamController<List<int>>();
    final FakeProcessManager processManager = FakeProcessManager.list(
        <FakeCommand>[
          FakeCommand(
              command: const <String>[
                'Artifact.engineDartSdkPath/bin/dart',
                '--disable-dart-dev',
                'Artifact.engineDartSdkPath/bin/snapshots/analysis_server.dart.snapshot',
                '--disable-server-feature-completion',
                '--disable-server-feature-search',
                '--sdk',
                'Artifact.engineDartSdkPath',
                '--suppress-analytics',
              ],
              stdin: IOSink(stdin.sink),
              stdout: '''
{"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
{"event":"analysis.errors","params":{"file":"/directoryA/bar","errors":[{"type":"TestError","message":"It's an error.","severity":"warning","code":"500","location":{"file":"/directoryA/bar","startLine":100,"startColumn":5,"offset":0}}]}}
{"event":"server.status","params":{"analysis":{"isAnalyzing":false}}}
'''
          ),
        ]);

    final Artifacts artifacts = Artifacts.test();
    final AnalyzeCommand command = AnalyzeCommand(
      terminal: Terminal.test(),
      artifacts: artifacts,
      logger: logger,
      platform: FakePlatform(),
      fileSystem: MemoryFileSystem.test(),
      processManager: processManager,
      allProjectValidators: <ProjectValidator>[],
      suppressAnalytics: true,
    );

    await FakeAsync().run((FakeAsync time) async {
      final TestFlutterCommandRunner commandRunner = TestFlutterCommandRunner();
      commandRunner.addCommand(command);
      unawaited(commandRunner.run(<String>['analyze', '--watch']));

      while (!logger.statusText.contains('analyzed 1 file')) {
        time.flushMicrotasks();
      }
      completer.complete();
      return completer.future;
    });

    expect(logger.statusText, contains('No issues found!'));
    expect(logger.errorText, isEmpty);
    expect(processManager, hasNoRemainingExpectations);
  });
}
