// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api

import 'dart:convert';
import 'dart:io';

import 'package:dart_framework/platform/process.dart';
import 'package:oto/cli/commands/install/regist_path.dart';
import 'package:oto/cli/printer.dart';
import 'package:oto/oto/core/system_runtime.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';

// Fake classes to avoid duplicate definition problems
class _ShellCall {
  final String shell;
  final String? workspace;
  final bool printStdout;
  final bool printStderr;
  final Converter<List<int>, String>? decoder;
  _ShellCall(this.shell, this.workspace, this.printStdout, this.printStderr,
      this.decoder);
}

class _RunExecutableCall {
  final String exe;
  final List<String> args;
  final String? workingDirectory;
  final Encoding? stdoutEncoding;
  final Encoding? stderrEncoding;
  _RunExecutableCall(this.exe, this.args, this.workingDirectory,
      this.stdoutEncoding, this.stderrEncoding);
}

class FakeProcessData extends ProcessData {
  FakeProcessData() : super(false, false) {
    exitCode = 0;
  }

  @override
  Future waitForExit() async {}

  @override
  Process get process =>
      throw UnsupportedError('FakeProcessData has no real process');
}

class FakeSystemRuntime implements SystemRuntime {
  final List<_ShellCall> startShellCalls = [];
  final List<_ShellCall> runShellCalls = [];
  final List<_RunExecutableCall> runExecutableCalls = [];
  final List<int> killedPids = [];

  final bool _isWindows;
  final bool _isMacOS;
  final bool _isLinux;
  final Map<String, String> _environment;

  FakeSystemRuntime({
    bool isWindows = false,
    bool isMacOS = false,
    bool isLinux = true,
    Map<String, String> environment = const {},
  })  : _isWindows = isWindows,
        _isMacOS = isMacOS,
        _isLinux = isLinux,
        _environment = environment;

  @override
  bool get isWindows => _isWindows;
  @override
  bool get isMacOS => _isMacOS;
  @override
  bool get isLinux => _isLinux;
  @override
  Map<String, String> get environment => _environment;

  @override
  Future<ProcessData> startShell(
    StringBuffer shell, {
    String? workspace,
    bool printStdout = true,
    bool printStderr = true,
    Converter<List<int>, String>? decoder,
    LogHandler? logHandler,
  }) async {
    startShellCalls.add(_ShellCall(
        shell.toString(), workspace, printStdout, printStderr, decoder));
    return FakeProcessData();
  }

  @override
  Future<ProcessResult> runShell(
    StringBuffer shell, {
    String? workspace,
    bool printStdout = true,
    bool printStderr = true,
    Converter<List<int>, String>? decoder,
    LogHandler? logHandler,
  }) async {
    runShellCalls.add(_ShellCall(
        shell.toString(), workspace, printStdout, printStderr, decoder));
    return ProcessResult(0, 0, '', '');
  }

  @override
  Future<ProcessResult> runExecutable(
    String exe,
    List<String> args, {
    String? workingDirectory,
    Encoding? stdoutEncoding,
    Encoding? stderrEncoding,
  }) async {
    runExecutableCalls.add(_RunExecutableCall(
        exe, args, workingDirectory, stdoutEncoding, stderrEncoding));
    return ProcessResult(0, 0, '', '');
  }

  @override
  Future<Process> startExecutable(
    String program,
    List<String> args, {
    String? workingDirectory,
    ProcessStartMode mode = ProcessStartMode.normal,
  }) async {
    throw UnsupportedError('FakeSystemRuntime does not return a real Process');
  }

  @override
  bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) {
    killedPids.add(pid);
    return true;
  }
}

void main() {
  group('Printer Runtime Rollout Tests', () {
    test('unix printer delegates script execution to system runtime', () async {
      final fakeRuntime = FakeSystemRuntime(
        isWindows: false,
        isMacOS: false,
        isLinux: true,
      );

      final printer = Printer.unix(runtime: fakeRuntime);
      await printer.printCLI(['hello', 'world']);

      expect(fakeRuntime.runExecutableCalls, hasLength(1));
      final call = fakeRuntime.runExecutableCalls.first;
      expect(call.exe, 'bash');
      expect(call.args, hasLength(1));
      expect(call.stdoutEncoding, utf8);
      expect(call.stderrEncoding, utf8);
      final tempFilePath = call.args.first;
      expect(tempFilePath, contains('temp_'));
      expect(tempFilePath, endsWith('.sh'));

      final tempFile = File(tempFilePath);
      expect(tempFile.existsSync(), isFalse); // printer deletes it
    });

    test('windows printer delegates script execution to system runtime',
        () async {
      final fakeRuntime = FakeSystemRuntime(
        isWindows: true,
        isMacOS: false,
        isLinux: false,
      );

      final printer = Printer.windows(runtime: fakeRuntime);
      await printer.printCLI(['hello', 'world']);

      expect(fakeRuntime.runExecutableCalls, hasLength(1));
      final call = fakeRuntime.runExecutableCalls.first;
      expect(call.exe, 'cmd');
      expect(call.args, ['/C', contains('temp_')]);
      final tempFilePath = call.args[1];
      expect(tempFilePath, endsWith('.bat'));

      final tempFile = File(tempFilePath);
      expect(tempFile.existsSync(), isFalse); // printer deletes it
    });
  });

  group('RegistPath Runtime Rollout Tests', () {
    late Directory tempDir;

    setUp(() async {
      tempDir = await Directory.systemTemp.createTemp('regist_path_test');
    });

    tearDown(() async {
      await tempDir.delete(recursive: true);
    });

    test('regist_path osx sources profiles through system runtime', () async {
      final fakeRuntime = FakeSystemRuntime(
        isWindows: false,
        isMacOS: true,
        isLinux: false,
        environment: {'HOME': tempDir.path},
      );

      final registPath = RegistPath.create(runtime: fakeRuntime);

      final zprofileFile = File(path.join(tempDir.path, '.zprofile'));
      final bashProfileFile = File(path.join(tempDir.path, '.bash_profile'));

      await registPath.add('/path/to/my/bin');

      expect(zprofileFile.existsSync(), isTrue);
      expect(bashProfileFile.existsSync(), isTrue);

      final zprofileContent = await zprofileFile.readAsString();
      expect(zprofileContent, contains('export PATH=\${PATH}:/path/to/my/bin'));

      expect(fakeRuntime.runShellCalls, hasLength(1));
      expect(fakeRuntime.runShellCalls.first.shell, contains('source'));

      // remove test
      await registPath.remove('/path/to/my/bin');
      final zprofileContentAfter = await zprofileFile.readAsString();
      expect(zprofileContentAfter, isNot(contains('/path/to/my/bin')));
    });

    test('regist_path windows delegates setx through system runtime', () async {
      final fakeRuntime = FakeSystemRuntime(
        isWindows: true,
        isMacOS: false,
        isLinux: false,
        environment: {'UserProfile': tempDir.path},
      );

      final registPath = RegistPath.create(runtime: fakeRuntime);

      await registPath.add('/path/to/win/bin');

      expect(fakeRuntime.runShellCalls, hasLength(1));
      expect(fakeRuntime.runShellCalls.first.shell,
          contains('setx path "%PathUser%;/path/to/win/bin'));

      fakeRuntime.runShellCalls.clear();

      // remove test (windows path removal creates a temporary path.txt in user profile)
      final pathTxtFile = File(path.join(tempDir.path, 'path.txt'));
      await pathTxtFile.writeAsString('some_old_path;/path/to/win/bin');

      await registPath.remove('/path/to/win/bin');

      // The temporary file pathTxt should be deleted after processing
      expect(pathTxtFile.existsSync(), isFalse);

      // There should be two shell calls: first to query/write path.txt, second to setx the new path.
      expect(fakeRuntime.runShellCalls, hasLength(2));
      expect(fakeRuntime.runShellCalls[1].shell,
          contains('setx path "some_old_path"'));
    });
  });
}
