// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api

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

import 'package:dart_framework/platform/process.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/build/build_flutter.dart';
import 'package:oto/oto/commands/build/build_ios.dart';
import 'package:oto/oto/commands/build/build_msbuild.dart';
import 'package:oto/oto/commands/command_runtime.dart';
// ignore: implementation_imports
import 'package:dart_framework/utils/path.dart' show FileType;
import 'package:oto/oto/commands/ftp/download.dart';
import 'package:oto/oto/commands/ftp/ftp.dart';
import 'package:oto/oto/commands/git/git.dart';
import 'package:oto/oto/commands/git/git_hub.dart';
import 'package:oto/oto/commands/git/git_remote.dart';
import 'package:oto/oto/commands/infra/smb_authorize.dart';
import 'package:oto/oto/commands/process/process.dart';
import 'package:oto/oto/commands/shell/shell.dart';
import 'package:oto/oto/commands/util/execute_app.dart';
import 'package:oto/oto/commands/util/json.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:test/test.dart';

class _StartCall {
  final String shell;
  final String? workspace;
  final bool printStdout;
  final bool printStderr;
  final Converter<List<int>, String>? decoder;
  _StartCall(this.shell, this.workspace, this.printStdout, this.printStderr,
      this.decoder);
}

class _RunCall {
  final String shell;
  final String? workspace;
  _RunCall(this.shell, this.workspace);
}

class _DetachedCall {
  final String shell;
  final String workspace;
  _DetachedCall(this.shell, this.workspace);
}

class _ExecutableCall {
  final String exe;
  final List<String> args;
  final String? workspace;
  final bool? printStdout;
  final bool? printStderr;
  _ExecutableCall(
      this.exe, this.args, this.workspace, this.printStdout, this.printStderr);
}

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

  @override
  Future waitForExit() async {}

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

class FakeRuntime implements CommandRuntime {
  final List<_StartCall> startCalls = [];
  final List<_RunCall> runCalls = [];
  final List<_DetachedCall> detachedCalls = [];
  final List<_ExecutableCall> executableCalls = [];
  int exitCode;
  String stdoutText;
  String stderrText;
  final List<int> startExitCodes;

  FakeRuntime(
      {this.exitCode = 0,
      this.stdoutText = '',
      this.stderrText = '',
      this.startExitCodes = const []});

  @override
  Future<ProcessData> start(StringBuffer shell,
      {String? workspace,
      bool printStdout = true,
      bool printStderr = true,
      Converter<List<int>, String>? decoder,
      LogHandler? logHandler}) async {
    final startExitCode = startCalls.length < startExitCodes.length
        ? startExitCodes[startCalls.length]
        : exitCode;
    startCalls.add(_StartCall(
        shell.toString(), workspace, printStdout, printStderr, decoder));
    return FakeProcessData(
        exitCode: startExitCode,
        stdoutText: stdoutText,
        stderrText: stderrText);
  }

  @override
  Future<ProcessResult> run(StringBuffer shell,
      {String? workspace,
      bool printStdout = true,
      bool printStderr = true,
      Converter<List<int>, String>? decoder,
      LogHandler? logHandler}) async {
    runCalls.add(_RunCall(shell.toString(), workspace));
    return ProcessResult(0, exitCode, stdoutText, stderrText);
  }

  @override
  Future<ProcessResult> runExecutable(String exe, List<String> args,
      {String? workspace,
      bool? printStdout,
      bool? printStderr,
      LogHandler? logHandler}) async {
    executableCalls
        .add(_ExecutableCall(exe, args, workspace, printStdout, printStderr));
    return ProcessResult(0, exitCode, stdoutText, stderrText);
  }

  @override
  Future<void> startDetached(StringBuffer shell,
      {required String workspace}) async {
    detachedCalls.add(_DetachedCall(shell.toString(), workspace));
  }
}

void main() {
  setUp(() {
    Application.instance.context = ExecutionContext();
    Application.instance.property['workspace'] = '/tmp/oto_runtime_test';
  });

  DataCommand commandWith(Map<String, dynamic> param) {
    return DataCommand.fromJson({
      'command': 'Shell',
      'id': 'fake',
      'param': param,
    });
  }

  test('shell command delegates process start to runtime', () async {
    final fake = FakeRuntime();
    final shell = Shell()..runtime = fake;
    final command = commandWith({
      'commands': ['echo hello'],
    });

    await shell.execute(command);

    expect(fake.startCalls, hasLength(1));
    expect(fake.startCalls.single.shell, contains('echo hello'));
    expect(fake.startCalls.single.shell, contains('/tmp/oto_runtime_test'));
    expect(fake.runCalls, isEmpty);
    expect(fake.detachedCalls, isEmpty);
  });

  test('git command delegates command buffer to runtime', () async {
    final fake = FakeRuntime();
    final git = Git()..runtime = fake;
    final command = commandWith({
      'commands': ['status', 'fetch origin'],
    });

    await git.execute(command);

    expect(fake.startCalls, hasLength(1));
    final buffer = fake.startCalls.single.shell;
    expect(buffer, contains('git status'));
    expect(buffer, contains('git fetch origin'));
    expect(fake.detachedCalls, isEmpty);
  });

  test('process run delegates detached start to runtime', () async {
    final fake = FakeRuntime();
    final processRun = ProcessRun()..runtime = fake;
    final command = commandWith({
      'executable': 'my_server',
      'param': '--port 8080',
      'workspace': '/tmp/oto_runtime_test',
    });

    await processRun.execute(command);

    expect(fake.detachedCalls, hasLength(1));
    expect(fake.detachedCalls.single.workspace, '/tmp/oto_runtime_test');
    expect(fake.detachedCalls.single.shell, contains('my_server'));
    expect(fake.detachedCalls.single.shell, contains('--port 8080'));
    expect(fake.detachedCalls.single.shell, contains('dontKillMe'));
    expect(fake.startCalls, isEmpty);
  });

  test('runtime captures start options', () async {
    final fake = FakeRuntime();
    final shell = StringBuffer('echo hello');

    await fake.start(shell,
        workspace: '/tmp/test', printStdout: false, printStderr: false);

    expect(fake.startCalls, hasLength(1));
    expect(fake.startCalls.single.workspace, '/tmp/test');
    expect(fake.startCalls.single.printStdout, isFalse);
    expect(fake.startCalls.single.printStderr, isFalse);
  });

  test('runtime captures run workspace', () async {
    final fake = FakeRuntime();
    final shell = StringBuffer('echo test');

    await fake.run(shell, workspace: '/some/workspace');

    expect(fake.runCalls, hasLength(1));
    expect(fake.runCalls.single.workspace, '/some/workspace');
    expect(fake.runCalls.single.shell, contains('echo test'));
  });

  test('build_msbuild delegates start with decoder and printStderr=false',
      () async {
    final fake = FakeRuntime();
    final msbuild = BuildMSBuild()..runtime = fake;
    final command = commandWith({
      'projectFile': 'MyApp.sln',
      'type': 'Build',
      'config': 'Release',
    });

    await msbuild.execute(command);

    expect(fake.startCalls, hasLength(1));
    expect(fake.startCalls.single.printStderr, isFalse);
    expect(fake.startCalls.single.decoder, isNotNull);
    expect(fake.startCalls.single.shell, contains('msbuild MyApp.sln'));
  });

  test('github_pull_request_list delegates start with decoder', () async {
    final fake = FakeRuntime(stdoutText: '[]');
    final prList = GitHubPullRequestList()..runtime = fake;
    final command = commandWith({
      'keys': ['number', 'title'],
      'targetKey': 'title',
      'targetValue': 'my-pr',
      'setValue': '<@property.pr>',
    });

    await prList.execute(command);

    expect(fake.startCalls, hasLength(1));
    expect(fake.startCalls.single.decoder, isNotNull);
    expect(fake.startCalls.single.shell, contains('gh pr list'));
  });

  test('execute_app delegates run to runtime with workspace', () async {
    final fake = FakeRuntime();
    final app = ExecuteApp()..runtime = fake;
    final command = commandWith({
      'executable': 'MyApp.exe',
      'desktopServicePath': '/some/desktop',
    });

    await app.execute(command);

    expect(fake.runCalls, hasLength(1));
    expect(fake.runCalls.single.workspace, '/some/desktop');
    expect(fake.runCalls.single.shell, contains('MyApp.exe'));
  });

  test('BuildiOS.initializeFastlane delegates fastlane init run to runtime',
      () async {
    final fake = FakeRuntime();
    final tmp = await Directory.systemTemp.createTemp('oto_ios_test_');
    try {
      final apiKeyFile = File('${tmp.path}/key.json');
      await apiKeyFile.writeAsString(jsonEncode({
        'key_id': 'KID',
        'issuer_id': 'IID',
        'key': 'content',
      }));
      await Directory('${tmp.path}/fastlane').create();

      await BuildiOS.initializeFastlane(
          tmp.path, apiKeyFile.path, 'com.test.app',
          runtime: fake);

      expect(fake.runCalls, hasLength(1));
      expect(fake.runCalls.single.shell, contains('fastlane init'));
    } finally {
      await tmp.delete(recursive: true);
    }
  });

  test('BuildiOS.execute delegates start to runtime via ProcessData.exitCode',
      () async {
    final fake = FakeRuntime();
    final tmp = await Directory.systemTemp.createTemp('oto_ios_main_');
    try {
      await Directory('${tmp.path}/MyApp.xcodeproj').create();
      await File('${tmp.path}/MyApp.xcodeproj/project.pbxproj')
          .writeAsString('');

      final ios = BuildiOS()..runtime = fake;
      final command = commandWith({
        'xcodeProjectFilePath': 'MyApp.xcodeproj',
        'scheme': 'MyApp',
        'version': '1.0.0',
        'buildNumber': '100',
        'workspace': tmp.path,
      });

      await ios.execute(command);

      expect(fake.startCalls, hasLength(1));
      expect(fake.startCalls.single.shell, contains('xcodebuild'));
      expect(fake.startCalls.single.printStderr, isFalse);
    } finally {
      await tmp.delete(recursive: true);
    }
  });

  test('BuildiOS.execute shell includes derivedDataPath and macPassword',
      () async {
    final fake = FakeRuntime();
    final tmp = await Directory.systemTemp.createTemp('oto_ios_opts_');
    try {
      await Directory('${tmp.path}/MyApp.xcodeproj').create();
      await File('${tmp.path}/MyApp.xcodeproj/project.pbxproj')
          .writeAsString('');

      final ios = BuildiOS()..runtime = fake;
      final command = commandWith({
        'xcodeProjectFilePath': 'MyApp.xcodeproj',
        'scheme': 'MyApp',
        'version': '1.0.0',
        'buildNumber': '100',
        'workspace': tmp.path,
        'derivedDataPath': '/tmp/derived',
        'macPassword': 'testpwd',
      });

      await ios.execute(command);

      final shell = fake.startCalls.single.shell;
      expect(shell, contains('-derivedDataPath /tmp/derived'));
      expect(shell, contains('security unlock-keychain -ptestpwd'));
    } finally {
      await tmp.delete(recursive: true);
    }
  });

  test('BuildFlutter uses resolved workspace, flavor, and platform in shell',
      () async {
    final fake = FakeRuntime();
    final buildFlutter = BuildFlutter()..runtime = fake;

    Application.instance.property['workspace'] = '/custom/workspace';
    Application.instance.property['flavor'] = 'production';
    Application.instance.property['platform'] = 'apk';

    final command = DataCommand.fromJson({
      'command': 'BuildFlutter',
      'id': 'build-flutter-test',
      'param': {
        'workspace': '<!property.workspace>',
        'platform': '<!property.platform>',
        'flavor': '<!property.flavor>',
        'additional': '--release',
        'executeBuildRunner': false,
      },
    });

    await buildFlutter.execute(command);

    expect(fake.startCalls, hasLength(1));
    final shell = fake.startCalls.single.shell;
    expect(shell, contains('cd /custom/workspace'));
    expect(shell, contains('flutter build apk'));
    expect(shell, contains('--flavor production'));
    expect(shell, contains('--release'));
  });

  test('CodeSign includes --entitlements only when entitlementPath is set',
      () async {
    final fakeWith = FakeRuntime();
    final fakeWithout = FakeRuntime();

    final csWithEntitlement = CodeSign()..runtime = fakeWith;
    final csWithout = CodeSign()..runtime = fakeWithout;

    await csWithEntitlement.execute(commandWith({
      'certificationName': 'Apple Dev',
      'appPath': '/path/App.app',
      'entitlementPath': '/path/app.entitlements',
    }));
    await csWithout.execute(commandWith({
      'certificationName': 'Apple Dev',
      'appPath': '/path/App.app',
    }));

    expect(fakeWith.startCalls.single.shell, contains('--entitlements'));
    expect(
        fakeWithout.startCalls.single.shell, isNot(contains('--entitlements')));
  });

  test('runtime captures executable arguments', () async {
    final fake = FakeRuntime(exitCode: 0, stdoutText: 'done');

    final result = await fake.runExecutable('my_exe', ['--flag', 'value'],
        workspace: '/tmp', printStdout: false, printStderr: false);

    expect(fake.executableCalls, hasLength(1));
    expect(fake.executableCalls.single.exe, 'my_exe');
    expect(fake.executableCalls.single.args, containsAll(['--flag', 'value']));
    expect(fake.executableCalls.single.workspace, '/tmp');
    expect(fake.executableCalls.single.printStdout, isFalse);
    expect(fake.executableCalls.single.printStderr, isFalse);
    expect(result.exitCode, 0);
    expect(result.stdout, 'done');
  });

  test('sftp list parses ls -l stdout into FileType map', () async {
    const lsOutput = 'total 8\n'
        'drwxr-xr-x 2 user group 4096 Jan  1 00:00 subdir\n'
        '-rw-r--r-- 1 user group 1234 Jan  1 00:00 file.txt\n';
    final fake = FakeRuntime(stdoutText: lsOutput);
    final sftp =
        SFTP('test.host', user: 'user', password: 'pass', runtime: fake);

    final result = await sftp.list('/remote');

    expect(fake.executableCalls, hasLength(1));
    expect(fake.executableCalls.single.exe, 'sshpass');
    expect(fake.executableCalls.single.args,
        containsAll(['ssh', 'ls -l /remote']));
    expect(result['/remote/subdir'], FileType.directory);
    expect(result['/remote/file.txt'], FileType.file);
  });

  test('sftp list propagates non-zero exit code as exception', () async {
    final fake = FakeRuntime(exitCode: 1, stderrText: 'ls failed');
    final sftp =
        SFTP('test.host', user: 'user', password: 'pass', runtime: fake);

    await expectLater(
      sftp.list('/remote'),
      throwsA(predicate((e) => e.toString().contains('ls failed'))),
    );
  });

  test('sftp delete calls sshpass ssh rm -rf', () async {
    final fake = FakeRuntime();
    final sftp =
        SFTP('test.host', user: 'user', password: 'pass', runtime: fake);

    await sftp.delete(['/remote/old.txt']);

    expect(fake.executableCalls, hasLength(1));
    expect(fake.executableCalls.single.exe, 'sshpass');
    final args = fake.executableCalls.single.args;
    expect(args, containsAll(['ssh', 'rm -rf /remote/old.txt']));
  });

  test('smb_auth delegates cmd net use to runtime with domain user password',
      () async {
    final fake = FakeRuntime(exitCode: 0);
    final smb = SMBAuth()..runtime = fake;
    final command = commandWith({
      'domain': 'fileserver',
      'id': 'testuser',
      'password': 'secret',
    });

    await smb.execute(command);

    expect(fake.executableCalls, hasLength(1));
    expect(fake.executableCalls.single.exe, 'cmd');
    final args = fake.executableCalls.single.args;
    expect(
        args,
        containsAll(
            ['/C', 'net', 'use', r'\\fileserver', '/user:testuser', 'secret']));
  });

  test('download maps local relative path to remote path for sftp', () async {
    const lsOutput = 'total 4\n'
        '-rw-r--r-- 1 user group 1234 Jan  1 00:00 app.json\n';
    final fake = FakeRuntime(stdoutText: lsOutput);
    final download = Download()..runtime = fake;
    final command = DataCommand.fromJson({
      'command': 'Download',
      'id': 'download-config',
      'param': {
        'host': 'test.host',
        'sftp': true,
        'port': 22,
        'user': 'user',
        'password': 'pass',
        'map': {
          'config/app.json': '/remote/config/app.json',
        },
      },
    });

    await download.execute(command);

    expect(fake.executableCalls, hasLength(2));
    expect(fake.executableCalls.first.args,
        containsAll(['ssh', 'ls -l /remote/config']));

    final scpArgs = fake.executableCalls.last.args;
    expect(scpArgs, containsAll(['scp', '-P', '22']));
    expect(scpArgs, contains('user@test.host:/remote/config/app.json'));
    expect(scpArgs, contains('/tmp/oto_runtime_test/config/app.json'));
  });

  test('sftp download failure propagates non-zero exit code as exception',
      () async {
    final fake = FakeRuntime(exitCode: 1, stderrText: 'ssh: connect failed');
    final download = Download()..runtime = fake;
    final command = DataCommand.fromJson({
      'command': 'Download',
      'id': 'download-fail',
      'param': {
        'host': 'test.host',
        'sftp': true,
        'port': 22,
        'user': 'user',
        'password': 'pass',
        'map': {
          'config/app.json': '/remote/config/app.json',
        },
      },
    });

    await expectLater(
      download.execute(command),
      throwsA(predicate((e) => e.toString().contains('ssh: connect failed'))),
    );
  });

  test('git_pull propagates exit code 1 as failure', () async {
    final fake = FakeRuntime(exitCode: 1, stderrText: 'remote rejected');
    final gitPull = GitPull()..runtime = fake;
    final command = commandWith({'branch': 'main'});

    await expectLater(
      gitPull.execute(command),
      throwsA(predicate((e) => e.toString().contains('remote rejected'))),
    );
  });

  test('git_push propagates exit code 1 as failure', () async {
    final fake = FakeRuntime(exitCode: 1, stderrText: 'remote rejected');
    final gitPush = GitPush()..runtime = fake;
    final command = commandWith({'branch': 'main'});

    await expectLater(
      gitPush.execute(command),
      throwsA(predicate((e) => e.toString().contains('remote rejected'))),
    );
  });

  test('git_checkout uses fake ProcessData exitCode for existing branch',
      () async {
    final fake = FakeRuntime(startExitCodes: [128, 0]);
    final gitCheckout = GitCheckout()..runtime = fake;
    final command = commandWith({'branch': 'develop', 'executePull': false});

    await gitCheckout.execute(command);

    expect(fake.startCalls, hasLength(2));
    expect(
        fake.startCalls.first.shell, contains('git checkout origin/develop'));
    expect(fake.startCalls.first.shell, contains('git checkout -b develop'));
    expect(fake.startCalls.last.shell, contains('git checkout develop'));
    expect(fake.startCalls.last.shell, isNot(contains('git pull origin')));
  });

  test('json_reader array path stores value without debug print', () async {
    final printed = <String>[];
    final reader = JsonReader();
    final command = DataCommand.fromJson({
      'command': 'JsonReader',
      'id': 'json-reader',
      'param': {
        'json': {
          'items': [
            {'name': 'alpha'}
          ],
        },
        'pair': [
          {'from': 'items[0].name', 'setValue': '<@property.itemName>'}
        ],
      },
    });

    await runZoned(
      () => reader.execute(command),
      zoneSpecification: ZoneSpecification(
        print: (self, parent, zone, line) => printed.add(line),
      ),
    );

    expect(Application.instance.property['itemName'], 'alpha');
    expect(printed, isNot(contains('1')));
  });

  test('json_writer_file updates file without debug print', () async {
    final printed = <String>[];
    final tmp = await Directory.systemTemp.createTemp('oto_json_writer_');
    try {
      final file = File('${tmp.path}/data.json');
      await file.writeAsString(jsonEncode({
        'nested': {'old': 'value'}
      }));
      final writer = JsonWriterFile();
      final command = DataCommand.fromJson({
        'command': 'JsonWriterFile',
        'id': 'json-writer',
        'param': {
          'path': file.path,
          'pair': [
            {'from': 'nested.newValue', 'value': '42'}
          ],
        },
      });

      await runZoned(
        () => writer.execute(command),
        zoneSpecification: ZoneSpecification(
          print: (self, parent, zone, line) => printed.add(line),
        ),
      );

      final json = jsonDecode(await file.readAsString()) as Map;
      expect((json['nested'] as Map)['newValue'], '42');
      expect(printed, isEmpty);
    } finally {
      await tmp.delete(recursive: true);
    }
  });

  test('github_pull_request_create treats existing-pr message as success',
      () async {
    const existingUrl = 'https://github.com/org/repo/pull/123';
    final fake = FakeRuntime(
      exitCode: 1,
      stderrText:
          'GraphQL: a pull request for branch feature already exists:\n$existingUrl',
    );
    final prCreate = GitHubPullRequestCreate()..runtime = fake;
    final command = commandWith({
      'title': 'My PR',
      'body': 'Description',
      'branch': 'main',
      'setURL': '<@property.prUrl>',
      'setNumber': '<@property.prNumber>',
    });

    await prCreate.execute(command);

    expect(Application.instance.property['prUrl'], existingUrl);
    expect(Application.instance.property['prNumber'], '123');
  });

  test('github_pull_request_create propagates unrelated exit code 1', () async {
    final fake = FakeRuntime(exitCode: 1, stderrText: 'authentication failed');
    final prCreate = GitHubPullRequestCreate()..runtime = fake;
    final command = commandWith({
      'title': 'My PR',
      'body': 'Description',
      'branch': 'main',
      'setURL': '<@property.prUrl>',
      'setNumber': '<@property.prNumber>',
    });

    await expectLater(
      prCreate.execute(command),
      throwsA(predicate((e) => e.toString().contains('authentication failed'))),
    );
  });
}
