// ignore_for_file: depend_on_referenced_packages

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

import 'package:dart_framework/platform/process.dart';
import 'package:oto/cli/commands/command_scheduler.dart';
import 'package:oto/cli/commands/scheduler/scheduler_manager.dart';
import 'package:oto/oto/core/system_runtime.dart';
import 'package:test/test.dart';

class StartExecutableCall {
  final String program;
  final List<String> args;
  final ProcessStartMode mode;
  StartExecutableCall(this.program, this.args, this.mode);
}

class FakeSystemRuntime implements SystemRuntime {
  final List<StringBuffer> runShellCalls = [];
  final List<StartExecutableCall> startExecutableCalls = [];
  final List<int> killedPids = [];
  final bool _isWindows;

  FakeSystemRuntime({bool isWindows = false}) : _isWindows = isWindows;

  @override
  bool get isWindows => _isWindows;
  @override
  bool get isMacOS => !_isWindows;
  @override
  bool get isLinux => !_isWindows;

  @override
  Map<String, String> get environment => {};

  @override
  Future<ProcessData> startShell(
    StringBuffer shell, {
    String? workspace,
    bool printStdout = true,
    bool printStderr = true,
    Converter<List<int>, String>? decoder,
    LogHandler? logHandler,
  }) async {
    throw UnimplementedError();
  }

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

  @override
  Future<ProcessResult> runExecutable(
    String exe,
    List<String> args, {
    String? workingDirectory,
    Encoding? stdoutEncoding,
    Encoding? stderrEncoding,
  }) async {
    throw UnimplementedError();
  }

  @override
  Future<Process> startExecutable(
    String program,
    List<String> args, {
    String? workingDirectory,
    ProcessStartMode mode = ProcessStartMode.normal,
  }) async {
    startExecutableCalls.add(StartExecutableCall(program, args, mode));
    return _DummyProcess();
  }

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

class _DummyProcess implements Process {
  @override
  Future<int> get exitCode => Future.value(0);
  @override
  bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => true;
  @override
  int get pid => 12345;
  @override
  Stream<List<int>> get stderr => const Stream.empty();
  @override
  IOSink get stdin => throw UnimplementedError();
  @override
  Stream<List<int>> get stdout => const Stream.empty();
}

class TestSchedulerManager extends SchedulerManager {
  TestSchedulerManager({
    super.runtime,
    super.processFinder,
    super.exitHandler,
  });

  @override
  String get exePath => '/bin/oto';
}

class FakeSchedulerManager extends SchedulerManager {
  bool showListCalled = false;
  bool terminateCalled = false;
  bool enableSchedulerCalled = false;
  String? enabledAlias;
  bool? enabledValue;
  bool registCalled = false;
  String? registeredFilePath;

  @override
  Future showList() async {
    showListCalled = true;
  }

  @override
  Future terminate() async {
    terminateCalled = true;
  }

  @override
  Future enableScheduler(String alias, bool enable) async {
    enableSchedulerCalled = true;
    enabledAlias = alias;
    enabledValue = enable;
  }

  @override
  Future regist(String filePath) async {
    registCalled = true;
    registeredFilePath = filePath;
  }
}

void main() {
  group('SchedulerManager Process Control via Injected Runtime', () {
    test('Windows starts detached executable', () async {
      final fakeRuntime = FakeSystemRuntime(isWindows: true);
      final manager = TestSchedulerManager(runtime: fakeRuntime);

      await manager.testStartProcess();

      expect(fakeRuntime.startExecutableCalls, hasLength(1));
      expect(fakeRuntime.startExecutableCalls.single.program,
          contains(Platform.resolvedExecutable));
      expect(fakeRuntime.startExecutableCalls.single.mode,
          ProcessStartMode.detached);
      expect(fakeRuntime.runShellCalls, isEmpty);
    });

    test('Unix starts background shell process via nohup', () async {
      final fakeRuntime = FakeSystemRuntime(isWindows: false);
      final manager = TestSchedulerManager(runtime: fakeRuntime);

      await manager.testStartProcess();

      expect(fakeRuntime.runShellCalls, hasLength(1));
      expect(fakeRuntime.runShellCalls.single.toString(), contains('nohup'));
      expect(fakeRuntime.runShellCalls.single.toString(),
          contains('scheduler -s &'));
      expect(fakeRuntime.startExecutableCalls, isEmpty);
    });

    test('terminate kills current scheduler process', () async {
      final fakeRuntime = FakeSystemRuntime();
      final manager = TestSchedulerManager(
        runtime: fakeRuntime,
        processFinder: (name) async => [],
      );

      var p = ProcessInfo();
      p.pid = 98765;
      p.name = 'scheduler -s';
      manager.currentScheduler = p;
      await manager.terminate();

      expect(fakeRuntime.killedPids, [98765]);
    });

    test('_checkProcess uses injected processFinder', () async {
      bool finderCalled = false;
      final manager = TestSchedulerManager(
        processFinder: (query) async {
          finderCalled = true;
          var p = ProcessInfo();
          p.pid = 11111;
          p.name = 'main.dart scheduler -s oto scheduler -s';
          return [p];
        },
      );

      await manager.testCheckProcess();

      expect(finderCalled, isTrue);
      expect(manager.currentScheduler?.pid, 11111);
    });
  });

  group('CommandScheduler routing', () {
    test(
        'command_scheduler accepts injected manager and exit handler for routing',
        () async {
      final fakeManager = FakeSchedulerManager();
      int? exitedCode;
      final cmd = CommandScheduler(
        scheduler: fakeManager,
        exitHandler: (code) {
          exitedCode = code;
        },
      );

      await cmd.execute(['-l']);
      expect(fakeManager.showListCalled, isTrue);

      await cmd.execute(['-t']);
      expect(fakeManager.terminateCalled, isTrue);

      await cmd.execute(['-r', 'dummy.yaml']);
      expect(fakeManager.registCalled, isTrue);
      expect(fakeManager.registeredFilePath, 'dummy.yaml');
      expect(exitedCode, 0);
    });
  });
}
