// 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/oto/application.dart';
import 'package:oto/oto/core/data_composer.dart';
import 'package:oto/oto/core/system_runtime.dart';
import 'package:test/test.dart';

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;
  _RunExecutableCall(this.exe, this.args, this.workingDirectory);
}

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

class FakeProcessData extends ProcessData {
  FakeProcessData({String stdoutText = ''}) : super(false, false) {
    exitCode = 0;
    this.stdout = StringBuffer(stdoutText);
  }

  @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<_StartExecutableCall> startExecutableCalls = [];
  final List<int> killedPids = [];

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

  final String runShellStdout;
  final String startShellStdout;

  FakeSystemRuntime({
    bool isWindows = false,
    bool isMacOS = false,
    bool isLinux = true,
    Map<String, String> environment = const {},
    this.runShellStdout = '',
    this.startShellStdout = '',
  })  : _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(stdoutText: startShellStdout);
  }

  @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, runShellStdout, '');
  }

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

  @override
  Future<Process> startExecutable(
    String program,
    List<String> args, {
    String? workingDirectory,
    ProcessStartMode mode = ProcessStartMode.normal,
  }) async {
    startExecutableCalls
        .add(_StartExecutableCall(program, args, workingDirectory, mode));
    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() {
  test('setUTF8 uses injected system runtime on Windows', () async {
    final fake = FakeSystemRuntime(isWindows: true);
    Application.instance.systemRuntime = fake;

    await Application.instance.setUTF8();

    expect(fake.startShellCalls, hasLength(1));
    expect(fake.startShellCalls.single.shell, contains('chcp 65001'));

    Application.instance.systemRuntime = const DefaultSystemRuntime();
  });

  test(
      'DataComposerJenkins composes linux env and BuildData from runtime stdout',
      () async {
    const jenkinsJson = '''
{
  "workspace": "/workspace",
  "jenkinsHome": "/jenkins",
  "jenkinsUrl": "http://jenkins/",
  "buildUrl": "http://jenkins/build/1/",
  "jobUrl": "http://jenkins/job/myjob/",
  "jobName": "myjob",
  "gitCommit": "abc123",
  "buildNumber": 42,
  "buildID": 42,
  "buildDisplayName": "#42",
  "gitBranch": "main",
  "buildTag": "myjob-42"
}
''';
    const buildYaml = 'property:\n  workspace: .\ncommands: []\n';
    final fake = FakeSystemRuntime(
      isLinux: true,
      runShellStdout: jenkinsJson,
      startShellStdout: buildYaml,
    );

    final composer = DataComposerJenkins(runtime: fake);
    await composer.compose(null, (msg, color) async {});

    expect(fake.runShellCalls, hasLength(1));
    expect(fake.startShellCalls, hasLength(1));
    expect(fake.startShellCalls.single.printStdout, isFalse);
    expect(composer.commonData, isNotNull);
    expect(composer.commonData!.workspace, '/workspace');
    expect(composer.buildYaml, buildYaml);
  });

  test(
      'JenkinsEnvironmentCollector collects linux env and build yaml from runtime',
      () async {
    const jenkinsJson = '''
{
  "workspace": "/workspace",
  "jenkinsHome": "/jenkins",
  "jenkinsUrl": "http://jenkins/",
  "buildUrl": "http://jenkins/build/1/",
  "jobUrl": "http://jenkins/job/myjob/",
  "jobName": "myjob",
  "gitCommit": "abc123",
  "buildNumber": 42,
  "buildID": 42,
  "buildDisplayName": "#42",
  "gitBranch": "main",
  "buildTag": "myjob-42"
}
''';
    const buildYaml = 'property:\n  workspace: .\ncommands: []\n';
    final fake = FakeSystemRuntime(
      isLinux: true,
      runShellStdout: jenkinsJson,
      startShellStdout: buildYaml,
    );

    final collector = JenkinsEnvironmentCollector(runtime: fake);
    final snapshot = await collector.collect((msg, color) async {});

    expect(snapshot, isNotNull);
    expect(snapshot!.variables['workspace'], '/workspace');
    expect(snapshot.buildYaml, buildYaml);
    expect(snapshot.rawVariablesJson, jenkinsJson);
    expect(fake.runShellCalls, hasLength(1));
    expect(fake.startShellCalls, hasLength(1));
    expect(fake.startShellCalls.single.printStdout, isFalse);
  });

  test('fake system runtime records process calls', () async {
    final fake = FakeSystemRuntime(
      isWindows: false,
      isMacOS: false,
      isLinux: true,
      environment: {'HOME': '/home/test'},
    );

    // startShell
    await fake.startShell(
      StringBuffer('echo hello'),
      workspace: '/tmp/test',
      printStdout: false,
      printStderr: false,
    );
    expect(fake.startShellCalls, hasLength(1));
    expect(fake.startShellCalls.single.shell, 'echo hello');
    expect(fake.startShellCalls.single.workspace, '/tmp/test');
    expect(fake.startShellCalls.single.printStdout, isFalse);
    expect(fake.startShellCalls.single.printStderr, isFalse);
    expect(fake.startShellCalls.single.decoder, isNull);

    // runShell
    await fake.runShell(StringBuffer('ls -la'), workspace: '/var/www');
    expect(fake.runShellCalls, hasLength(1));
    expect(fake.runShellCalls.single.shell, 'ls -la');
    expect(fake.runShellCalls.single.workspace, '/var/www');

    // runExecutable
    await fake.runExecutable('git', ['status', '--short'],
        workingDirectory: '/repo');
    expect(fake.runExecutableCalls, hasLength(1));
    expect(fake.runExecutableCalls.single.exe, 'git');
    expect(fake.runExecutableCalls.single.args, ['status', '--short']);
    expect(fake.runExecutableCalls.single.workingDirectory, '/repo');

    // startExecutable (detached)
    await expectLater(
      fake.startExecutable('my_daemon', ['--port', '8080'],
          workingDirectory: '/srv', mode: ProcessStartMode.detached),
      throwsUnsupportedError,
    );
    expect(fake.startExecutableCalls, hasLength(1));
    expect(fake.startExecutableCalls.single.program, 'my_daemon');
    expect(fake.startExecutableCalls.single.args, ['--port', '8080']);
    expect(fake.startExecutableCalls.single.workingDirectory, '/srv');
    expect(fake.startExecutableCalls.single.mode, ProcessStartMode.detached);

    // killPid
    fake.killPid(12345);
    fake.killPid(67890, ProcessSignal.sigkill);
    expect(fake.killedPids, [12345, 67890]);

    // OS flags
    expect(fake.isLinux, isTrue);
    expect(fake.isWindows, isFalse);
    expect(fake.isMacOS, isFalse);

    // environment
    expect(fake.environment['HOME'], '/home/test');
  });
}
