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

import 'package:flutter_test/flutter_test.dart';

import 'package:appsok/src/services/adb_service.dart';

void main() {
  group('AdbRuntimeConfig', () {
    test('prefers bundled adb when it exists', () {
      final tempDir = Directory.systemTemp.createTempSync(
        'appsok-adb-runtime-',
      );
      addTearDown(() => tempDir.deleteSync(recursive: true));

      final bundledAdb = File('${tempDir.path}/adb')..writeAsStringSync('');
      final runtime = AdbRuntimeConfig(
        bundledAdbPath: bundledAdb.path,
      ).resolve();

      expect(runtime.executablePath, bundledAdb.path);
      expect(runtime.source, AdbExecutableSource.bundled);
      expect(runtime.bundledExists, isTrue);
      expect(runtime.fallbackUsed, isFalse);
      expect(runtime.serverPort, AdbRuntimeConfig.defaultAppSokServerPort);
    });

    test('falls back to system adb when bundled adb is missing', () {
      final runtime = const AdbRuntimeConfig(
        bundledAdbPath: '/tmp/appsok-missing-adb',
      ).resolve();

      expect(runtime.executablePath, AdbRuntimeConfig.systemAdbPath);
      expect(runtime.source, AdbExecutableSource.system);
      expect(runtime.bundledExists, isFalse);
      expect(runtime.fallbackUsed, isTrue);
    });

    test('uses custom adb as an explicit override', () {
      final tempDir = Directory.systemTemp.createTempSync(
        'appsok-adb-runtime-',
      );
      addTearDown(() => tempDir.deleteSync(recursive: true));

      final bundledAdb = File('${tempDir.path}/adb')..writeAsStringSync('');
      final runtime = AdbRuntimeConfig(
        bundledAdbPath: bundledAdb.path,
        customAdbPath: '/custom/platform-tools/adb',
      ).resolve();

      expect(runtime.executablePath, '/custom/platform-tools/adb');
      expect(runtime.source, AdbExecutableSource.custom);
      expect(runtime.fallbackUsed, isFalse);
    });

    test('uses shared ADB server port when requested', () {
      final runtime = const AdbRuntimeConfig(
        serverMode: AdbServerMode.shared,
      ).resolve();

      expect(runtime.serverPort, AdbRuntimeConfig.sharedServerPort);
      expect(runtime.environment, {'ADB_SERVER_PORT': '5037'});
    });
  });

  group('AdbService.collectDiagnostics', () {
    test('captures adb version on success', () async {
      const versionOutput =
          'Android Debug Bridge version 1.0.41\n'
          'Version 35.0.2-12147458\n';
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/opt/appsok/adb'),
        processRunner: (executable, args, {environment}) async {
          expect(args, ['version']);
          return ProcessResult(42, 0, versionOutput, '');
        },
      );

      final diag = await service.collectDiagnostics();

      expect(diag.version, versionOutput.trim());
      expect(diag.versionError, isNull);
      expect(diag.runtime.executablePath, '/opt/appsok/adb');
    });

    test('records versionError on non-zero exit', () async {
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/opt/appsok/adb'),
        processRunner: (_, args, {environment}) async {
          return ProcessResult(42, 1, '', 'cannot run adb');
        },
      );

      final diag = await service.collectDiagnostics();

      expect(diag.version, isNull);
      expect(diag.versionError, 'cannot run adb');
    });

    test('records versionError with exit code when stderr is empty', () async {
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/opt/appsok/adb'),
        processRunner: (_, args, {environment}) async {
          return ProcessResult(42, 127, '', '');
        },
      );

      final diag = await service.collectDiagnostics();

      expect(diag.version, isNull);
      expect(diag.versionError, 'exit code 127');
    });

    test('records versionError when process throws', () async {
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/not/found/adb'),
        processRunner: (_, args, {environment}) async {
          throw const OSError('No such file or directory', 2);
        },
      );

      final diag = await service.collectDiagnostics();

      expect(diag.version, isNull);
      expect(diag.versionError, isNotNull);
    });

    test('computes stable sha256 for a temp executable', () async {
      final tempDir = Directory.systemTemp.createTempSync('appsok-diag-');
      addTearDown(() => tempDir.deleteSync(recursive: true));

      final fakeAdb = File('${tempDir.path}/adb')
        ..writeAsBytesSync([0x7f, 0x45, 0x4c, 0x46]);

      final service = AdbService(
        runtimeConfig: AdbRuntimeConfig(bundledAdbPath: fakeAdb.path),
        processRunner: (_, args, {environment}) async =>
            ProcessResult(0, 0, 'Android Debug Bridge version 1.0.0', ''),
      );

      final diag1 = await service.collectDiagnostics();
      final diag2 = await service.collectDiagnostics();

      expect(diag1.sha256, isNotNull);
      expect(diag1.sha256, diag2.sha256);
      expect(diag1.sha256, hasLength(64));
    });

    test('sha256 is null when executable does not exist', () async {
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/no/such/adb'),
        processRunner: (_, args, {environment}) async =>
            ProcessResult(0, 0, '', ''),
      );

      final diag = await service.collectDiagnostics();

      expect(diag.sha256, isNull);
    });
  });

  group('AdbService', () {
    test(
      'runs adb devices with the resolved executable and server port',
      () async {
        final calls =
            <({String executable, List<String> args, String? port})>[];
        final service = AdbService(
          runtimeConfig: const AdbRuntimeConfig(
            customAdbPath: '/opt/appsok/adb',
            serverPort: 15037,
          ),
          processRunner: (executable, args, {environment}) async {
            calls.add((
              executable: executable,
              args: args,
              port: environment?['ADB_SERVER_PORT'],
            ));
            return ProcessResult(
              42,
              0,
              'List of devices attached\n'
                  'emulator-5554 device product:emu64a '
                  'model:sdk_gphone64_arm64 device:emu64a\n',
              '',
            );
          },
        );

        final devices = await service.listDevices();

        expect(calls, hasLength(1));
        expect(calls.single.executable, '/opt/appsok/adb');
        expect(calls.single.args, ['devices', '-l']);
        expect(calls.single.port, '15037');
        expect(devices.single.serial, 'emulator-5554');
        expect(devices.single.state, 'device');
        expect(devices.single.model, 'sdk_gphone64_arm64');
      },
    );

    test(
      'parses device, unauthorized, and offline adb devices fixtures',
      () async {
        final service = AdbService(
          runtimeConfig: const AdbRuntimeConfig(
            customAdbPath: '/opt/appsok/adb',
          ),
          processRunner: (_, args, {environment}) async {
            expect(args, ['devices', '-l']);
            return ProcessResult(
              42,
              0,
              'List of devices attached\n'
                  'R5CT90A1B2C device product:dm3q '
                  'model:SM_S918N device:dm3q transport_id:1\n'
                  'ZX1G22K7Q9 unauthorized usb:336592896X '
                  'product:shiba model:Pixel_8 device:shiba\n'
                  'emulator-5554 offline product:sdk_gphone64_arm64 '
                  'model:sdk_gphone64_arm64 device:emu64a\n',
              '',
            );
          },
        );

        final devices = await service.listDevices();

        expect(devices, hasLength(3));
        expect(devices[0].serial, 'R5CT90A1B2C');
        expect(devices[0].state, 'device');
        expect(devices[0].isReady, isTrue);
        expect(devices[0].product, 'dm3q');
        expect(devices[0].model, 'SM_S918N');
        expect(devices[1].serial, 'ZX1G22K7Q9');
        expect(devices[1].state, 'unauthorized');
        expect(devices[1].needsAuthorization, isTrue);
        expect(devices[1].model, 'Pixel_8');
        expect(devices[2].serial, 'emulator-5554');
        expect(devices[2].state, 'offline');
        expect(devices[2].isOffline, isTrue);
        expect(devices[2].product, 'sdk_gphone64_arm64');
      },
    );

    test('startLogcat runs adb logcat for the requested serial', () async {
      final calls =
          <({String executable, List<String> args, String? port})>[];
      final fake = _FakeProcess();
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(
          customAdbPath: '/opt/appsok/adb',
          serverPort: 15037,
        ),
        processStarter: (executable, args, {environment}) async {
          calls.add((
            executable: executable,
            args: args,
            port: environment?['ADB_SERVER_PORT'],
          ));
          return fake;
        },
      );

      final session = await service.startLogcat(serial: 'R5CT90A1B2C');
      addTearDown(session.stop);

      expect(calls, hasLength(1));
      expect(calls.single.executable, '/opt/appsok/adb');
      expect(calls.single.args, ['-s', 'R5CT90A1B2C', 'logcat']);
      expect(calls.single.port, '15037');
    });

    test('logcat session merges stdout and stderr lines', () async {
      final fake = _FakeProcess();
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/opt/appsok/adb'),
        processStarter: (_, args, {environment}) async => fake,
      );

      final session = await service.startLogcat();
      final received = <String>[];
      final sub = session.lines.listen(received.add);

      fake.emitStdout('I/out: first\n');
      fake.emitStderr('E/err: oops\n');
      fake.emitStdout('I/out: second\n');
      await Future<void>.delayed(Duration.zero);

      expect(received, [
        'I/out: first',
        'E/err: oops',
        'I/out: second',
      ]);

      await sub.cancel();
      await session.stop();
    });

    test('logcat session stop kills the adb process', () async {
      final fake = _FakeProcess();
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/opt/appsok/adb'),
        processStarter: (_, args, {environment}) async => fake,
      );

      final session = await service.startLogcat();
      expect(fake.killCalled, isFalse);

      await session.stop();

      expect(fake.killCalled, isTrue);
    });

    test('logcat stream cancellation kills the adb process', () async {
      final fake = _FakeProcess();
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(customAdbPath: '/opt/appsok/adb'),
        processStarter: (_, args, {environment}) async => fake,
      );

      final sub = service.logcat().listen((_) {});
      await Future<void>.delayed(Duration.zero);
      expect(fake.killCalled, isFalse);

      await sub.cancel();

      expect(fake.killCalled, isTrue);
    });

    test('runs adb install with the shared server port', () async {
      final calls = <({List<String> args, String? port})>[];
      final service = AdbService(
        runtimeConfig: const AdbRuntimeConfig(
          customAdbPath: '/opt/appsok/adb',
          serverMode: AdbServerMode.shared,
        ),
        processRunner: (_, args, {environment}) async {
          calls.add((args: args, port: environment?['ADB_SERVER_PORT']));
          return ProcessResult(42, 0, 'Success', '');
        },
      );

      final result = await service.installApk(
        serial: 'R5CT90A1B2C',
        apkPath: '/tmp/app.apk',
      );

      expect(result.succeeded, isTrue);
      expect(calls.single.args, [
        '-s',
        'R5CT90A1B2C',
        'install',
        '-r',
        '/tmp/app.apk',
      ]);
      expect(calls.single.port, '5037');
    });
  });
}

/// Minimal in-memory [Process] stand-in for logcat tests. Only the members the
/// logcat session touches (stdout, stderr, kill, exitCode) are implemented.
class _FakeProcess implements Process {
  final _stdout = StreamController<List<int>>();
  final _stderr = StreamController<List<int>>();
  final _exitCode = Completer<int>();

  bool killCalled = false;

  void emitStdout(String text) => _stdout.add(utf8.encode(text));
  void emitStderr(String text) => _stderr.add(utf8.encode(text));

  @override
  Stream<List<int>> get stdout => _stdout.stream;

  @override
  Stream<List<int>> get stderr => _stderr.stream;

  @override
  Future<int> get exitCode => _exitCode.future;

  @override
  bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
    killCalled = true;
    if (!_exitCode.isCompleted) _exitCode.complete(-15);
    _stdout.close();
    _stderr.close();
    return true;
  }

  @override
  int get pid => 4242;

  @override
  IOSink get stdin => throw UnimplementedError();
}
