// 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/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cmake_project.dart';
import 'package:flutter_tools/src/windows/migrations/version_migration.dart';
import 'package:test/fake.dart';

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

void main () {
  group('Windows Flutter version migration', () {
    late MemoryFileSystem memoryFileSystem;
    late BufferLogger testLogger;
    late FakeWindowsProject mockProject;
    late File cmakeFile;
    late File resourceFile;

    setUp(() {
      memoryFileSystem = MemoryFileSystem.test();
      cmakeFile = memoryFileSystem.file('CMakeLists.txt');
      resourceFile = memoryFileSystem.file('Runner.rc');

      testLogger = BufferLogger(
        terminal: Terminal.test(),
        outputPreferences: OutputPreferences.test(),
      );

      mockProject = FakeWindowsProject(cmakeFile, resourceFile);
    });

    testWithoutContext('skipped if CMake file is missing', () {
      const String resourceFileContents = 'Hello world';

      resourceFile.writeAsStringSync(resourceFileContents);
      final VersionMigration migration = VersionMigration(
        mockProject,
        testLogger,
      );
      migration.migrate();
      expect(cmakeFile.existsSync(), isFalse);
      expect(resourceFile.existsSync(), isTrue);

      expect(testLogger.traceText,
        contains('windows/runner/CMakeLists.txt file not found, skipping version migration'));
      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if resource file is missing', () {
      const String cmakeFileContents = 'Hello world';

      cmakeFile.writeAsStringSync(cmakeFileContents);
      final VersionMigration migration = VersionMigration(
        mockProject,
        testLogger,
      );
      migration.migrate();
      expect(cmakeFile.existsSync(), isTrue);
      expect(resourceFile.existsSync(), isFalse);

      expect(testLogger.traceText,
        contains('windows/runner/Runner.rc file not found, skipping version migration'));
      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if nothing to migrate', () {
      const String cmakeFileContents = 'Nothing to migrate';
      const String resourceFileContents = 'Nothing to migrate';

      cmakeFile.writeAsStringSync(cmakeFileContents);
      resourceFile.writeAsStringSync(resourceFileContents);

      final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync();
      final DateTime resourceUpdatedAt = resourceFile.lastModifiedSync();

      final VersionMigration versionMigration = VersionMigration(
        mockProject,
        testLogger,
      );
      versionMigration.migrate();

      expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt);
      expect(cmakeFile.readAsStringSync(), cmakeFileContents);
      expect(resourceFile.lastModifiedSync(), resourceUpdatedAt);
      expect(resourceFile.readAsStringSync(), resourceFileContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if already migrated', () {
      const String cmakeFileContents =
        '# Apply the standard set of build settings. This can be removed for applications\n'
        '# that need different build settings.\n'
        'apply_standard_settings(\${BINARY_NAME})\n'
        '\n'
        '# Add preprocessor definitions for the build version.\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\n'
        '\n'
        '# Disable Windows macros that collide with C++ standard library functions.\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\n';
      const String resourceFileContents =
        '#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n'
        '#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n'
        '#else\n'
        '#define VERSION_AS_NUMBER 1,0,0,0\n'
        '#endif\n'
        '\n'
        '#if defined(FLUTTER_VERSION)\n'
        '#define VERSION_AS_STRING FLUTTER_VERSION\n'
        '#else\n'
        '#define VERSION_AS_STRING "1.0.0"\n'
        '#endif\n';

      cmakeFile.writeAsStringSync(cmakeFileContents);
      resourceFile.writeAsStringSync(resourceFileContents);

      final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync();
      final DateTime resourceUpdatedAt = resourceFile.lastModifiedSync();

      final VersionMigration versionMigration = VersionMigration(
        mockProject,
        testLogger,
      );
      versionMigration.migrate();

      expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt);
      expect(cmakeFile.readAsStringSync(), cmakeFileContents);
      expect(resourceFile.lastModifiedSync(), resourceUpdatedAt);
      expect(resourceFile.readAsStringSync(), resourceFileContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if already migrated (CRLF)', () {
      const String cmakeFileContents =
        '# Apply the standard set of build settings. This can be removed for applications\r\n'
        '# that need different build settings.\r\n'
        'apply_standard_settings(\${BINARY_NAME})\r\n'
        '\r\n'
        '# Add preprocessor definitions for the build version.\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\r\n'
        '\r\n'
        '# Disable Windows macros that collide with C++ standard library functions.\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\r\n';
      const String resourceFileContents =
        '#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\r\n'
        '#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\r\n'
        '#else\r\n'
        '#define VERSION_AS_NUMBER 1,0,0,0\r\n'
        '#endif\r\n'
        '\r\n'
        '#if defined(FLUTTER_VERSION)\r\n'
        '#define VERSION_AS_STRING FLUTTER_VERSION\r\n'
        '#else\r\n'
        '#define VERSION_AS_STRING "1.0.0"\r\n'
        '#endif\r\n';

      cmakeFile.writeAsStringSync(cmakeFileContents);
      resourceFile.writeAsStringSync(resourceFileContents);

      final DateTime cmakeUpdatedAt = cmakeFile.lastModifiedSync();
      final DateTime resourceUpdatedAt = resourceFile.lastModifiedSync();

      final VersionMigration versionMigration = VersionMigration(
        mockProject,
        testLogger,
      );
      versionMigration.migrate();

      expect(cmakeFile.lastModifiedSync(), cmakeUpdatedAt);
      expect(cmakeFile.readAsStringSync(), cmakeFileContents);
      expect(resourceFile.lastModifiedSync(), resourceUpdatedAt);
      expect(resourceFile.readAsStringSync(), resourceFileContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('migrates project to set version information', () {
      cmakeFile.writeAsStringSync(
        '# Apply the standard set of build settings. This can be removed for applications\n'
        '# that need different build settings.\n'
        'apply_standard_settings(\${BINARY_NAME})\n'
        '\n'
        '# Disable Windows macros that collide with C++ standard library functions.\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\n'
      );
      resourceFile.writeAsStringSync(
        '#ifdef FLUTTER_BUILD_NUMBER\n'
        '#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\n'
        '#else\n'
        '#define VERSION_AS_NUMBER 1,0,0\n'
        '#endif\n'
        '\n'
        '#ifdef FLUTTER_BUILD_NAME\n'
        '#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\n'
        '#else\n'
        '#define VERSION_AS_STRING "1.0.0"\n'
        '#endif\n'
      );

      final VersionMigration versionMigration = VersionMigration(
        mockProject,
        testLogger,
      );
      versionMigration.migrate();

      expect(cmakeFile.readAsStringSync(),
        '# Apply the standard set of build settings. This can be removed for applications\n'
        '# that need different build settings.\n'
        'apply_standard_settings(\${BINARY_NAME})\n'
        '\n'
        '# Add preprocessor definitions for the build version.\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\n'
        '\n'
        '# Disable Windows macros that collide with C++ standard library functions.\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\n'
      );
      expect(resourceFile.readAsStringSync(),
        '#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n'
        '#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n'
        '#else\n'
        '#define VERSION_AS_NUMBER 1,0,0,0\n'
        '#endif\n'
        '\n'
        '#if defined(FLUTTER_VERSION)\n'
        '#define VERSION_AS_STRING FLUTTER_VERSION\n'
        '#else\n'
        '#define VERSION_AS_STRING "1.0.0"\n'
        '#endif\n'
      );

      expect(testLogger.statusText, contains('windows/runner/CMakeLists.txt does not define version information, updating.'));
      expect(testLogger.statusText, contains('windows/runner/Runner.rc does not use Flutter version information, updating.'));
    });

    testWithoutContext('migrates project to set version information (CRLF)', () {
      cmakeFile.writeAsStringSync(
        '# Apply the standard set of build settings. This can be removed for applications\r\n'
        '# that need different build settings.\r\n'
        'apply_standard_settings(\${BINARY_NAME})\r\n'
        '\r\n'
        '# Disable Windows macros that collide with C++ standard library functions.\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\r\n'
      );
      resourceFile.writeAsStringSync(
        '#ifdef FLUTTER_BUILD_NUMBER\r\n'
        '#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\r\n'
        '#else\r\n'
        '#define VERSION_AS_NUMBER 1,0,0\r\n'
        '#endif\r\n'
        '\r\n'
        '#ifdef FLUTTER_BUILD_NAME\r\n'
        '#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\r\n'
        '#else\r\n'
        '#define VERSION_AS_STRING "1.0.0"\r\n'
        '#endif\r\n'
      );

      final VersionMigration versionMigration = VersionMigration(
        mockProject,
        testLogger,
      );
      versionMigration.migrate();

      expect(cmakeFile.readAsStringSync(),
        '# Apply the standard set of build settings. This can be removed for applications\r\n'
        '# that need different build settings.\r\n'
        'apply_standard_settings(\${BINARY_NAME})\r\n'
        '\r\n'
        '# Add preprocessor definitions for the build version.\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\\"\${FLUTTER_VERSION}\\"")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=\${FLUTTER_VERSION_MAJOR}")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=\${FLUTTER_VERSION_MINOR}")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=\${FLUTTER_VERSION_PATCH}")\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=\${FLUTTER_VERSION_BUILD}")\r\n'
        '\r\n'
        '# Disable Windows macros that collide with C++ standard library functions.\r\n'
        'target_compile_definitions(\${BINARY_NAME} PRIVATE "NOMINMAX")\r\n'
      );
      expect(resourceFile.readAsStringSync(),
        '#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\r\n'
        '#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\r\n'
        '#else\r\n'
        '#define VERSION_AS_NUMBER 1,0,0,0\r\n'
        '#endif\r\n'
        '\r\n'
        '#if defined(FLUTTER_VERSION)\r\n'
        '#define VERSION_AS_STRING FLUTTER_VERSION\r\n'
        '#else\r\n'
        '#define VERSION_AS_STRING "1.0.0"\r\n'
        '#endif\r\n'
      );

      expect(testLogger.statusText, contains('windows/runner/CMakeLists.txt does not define version information, updating.'));
      expect(testLogger.statusText, contains('windows/runner/Runner.rc does not use Flutter version information, updating.'));
    });
  });
}

class FakeWindowsProject extends Fake implements WindowsProject {
  FakeWindowsProject(this.runnerCmakeFile, this.runnerResourceFile);

  @override
  final File runnerCmakeFile;

  @override
  final File runnerResourceFile;
}