// 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 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/migrate/migrate_manifest.dart';
import 'package:flutter_tools/src/migrate/migrate_result.dart';
import 'package:flutter_tools/src/migrate/migrate_utils.dart';

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

void main() {
  late FileSystem fileSystem;
  late File manifestFile;
  late BufferLogger logger;

  setUpAll(() {
    fileSystem = MemoryFileSystem.test();
    logger = BufferLogger.test();
    manifestFile = fileSystem.file('.migrate_manifest');
  });

  group('checkAndPrintMigrateStatus', () {
    testWithoutContext('empty MigrateResult produces empty output', () async {
      final Directory workingDir = fileSystem.directory('migrate_working_dir');
      workingDir.createSync(recursive: true);
      final MigrateManifest manifest = MigrateManifest(migrateRootDir: workingDir, migrateResult: MigrateResult(
        mergeResults: <MergeResult>[],
        addedFiles: <FilePendingMigration>[],
        deletedFiles: <FilePendingMigration>[],
        mergeTypeMap: <String, MergeType>{},
        diffMap: <String, DiffResult>{},
        tempDirectories: <Directory>[],
        sdkDirs: <String, Directory>{},
      ));

      checkAndPrintMigrateStatus(manifest, workingDir, warnConflict: true, logger: logger);

      expect(logger.statusText, contains('\n'));
    });

    testWithoutContext('populated MigrateResult produces correct output', () async {
      final Directory workingDir = fileSystem.directory('migrate_working_dir');
      workingDir.createSync(recursive: true);
      final MigrateManifest manifest = MigrateManifest(migrateRootDir: workingDir, migrateResult: MigrateResult(
        mergeResults: <MergeResult>[
          StringMergeResult.explicit(
            localPath: 'merged_file',
            mergedString: 'str',
            hasConflict: false,
            exitCode: 0,
          ),
          StringMergeResult.explicit(
            localPath: 'conflict_file',
            mergedString: 'hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n',
            hasConflict: true,
            exitCode: 1,
          ),
        ],
        addedFiles: <FilePendingMigration>[FilePendingMigration('added_file', fileSystem.file('added_file'))],
        deletedFiles: <FilePendingMigration>[FilePendingMigration('deleted_file', fileSystem.file('deleted_file'))],
        // The following are ignored by the manifest.
        mergeTypeMap: <String, MergeType>{'test': MergeType.threeWay},
        diffMap: <String, DiffResult>{},
        tempDirectories: <Directory>[],
        sdkDirs: <String, Directory>{},
      ));

      final File conflictFile = workingDir.childFile('conflict_file');
      conflictFile.writeAsStringSync('hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n', flush: true);

      checkAndPrintMigrateStatus(manifest, workingDir, warnConflict: true, logger: logger);

      expect(logger.statusText, contains('''
Added files:
  - added_file
Deleted files:
  - deleted_file
Modified files:
  - conflict_file
  - merged_file
'''));
    });

    testWithoutContext('populated MigrateResult detects fixed conflict', () async {
      final Directory workingDir = fileSystem.directory('migrate_working_dir');
      workingDir.createSync(recursive: true);
      final MigrateManifest manifest = MigrateManifest(migrateRootDir: workingDir, migrateResult: MigrateResult(
        mergeResults: <MergeResult>[
          StringMergeResult.explicit(
            localPath: 'merged_file',
            mergedString: 'str',
            hasConflict: false,
            exitCode: 0,
          ),
          StringMergeResult.explicit(
            localPath: 'conflict_file',
            mergedString: 'hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n',
            hasConflict: true,
            exitCode: 1,
          ),
        ],
        addedFiles: <FilePendingMigration>[FilePendingMigration('added_file', fileSystem.file('added_file'))],
        deletedFiles: <FilePendingMigration>[FilePendingMigration('deleted_file', fileSystem.file('deleted_file'))],
        // The following are ignored by the manifest.
        mergeTypeMap: <String, MergeType>{'test': MergeType.threeWay},
        diffMap: <String, DiffResult>{},
        tempDirectories: <Directory>[],
        sdkDirs: <String, Directory>{},
      ));

      final File conflictFile = workingDir.childFile('conflict_file');
      conflictFile.writeAsStringSync('hello\nwow a bunch of lines\nhi\n', flush: true);

      checkAndPrintMigrateStatus(manifest, workingDir, warnConflict: true, logger: logger);
      expect(logger.statusText, contains('''
Added files:
  - added_file
Deleted files:
  - deleted_file
Modified files:
  - conflict_file
  - merged_file
'''));
    });
  });

  group('manifest file parsing', () {
    testWithoutContext('empty fails', () async {
      manifestFile.writeAsStringSync('');
      bool exceptionFound = false;
      try {
        MigrateManifest.fromFile(manifestFile);
      } on Exception catch (e) {
        exceptionFound = true;
        expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is not a Yaml map.');
      }
      expect(exceptionFound, true);
    });

    testWithoutContext('invalid name fails', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
        conflict_files:
        added_filessssss:
        deleted_files:
      ''');
      bool exceptionFound = false;
      try {
       MigrateManifest.fromFile(manifestFile);
      } on Exception catch (e) {
        exceptionFound = true;
        expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
      }
      expect(exceptionFound, true);
    });

    testWithoutContext('missing name fails', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
        conflict_files:
        deleted_files:
      ''');
      bool exceptionFound = false;
      try {
        MigrateManifest.fromFile(manifestFile);
      } on Exception catch (e) {
        exceptionFound = true;
        expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
      }
      expect(exceptionFound, true);
    });

    testWithoutContext('wrong entry type fails', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
        conflict_files:
          other_key:
        added_files:
        deleted_files:
      ''');
      bool exceptionFound = false;
      try {
        MigrateManifest.fromFile(manifestFile);
      } on Exception catch (e) {
        exceptionFound = true;
        expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. Entry is not a Yaml list.');
      }
      expect(exceptionFound, true);
    });

    testWithoutContext('unpopulated succeeds', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
        conflict_files:
        added_files:
        deleted_files:
      ''');
      final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
      expect(manifest.mergedFiles.isEmpty, true);
      expect(manifest.conflictFiles.isEmpty, true);
      expect(manifest.addedFiles.isEmpty, true);
      expect(manifest.deletedFiles.isEmpty, true);
    });

    testWithoutContext('order does not matter', () async {
      manifestFile.writeAsStringSync('''
        added_files:
        merged_files:
        deleted_files:
        conflict_files:
      ''');
      final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
      expect(manifest.mergedFiles.isEmpty, true);
      expect(manifest.conflictFiles.isEmpty, true);
      expect(manifest.addedFiles.isEmpty, true);
      expect(manifest.deletedFiles.isEmpty, true);
    });

    testWithoutContext('basic succeeds', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
          - file1
        conflict_files:
          - file2
        added_files:
          - file3
        deleted_files:
          - file4
      ''');
      final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
      expect(manifest.mergedFiles.isEmpty, false);
      expect(manifest.conflictFiles.isEmpty, false);
      expect(manifest.addedFiles.isEmpty, false);
      expect(manifest.deletedFiles.isEmpty, false);

      expect(manifest.mergedFiles.length, 1);
      expect(manifest.conflictFiles.length, 1);
      expect(manifest.addedFiles.length, 1);
      expect(manifest.deletedFiles.length, 1);

      expect(manifest.mergedFiles[0], 'file1');
      expect(manifest.conflictFiles[0], 'file2');
      expect(manifest.addedFiles[0], 'file3');
      expect(manifest.deletedFiles[0], 'file4');
    });

    testWithoutContext('basic multi-list succeeds', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
          - file1
          - file2
        conflict_files:
        added_files:
        deleted_files:
          - file3
          - file4
      ''');
      final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
      expect(manifest.mergedFiles.isEmpty, false);
      expect(manifest.conflictFiles.isEmpty, true);
      expect(manifest.addedFiles.isEmpty, true);
      expect(manifest.deletedFiles.isEmpty, false);

      expect(manifest.mergedFiles.length, 2);
      expect(manifest.conflictFiles.length, 0);
      expect(manifest.addedFiles.length, 0);
      expect(manifest.deletedFiles.length, 2);

      expect(manifest.mergedFiles[0], 'file1');
      expect(manifest.mergedFiles[1], 'file2');
      expect(manifest.deletedFiles[0], 'file3');
      expect(manifest.deletedFiles[1], 'file4');
    });
  });

  group('manifest MigrateResult creation', () {
    testWithoutContext('empty MigrateResult', () async {
      final MigrateManifest manifest = MigrateManifest(migrateRootDir: fileSystem.directory('root'), migrateResult: MigrateResult(
        mergeResults: <MergeResult>[],
        addedFiles: <FilePendingMigration>[],
        deletedFiles: <FilePendingMigration>[],
        mergeTypeMap: <String, MergeType>{},
        diffMap: <String, DiffResult>{},
        tempDirectories: <Directory>[],
        sdkDirs: <String, Directory>{},
      ));
      expect(manifest.mergedFiles.isEmpty, true);
      expect(manifest.conflictFiles.isEmpty, true);
      expect(manifest.addedFiles.isEmpty, true);
      expect(manifest.deletedFiles.isEmpty, true);
    });

    testWithoutContext('simple MigrateResult', () async {
      final MigrateManifest manifest = MigrateManifest(migrateRootDir: fileSystem.directory('root'), migrateResult: MigrateResult(
        mergeResults: <MergeResult>[
          StringMergeResult.explicit(
            localPath: 'merged_file',
            mergedString: 'str',
            hasConflict: false,
            exitCode: 0,
          ),
          StringMergeResult.explicit(
            localPath: 'conflict_file',
            mergedString: '<<<<<<<<<<<',
            hasConflict: true,
            exitCode: 1,
          ),
        ],
        addedFiles: <FilePendingMigration>[FilePendingMigration('added_file', fileSystem.file('added_file'))],
        deletedFiles: <FilePendingMigration>[FilePendingMigration('deleted_file', fileSystem.file('deleted_file'))],
        // The following are ignored by the manifest.
        mergeTypeMap: <String, MergeType>{'test': MergeType.threeWay},
        diffMap: <String, DiffResult>{},
        tempDirectories: <Directory>[],
        sdkDirs: <String, Directory>{},
      ));
      expect(manifest.mergedFiles.isEmpty, false);
      expect(manifest.conflictFiles.isEmpty, false);
      expect(manifest.addedFiles.isEmpty, false);
      expect(manifest.deletedFiles.isEmpty, false);

      expect(manifest.mergedFiles.length, 1);
      expect(manifest.conflictFiles.length, 1);
      expect(manifest.addedFiles.length, 1);
      expect(manifest.deletedFiles.length, 1);

      expect(manifest.mergedFiles[0], 'merged_file');
      expect(manifest.conflictFiles[0], 'conflict_file');
      expect(manifest.addedFiles[0], 'added_file');
      expect(manifest.deletedFiles[0], 'deleted_file');
    });
  });

  group('manifest write', () {
    testWithoutContext('empty', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
        conflict_files:
        added_files:
        deleted_files:
      ''');
      final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
      expect(manifest.mergedFiles.isEmpty, true);
      expect(manifest.conflictFiles.isEmpty, true);
      expect(manifest.addedFiles.isEmpty, true);
      expect(manifest.deletedFiles.isEmpty, true);

      manifest.writeFile();
      expect(manifestFile.readAsStringSync(), '''
merged_files:
conflict_files:
added_files:
deleted_files:
''');
    });

    testWithoutContext('basic multi-list', () async {
      manifestFile.writeAsStringSync('''
        merged_files:
          - file1
          - file2
        conflict_files:
        added_files:
        deleted_files:
          - file3
          - file4
      ''');
      final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
      expect(manifest.mergedFiles.isEmpty, false);
      expect(manifest.conflictFiles.isEmpty, true);
      expect(manifest.addedFiles.isEmpty, true);
      expect(manifest.deletedFiles.isEmpty, false);

      expect(manifest.mergedFiles.length, 2);
      expect(manifest.conflictFiles.length, 0);
      expect(manifest.addedFiles.length, 0);
      expect(manifest.deletedFiles.length, 2);

      expect(manifest.mergedFiles[0], 'file1');
      expect(manifest.mergedFiles[1], 'file2');
      expect(manifest.deletedFiles[0], 'file3');
      expect(manifest.deletedFiles[1], 'file4');

      manifest.writeFile();
      expect(manifestFile.readAsStringSync(), '''
merged_files:
  - file1
  - file2
conflict_files:
added_files:
deleted_files:
  - file3
  - file4
''');
    });
  });
}