import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
import 'package:web_shell_core/core_app.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

const MethodChannel _platformChannel = SystemChannels.platform;
const _permissionChannel = MethodChannel(
  'flutter.baseflow.com/permissions/methods',
);

const _testEnvironment = ShellEnvironment(
  appName: '测试应用',
  appKey: 'test_app',
  accentColor: Color(0xFF3ED37B),
  backgroundColor: Color(0xFFFFFFFF),
  textColor: Color(0xFF1F2937),
  mutedTextColor: Color(0xFF6B7280),
  initialUrl: 'example.com/login',
);

Future<Uri> _startJsonServer(
  Future<String> Function(HttpRequest request) responder,
) async {
  final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
  server.listen((request) async {
    try {
      final body = await responder(request);
      request.response.statusCode = HttpStatus.ok;
      request.response.headers.set(
        HttpHeaders.contentTypeHeader,
        'application/json; charset=utf-8',
      );
      request.response.write(body);
    } catch (error) {
      request.response.statusCode =
          error is int ? error : HttpStatus.internalServerError;
    } finally {
      await request.response.close();
    }
  });
  addTearDown(server.close);
  return Uri.parse('http://127.0.0.1:${server.port}/config.json');
}

Future<T> _runWithRealHttpClient<T>(Future<T> Function() body) {
  return HttpOverrides.runWithHttpOverrides(body, _RealHttpOverrides());
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late _FakeWebViewPlatform fakeWebViewPlatform;
  late _FakeImagePickerPlatform fakeImagePickerPlatform;
  late _FakeUrlLauncherPlatform fakeUrlLauncherPlatform;
  late List<String> platformCalls;
  late List<MethodCall> platformMethodCalls;
  late Map<String, String> assetContents;
  late bool cameraPermissionGranted;

  setUp(() {
    SharedPreferences.setMockInitialValues(<String, Object>{});
    platformCalls = <String>[];
    platformMethodCalls = <MethodCall>[];
    assetContents = <String, String>{};
    cameraPermissionGranted = true;
    fakeWebViewPlatform = _FakeWebViewPlatform();
    fakeImagePickerPlatform = _FakeImagePickerPlatform();
    fakeUrlLauncherPlatform = _FakeUrlLauncherPlatform();

    WebViewPlatform.instance = fakeWebViewPlatform;
    ImagePickerPlatform.instance = fakeImagePickerPlatform;
    UrlLauncherPlatform.instance = fakeUrlLauncherPlatform;
    shellCoreTestHooks.initializeEnvironment(_testEnvironment);

    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(_platformChannel, (call) async {
          platformCalls.add(call.method);
          platformMethodCalls.add(call);
          return null;
        });

    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMessageHandler('flutter/assets', (message) async {
          if (message == null) {
            return null;
          }
          final assetKey = utf8.decode(message.buffer.asUint8List());
          final content = assetContents[assetKey];
          if (content == null) {
            return null;
          }
          final bytes = Uint8List.fromList(utf8.encode(content));
          return ByteData.view(bytes.buffer);
        });

    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(_permissionChannel, (call) async {
          if (call.method != 'requestPermissions') {
            return null;
          }

          final permissions = (call.arguments as List<Object?>).cast<int>();
          final statusValue = cameraPermissionGranted ? 1 : 0;
          return <int, int>{
            for (final permission in permissions) permission: statusValue,
          };
        });
  });

  tearDown(() {
    debugDefaultTargetPlatformOverride = null;
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(_platformChannel, null);
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(_permissionChannel, null);
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMessageHandler('flutter/assets', null);
  });

  group('运行时与平台', () {
    test('只支持 Android 内嵌 WebView', () {
      expect(
        shellCoreTestHooks.supportsEmbeddedWebView(
          platform: TargetPlatform.android,
        ),
        isTrue,
      );
      expect(
        shellCoreTestHooks.supportsEmbeddedWebView(
          platform: TargetPlatform.iOS,
        ),
        isFalse,
      );
      expect(
        shellCoreTestHooks.supportsEmbeddedWebView(
          platform: TargetPlatform.macOS,
        ),
        isFalse,
      );
      expect(shellCoreTestHooks.supportsEmbeddedWebView(isWeb: true), isFalse);
    });

    test('enterImmersiveMode 会透传到 SystemChrome', () async {
      await shellCoreTestHooks.enterImmersiveMode();

      expect(platformCalls, contains('SystemChrome.setEnabledSystemUIMode'));
    });

    testWidgets('runShellApp 会设置系统 UI 并启动应用', (tester) async {
      try {
        debugDefaultTargetPlatformOverride = TargetPlatform.linux;

        await runShellApp(_testEnvironment);
        await tester.pump();

        expect(
          platformCalls,
          contains('SystemChrome.setPreferredOrientations'),
        );
        expect(platformCalls, contains('SystemChrome.setEnabledSystemUIMode'));
        expect(platformCalls, contains('SystemChrome.setSystemUIOverlayStyle'));
        expect(find.byType(ShellApp), findsOneWidget);
        expect(find.byType(UnsupportedPlatformPage), findsOneWidget);
      } finally {
        debugDefaultTargetPlatformOverride = null;
      }
    });

    testWidgets('runShellApp 会应用自定义首屏方向', (tester) async {
      try {
        debugDefaultTargetPlatformOverride = TargetPlatform.linux;

        await runShellApp(
          const ShellEnvironment(
            appName: '横屏应用',
            appKey: 'landscape_app',
            accentColor: Color(0xFF3ED37B),
            backgroundColor: Color(0xFFFFFFFF),
            textColor: Color(0xFF1F2937),
            mutedTextColor: Color(0xFF6B7280),
            initialUrl: 'https://example.com/start',
            preferredOrientations: <DeviceOrientation>[
              DeviceOrientation.landscapeLeft,
            ],
          ),
        );
        await tester.pump();

        expect(
          shellCoreTestHooks.preferredOrientations,
          <DeviceOrientation>[DeviceOrientation.landscapeLeft],
        );
        final orientationCall = platformMethodCalls.lastWhere(
          (call) => call.method == 'SystemChrome.setPreferredOrientations',
        );
        expect(orientationCall.arguments.toString(), contains('landscapeLeft'));
      } finally {
        debugDefaultTargetPlatformOverride = null;
      }
    });

    testWidgets('runShellApp 会读取本地默认启动配置文件', (tester) async {
      try {
        debugDefaultTargetPlatformOverride = TargetPlatform.linux;
        assetContents['assets/config/test_bootstrap.json'] =
            '{"initialUrl":"https://asset.example.com/start","preferredOrientations":["landscapeRight"]}';

        await runShellApp(
          const ShellEnvironment(
            appName: '本地配置应用',
            appKey: 'asset_app',
            accentColor: Color(0xFF3ED37B),
            backgroundColor: Color(0xFFFFFFFF),
            textColor: Color(0xFF1F2937),
            mutedTextColor: Color(0xFF6B7280),
            bootstrapConfigAsset: 'assets/config/test_bootstrap.json',
          ),
        );
        await tester.pump();

        expect(
          shellCoreTestHooks.initialUrl,
          'https://asset.example.com/start',
        );
        expect(
          shellCoreTestHooks.preferredOrientations,
          <DeviceOrientation>[DeviceOrientation.landscapeRight],
        );
      } finally {
        debugDefaultTargetPlatformOverride = null;
      }
    });

    testWidgets('ShellApp 在非 Android 下展示兜底页', (tester) async {
      try {
        debugDefaultTargetPlatformOverride = TargetPlatform.windows;

        await tester.pumpWidget(const ShellApp());

        expect(find.byType(UnsupportedPlatformPage), findsOneWidget);
        expect(find.text('当前平台不支持内嵌 WebView'), findsOneWidget);
      } finally {
        debugDefaultTargetPlatformOverride = null;
      }
    });

    testWidgets('ShellApp 在 Android 下展示 WebView 容器', (tester) async {
      try {
        debugDefaultTargetPlatformOverride = TargetPlatform.android;

        await tester.pumpWidget(const ShellApp());
        await tester.pump();

        expect(
          find.byKey(const ValueKey<String>('fake-webview')),
          findsOneWidget,
        );
        expect(fakeWebViewPlatform.createdControllers, isNotEmpty);
      } finally {
        debugDefaultTargetPlatformOverride = null;
      }
    });
  });

  group('URL 与兼容策略', () {
    test('初始化 URL 时会补全 https 协议', () {
      expect(shellCoreTestHooks.initialUrl, 'https://example.com/login');
      expect(
        shellCoreTestHooks.initialUri,
        Uri.parse('https://example.com/login'),
      );
    });

    test('初始化 URL 会处理空值、协议相对地址和非法地址', () {
      shellCoreTestHooks.initializeEnvironment(
        const ShellEnvironment(
          appName: '空地址',
          appKey: 'empty',
          accentColor: Color(0xFF000000),
          backgroundColor: Color(0xFFFFFFFF),
          textColor: Color(0xFF000000),
          mutedTextColor: Color(0xFF888888),
        ),
      );
      expect(
        shellCoreTestHooks.initialUrl,
        'http://xszy.lzzneng.com/login.html',
      );

      shellCoreTestHooks.initializeEnvironment(
        const ShellEnvironment(
          appName: '协议相对',
          appKey: 'protocol_relative',
          accentColor: Color(0xFF000000),
          backgroundColor: Color(0xFFFFFFFF),
          textColor: Color(0xFF000000),
          mutedTextColor: Color(0xFF888888),
          initialUrl: '//example.org/index.html',
        ),
      );
      expect(shellCoreTestHooks.initialUrl, 'https://example.org/index.html');

      shellCoreTestHooks.initializeEnvironment(
        const ShellEnvironment(
          appName: '非法地址',
          appKey: 'invalid',
          accentColor: Color(0xFF000000),
          backgroundColor: Color(0xFFFFFFFF),
          textColor: Color(0xFF000000),
          mutedTextColor: Color(0xFF888888),
          initialUrl: '/relative-path',
        ),
      );
      expect(
        shellCoreTestHooks.initialUrl,
        'http://xszy.lzzneng.com/login.html',
      );

      shellCoreTestHooks.initializeEnvironment(_testEnvironment);
    });

    test('网络 URL 与 URI 归一化逻辑正确', () {
      expect(shellCoreTestHooks.isNetworkUrl('https://example.com'), isTrue);
      expect(shellCoreTestHooks.isNetworkUrl('http://example.com'), isTrue);
      expect(shellCoreTestHooks.isNetworkUrl('file:///tmp/demo.txt'), isFalse);
      expect(shellCoreTestHooks.defaultPortForScheme('https'), 443);
      expect(shellCoreTestHooks.defaultPortForScheme('custom'), -1);
      expect(shellCoreTestHooks.normalizeComparablePath(''), '/');
      expect(shellCoreTestHooks.normalizeComparablePath('/demo/'), '/demo');
      expect(
        shellCoreTestHooks.normalizeComparableUri(
          Uri.parse('https://Example.com/demo/?a=1'),
        ),
        'https://example.com:443/demo?a=1',
      );
    });

    test('AndroidWebViewInfo 能从 map 构建并输出摘要', () {
      final info = AndroidWebViewInfo.fromMap(<Object?, Object?>{
        'sdkInt': '28',
        'manufacturer': 'Yuanxuan',
        'brand': 'YX',
        'model': 'F136A',
        'webViewDataDirectorySuffix': 'shell',
        'webViewPackageName': 'com.google.android.webview',
        'webViewVersionName': '109.0.0',
        'webViewLongVersionCode': '123',
      });

      expect(info.sdkInt, 28);
      expect(info.manufacturer, 'Yuanxuan');
      expect(info.brand, 'YX');
      expect(info.model, 'F136A');
      expect(info.webViewDataDirectorySuffix, 'shell');
      expect(info.webViewPackageName, 'com.google.android.webview');
      expect(info.webViewVersionName, '109.0.0');
      expect(info.webViewLongVersionCode, 123);
      expect(info.webViewMajorVersion, 109);
      expect(info.isLegacyWebView, isTrue);
      expect(info.isF136A, isTrue);
      expect(info.summary, contains('sdk=28'));
      expect(info.summary, contains('device=Yuanxuan F136A'));
      expect(info.summary, contains('webViewVersion=109.0.0'));
    });

    test('AndroidCompatibilityPlan 会根据设备信息产出恢复策略', () {
      final info = AndroidWebViewInfo(
        sdkInt: 28,
        manufacturer: 'YX',
        brand: 'YX',
        model: 'F136A',
        webViewVersionName: '109.0.1',
      );

      final fallbackPlan = AndroidCompatibilityPlan.fallback();
      final plan = AndroidCompatibilityPlan.fromInfo(info);
      final nullInfoPlan = AndroidCompatibilityPlan.fromInfo(null);

      expect(fallbackPlan.renderModes, isNotEmpty);
      expect(nullInfoPlan.renderModes, isNotEmpty);
      expect(plan.renderModes.first, AndroidRenderMode.hybrid);
      expect(AndroidRenderMode.texture.displayName, '标准模式');
      expect(AndroidRenderMode.hybrid.displayName, '兼容模式');
      expect(plan.useWideViewPort, isTrue);
      expect(plan.suggestWebViewUpdate, isTrue);
      expect(plan.prefersAggressiveRecovery, isTrue);
      expect(plan.describe(), contains('modes='));
      expect(plan.describe(), contains('aggressiveRecovery=true'));
    });

    test('WebView 主版本解析兼容空值和正常值', () {
      expect(shellCoreTestHooks.parseWebViewMajorVersion(null), isNull);
      expect(shellCoreTestHooks.parseWebViewMajorVersion(''), isNull);
      expect(
        shellCoreTestHooks.parseWebViewMajorVersion('122.0.6261.86'),
        122,
      );
    });
  });

  group('Bridge 与错误映射', () {
    test('bridge 脚本包含主要能力与兼容层', () {
      final script = shellCoreTestHooks.buildAppShellBridgeScript();

      expect(script, contains('window.AppShell'));
      expect(script, contains('pickImage'));
      expect(script, contains('captureImage'));
      expect(script, contains('pickFile'));
      expect(script, contains('requestPermissions'));
      expect(script, contains(shellCoreTestHooks.legacyCameraCompatScript));
    });

    test('bridge 注入、响应与 alert 都会调用 JS', () async {
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      final controller = WebViewController.fromPlatform(platformController);

      await shellCoreTestHooks.injectAppShellBridge(
        controller,
        'https://a.com',
      );
      await shellCoreTestHooks.sendBridgeResponse(
        controller,
        requestId: 'req-1',
        success: true,
        data: <String, Object?>{'ok': true},
      );
      await shellCoreTestHooks.showWebAlert(controller, 'hello');

      expect(platformController.javaScriptCalls, hasLength(3));
      expect(
        platformController.javaScriptCalls.first,
        contains('window.AppShell'),
      );
      expect(
        platformController.javaScriptCalls[1],
        contains('window.__appShellReceiveResponse'),
      );
      expect(platformController.javaScriptCalls.last, contains('window.alert'));
    });

    test('bridge 注入和 alert 的异常会被吞掉', () async {
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      )..throwOnRunJavaScript = true;
      final controller = WebViewController.fromPlatform(platformController);

      await shellCoreTestHooks.injectAppShellBridge(
        controller,
        'https://a.com',
      );
      await shellCoreTestHooks.sendBridgeResponse(
        controller,
        requestId: 'req-2',
        success: false,
        error: 'boom',
      );
      await shellCoreTestHooks.showWebAlert(controller, 'hello');

      expect(platformController.javaScriptCalls, isEmpty);
    });

    test('错误标题与文案映射正确', () {
      const timeoutError = WebResourceError(
        errorCode: -1,
        description: 'timeout',
        errorType: WebResourceErrorType.timeout,
      );
      const connectError = WebResourceError(
        errorCode: -2,
        description: 'connect',
        errorType: WebResourceErrorType.connect,
      );
      const sslError = WebResourceError(
        errorCode: -3,
        description: 'ssl',
        errorType: WebResourceErrorType.failedSslHandshake,
      );
      const customError = WebResourceError(
        errorCode: -4,
        description: 'custom error',
        errorType: WebResourceErrorType.unknown,
      );

      expect(shellCoreTestHooks.friendlyErrorTitle(timeoutError), '请求超时');
      expect(shellCoreTestHooks.friendlyErrorTitle(connectError), '网络连接失败');
      expect(shellCoreTestHooks.friendlyErrorTitle(sslError), '安全连接失败');
      expect(shellCoreTestHooks.friendlyErrorTitle(customError), '页面加载失败');

      expect(
        shellCoreTestHooks.friendlyErrorMessage(timeoutError),
        '当前网络较慢，请稍后重新加载。',
      );
      expect(
        shellCoreTestHooks.friendlyErrorMessage(connectError),
        '没有成功连接到服务器，请检查网络后重试。',
      );
      expect(
        shellCoreTestHooks.friendlyErrorMessage(sslError),
        '当前站点证书校验失败，请稍后再试。',
      );
      expect(
        shellCoreTestHooks.friendlyErrorMessage(customError),
        'custom error',
      );
    });
  });

  group('媒体与权限工具', () {
    test('序列化 XFile 与 PlatformFile', () async {
      final tempDirectory = await Directory.systemTemp.createTemp('web-shell');
      final file = File('${tempDirectory.path}/demo.txt')
        ..writeAsStringSync('hello');
      final binaryFile = File('${tempDirectory.path}/demo.bin')
        ..writeAsBytesSync(<int>[4, 5, 6]);
      addTearDown(() async {
        if (tempDirectory.existsSync()) {
          await tempDirectory.delete(recursive: true);
        }
      });

      final xFile = XFile(file.path, name: 'demo.txt');
      final serializedXFiles = await shellCoreTestHooks.serializeXFiles(
        <XFile>[xFile],
        responseType: 'dataUrl',
      );
      final serializedPlatformFiles = await shellCoreTestHooks
          .serializePlatformFiles(
            <PlatformFile>[
              PlatformFile(
                name: 'demo.apk',
                size: 3,
                bytes: Uint8List.fromList(<int>[1, 2, 3]),
              ),
              PlatformFile(
                name: 'demo.bin',
                path: binaryFile.path,
                size: 3,
              ),
            ],
            responseType: 'dataUrl',
          );
      final uriStrings = shellCoreTestHooks.xFilesToUriStrings(<XFile>[xFile]);

      expect(serializedXFiles.single['name'], 'demo.txt');
      expect(serializedXFiles.single['mimeType'], 'text/plain');
      expect(serializedXFiles.single['dataUrl'], startsWith('data:text/plain'));
      expect(
        serializedPlatformFiles.first['mimeType'],
        'application/vnd.android.package-archive',
      );
      expect(
        serializedPlatformFiles.first['dataUrl'],
        startsWith('data:application/vnd.android.package-archive'),
      );
      expect(
        serializedPlatformFiles.last['dataUrl'],
        startsWith('data:application/octet-stream'),
      );
      expect(uriStrings.single, Uri.file(file.path).toString());
    });

    test('文件 accept、MIME 与布尔转换正确', () {
      expect(shellCoreTestHooks.acceptsImages(<String>[' image/* ']), isTrue);
      expect(
        shellCoreTestHooks.acceptsOnlyImages(<String>['.png', '.jpg']),
        isTrue,
      );
      expect(
        shellCoreTestHooks.acceptsOnlyImages(<String>['.png', '.pdf']),
        isFalse,
      );
      expect(shellCoreTestHooks.isImageAcceptType('.heic'), isTrue);
      expect(shellCoreTestHooks.guessMimeType('photo.jpeg'), 'image/jpeg');
      expect(shellCoreTestHooks.guessMimeType('photo.webp'), 'image/webp');
      expect(shellCoreTestHooks.guessMimeType('photo.gif'), 'image/gif');
      expect(shellCoreTestHooks.guessMimeType('photo.bmp'), 'image/bmp');
      expect(shellCoreTestHooks.guessMimeType('photo.heic'), 'image/heic');
      expect(shellCoreTestHooks.guessMimeType('photo.heif'), 'image/heif');
      expect(shellCoreTestHooks.guessMimeType('book.pdf'), 'application/pdf');
      expect(
        shellCoreTestHooks.guessMimeType('demo.apk'),
        'application/vnd.android.package-archive',
      );
      expect(
        shellCoreTestHooks.guessMimeType('file.bin'),
        'application/octet-stream',
      );
      expect(shellCoreTestHooks.boolValue(true), isTrue);
      expect(shellCoreTestHooks.boolValue('true'), isTrue);
      expect(shellCoreTestHooks.boolValue(1), isTrue);
      expect(
        shellCoreTestHooks.boolValue('other', defaultValue: true),
        isFalse,
      );
      expect(shellCoreTestHooks.boolValue(0), isFalse);
    });

    test('相机权限通过时会调用 image picker', () async {
      final tempDirectory = await Directory.systemTemp.createTemp('camera-ok');
      final file = File('${tempDirectory.path}/camera.jpg')
        ..writeAsBytesSync(<int>[1, 2, 3]);
      addTearDown(() async {
        if (tempDirectory.existsSync()) {
          await tempDirectory.delete(recursive: true);
        }
      });
      fakeImagePickerPlatform.nextImage = XFile(file.path, name: 'camera.jpg');

      final result = await shellCoreTestHooks.pickCameraImage(ImagePicker());

      expect(result, isNotNull);
      expect(result!.path, file.path);
      expect(fakeImagePickerPlatform.lastSource, ImageSource.camera);
    });

    test('相机权限拒绝时返回 null 并可提示 Web alert', () async {
      cameraPermissionGranted = false;
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      final controller = WebViewController.fromPlatform(platformController);

      final result = await shellCoreTestHooks.pickCameraImage(
        ImagePicker(),
        showPermissionAlert: true,
        controller: controller,
      );

      expect(result, isNull);
      expect(
        platformController.javaScriptCalls.single,
        contains('请先在系统设置中允许相机权限'),
      );
    });

    test('相机打开失败时返回 null 并可提示 Web alert', () async {
      fakeImagePickerPlatform.shouldThrow = true;
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      final controller = WebViewController.fromPlatform(platformController);

      final result = await shellCoreTestHooks.pickCameraImage(
        ImagePicker(),
        showPermissionAlert: true,
        controller: controller,
      );

      expect(result, isNull);
      expect(platformController.javaScriptCalls.single, contains('无法打开系统相机'));
    });

    test('权限类型映射正确', () {
      expect(shellCoreTestHooks.permissionForType('camera'), Permission.camera);
      expect(
        shellCoreTestHooks.permissionForType('microphone'),
        Permission.microphone,
      );
      expect(
        shellCoreTestHooks.permissionForType('audio'),
        Permission.microphone,
      );
      expect(
        shellCoreTestHooks.permissionForType('location'),
        Permission.location,
      );
      expect(shellCoreTestHooks.permissionForType('photos'), Permission.photos);
      expect(shellCoreTestHooks.permissionForType('videos'), Permission.videos);
      expect(
        shellCoreTestHooks.permissionForType('storage'),
        Permission.storage,
      );
      expect(shellCoreTestHooks.permissionForType('unknown'), isNull);
    });
  });

  group('导航与组件', () {
    test('导航 scheme 判断和外部打开行为正确', () async {
      expect(
        shellCoreTestHooks.shouldStayInWebView(
          Uri.parse('https://example.com'),
        ),
        isTrue,
      );
      expect(
        shellCoreTestHooks.shouldStayInWebView(
          Uri.parse('javascript:alert(1)'),
        ),
        isTrue,
      );
      expect(
        shellCoreTestHooks.shouldStayInWebView(Uri.parse('tel:10086')),
        isFalse,
      );

      fakeUrlLauncherPlatform.shouldSucceed = true;
      expect(
        await shellCoreTestHooks.openExternalUri(Uri.parse('tel:10086')),
        isTrue,
      );
      expect(fakeUrlLauncherPlatform.lastUrl, 'tel:10086');

      fakeUrlLauncherPlatform.shouldThrow = true;
      expect(
        await shellCoreTestHooks.openExternalUri(Uri.parse('weixin://demo')),
        isFalse,
      );
    });

    testWidgets('UnsupportedPlatformPage 展示 Android-only 文案', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(home: UnsupportedPlatformPage()),
      );

      expect(find.text('当前平台不支持内嵌 WebView'), findsOneWidget);
      expect(find.textContaining('当前项目仅支持 Android 平板运行。'), findsOneWidget);
      expect(find.textContaining('https://example.com/login'), findsOneWidget);
    });

    testWidgets('ErrorOverlay 点击重试会触发回调', (tester) async {
      var retried = false;

      await tester.pumpWidget(
        MaterialApp(
          home: ErrorOverlay(
            title: '错误标题',
            message: '错误信息',
            currentUrl: 'https://example.com',
            onRetry: () async {
              retried = true;
            },
          ),
        ),
      );

      expect(find.text('错误标题'), findsOneWidget);
      expect(find.text('错误信息'), findsOneWidget);
      expect(find.text('https://example.com'), findsOneWidget);
      await tester.tap(find.text('重新加载'));
      await tester.pump();
      expect(retried, isTrue);
    });

    testWidgets('LaunchOverlay 与 TopProgressBar 能正确渲染进度', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Column(
            children: <Widget>[
              Expanded(
                child: LaunchOverlay(progress: 80, hasMeasuredProgress: true),
              ),
              TopProgressBar(progress: 50, hasMeasuredProgress: true),
            ],
          ),
        ),
      );

      // LaunchOverlay 显示品牌名 + 不定态转圈
      expect(find.text('测试应用'), findsOneWidget);
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
      // TopProgressBar 仍使用线性进度条
      expect(find.byType(LinearProgressIndicator), findsOneWidget);

      final indicator = tester.widget<LinearProgressIndicator>(
        find.byType(LinearProgressIndicator),
      );
      expect(indicator.value, 0.5);
    });

    testWidgets('LaunchOverlay 始终显示不定态 CircularProgressIndicator', (
      tester,
    ) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: LaunchOverlay(progress: 0, hasMeasuredProgress: false),
        ),
      );

      // 不定态：CircularProgressIndicator 无 value
      final indicator = tester.widget<CircularProgressIndicator>(
        find.byType(CircularProgressIndicator),
      );
      expect(indicator.value, isNull);
    });

    testWidgets('TopProgressBar 未量测进度时显示不确定进度', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: TopProgressBar(progress: 0, hasMeasuredProgress: false),
          ),
        ),
      );

      final indicator = tester.widget<LinearProgressIndicator>(
        find.byType(LinearProgressIndicator),
      );
      expect(indicator.value, isNull);
    });
  });

  group('导航 scheme 边界场景', () {
    test('data/blob/about/file 协议留在 WebView 中', () {
      expect(
        shellCoreTestHooks.shouldStayInWebView(
          Uri.parse('data:text/html,<h1>hello</h1>'),
        ),
        isTrue,
      );
      expect(
        shellCoreTestHooks.shouldStayInWebView(Uri.parse('about:blank')),
        isTrue,
      );
      expect(
        shellCoreTestHooks.shouldStayInWebView(
          Uri.parse('blob:https://example.com/uuid'),
        ),
        isTrue,
      );
      expect(
        shellCoreTestHooks.shouldStayInWebView(
          Uri.parse('file:///tmp/test.html'),
        ),
        isTrue,
      );
    });

    test('intent/alipay/weixin/sms 等自定义 scheme 走外部应用', () {
      for (final scheme in <String>[
        'intent://demo',
        'alipays://pay',
        'weixin://dl/business',
        'sms:10086',
        'mailto:test@example.com',
      ]) {
        expect(
          shellCoreTestHooks.shouldStayInWebView(Uri.parse(scheme)),
          isFalse,
          reason: 'scheme=$scheme should leave WebView',
        );
      }
    });

    test('openExternalUri 成功返回 true', () async {
      fakeUrlLauncherPlatform
        ..shouldSucceed = true
        ..shouldThrow = false;

      final result = await shellCoreTestHooks.openExternalUri(
        Uri.parse('alipays://pay?orderId=123'),
      );

      expect(result, isTrue);
      expect(fakeUrlLauncherPlatform.lastUrl, 'alipays://pay?orderId=123');
    });
  });

  group('序列化 responseType 边界', () {
    test('base64 模式包含 base64 但不包含 dataUrl', () async {
      final tempDirectory = await Directory.systemTemp.createTemp('ser-b64');
      final file = File('${tempDirectory.path}/data.txt')
        ..writeAsStringSync('abc');
      addTearDown(() async {
        if (tempDirectory.existsSync()) {
          await tempDirectory.delete(recursive: true);
        }
      });

      final result = await shellCoreTestHooks.serializeXFiles(
        <XFile>[XFile(file.path, name: 'data.txt')],
        responseType: 'base64',
      );

      expect(result.single['base64'], isNotNull);
      expect(result.single.containsKey('dataUrl'), isFalse);
    });

    test('uri 模式不包含 base64 也不包含 dataUrl', () async {
      final tempDirectory = await Directory.systemTemp.createTemp('ser-uri');
      final file = File('${tempDirectory.path}/data.txt')
        ..writeAsStringSync('abc');
      addTearDown(() async {
        if (tempDirectory.existsSync()) {
          await tempDirectory.delete(recursive: true);
        }
      });

      final result = await shellCoreTestHooks.serializeXFiles(
        <XFile>[XFile(file.path, name: 'data.txt')],
        responseType: 'uri',
      );

      expect(result.single.containsKey('base64'), isFalse);
      expect(result.single.containsKey('dataUrl'), isFalse);
      expect(result.single['uri'], contains('data.txt'));
    });

    test('空文件列表返回空数组', () async {
      final xResult = await shellCoreTestHooks.serializeXFiles(
        <XFile>[],
        responseType: 'dataUrl',
      );
      final pfResult = await shellCoreTestHooks.serializePlatformFiles(
        <PlatformFile>[],
        responseType: 'base64',
      );

      expect(xResult, isEmpty);
      expect(pfResult, isEmpty);
    });

    test('PlatformFile 无 bytes 也无 path 时 base64 为 null', () async {
      final result = await shellCoreTestHooks.serializePlatformFiles(
        <PlatformFile>[PlatformFile(name: 'ghost.dat', size: 0)],
        responseType: 'base64',
      );

      expect(result.single['base64'], isNull);
      expect(result.single['uri'], isNull);
    });
  });

  group('AndroidCompatibilityPlan 扩展场景', () {
    test('现代设备（高 SDK + 新 WebView）优先使用 texture', () {
      final info = AndroidWebViewInfo(
        sdkInt: 34,
        manufacturer: 'Samsung',
        brand: 'samsung',
        model: 'SM-X810',
        webViewVersionName: '122.0.6261.86',
      );

      final plan = AndroidCompatibilityPlan.fromInfo(info);

      expect(info.isLegacyWebView, isFalse);
      expect(info.isF136A, isFalse);
      expect(plan.renderModes.first, AndroidRenderMode.texture);
      expect(plan.suggestWebViewUpdate, isFalse);
      expect(plan.prefersAggressiveRecovery, isFalse);
    });

    test('低 SDK（≤28）但非 F136A 也会使用 hybrid 优先', () {
      final info = AndroidWebViewInfo(
        sdkInt: 26,
        manufacturer: 'Huawei',
        brand: 'HUAWEI',
        model: 'MediaPad T3',
        webViewVersionName: '120.0.0',
      );

      final plan = AndroidCompatibilityPlan.fromInfo(info);

      expect(plan.renderModes.first, AndroidRenderMode.hybrid);
      expect(plan.suggestWebViewUpdate, isTrue);
      expect(plan.prefersAggressiveRecovery, isTrue);
    });

    test('AndroidWebViewInfo 字段缺失时使用安全默认值', () {
      final info = AndroidWebViewInfo.fromMap(<Object?, Object?>{});

      expect(info.sdkInt, 0);
      expect(info.manufacturer, '');
      expect(info.brand, '');
      expect(info.model, '');
      expect(info.webViewDataDirectorySuffix, isNull);
      expect(info.webViewPackageName, isNull);
      expect(info.webViewVersionName, isNull);
      expect(info.webViewLongVersionCode, isNull);
      expect(info.webViewMajorVersion, isNull);
    });

    test('AndroidRenderMode 枚举属性正确', () {
      expect(AndroidRenderMode.texture.usesHybridComposition, isFalse);
      expect(AndroidRenderMode.hybrid.usesHybridComposition, isTrue);
      expect(AndroidRenderMode.texture.logName, 'texture-layer');
      expect(AndroidRenderMode.hybrid.logName, 'hybrid-composition');
    });
  });

  group('错误映射扩展场景', () {
    test('hostLookup 和 io 错误映射为网络连接失败', () {
      const hostLookupError = WebResourceError(
        errorCode: -10,
        description: 'dns lookup failed',
        errorType: WebResourceErrorType.hostLookup,
      );
      const ioError = WebResourceError(
        errorCode: -11,
        description: 'io interrupted',
        errorType: WebResourceErrorType.io,
      );

      expect(shellCoreTestHooks.friendlyErrorTitle(hostLookupError), '网络连接失败');
      expect(shellCoreTestHooks.friendlyErrorTitle(ioError), '网络连接失败');
      expect(
        shellCoreTestHooks.friendlyErrorMessage(hostLookupError),
        '没有成功连接到服务器，请检查网络后重试。',
      );
      expect(
        shellCoreTestHooks.friendlyErrorMessage(ioError),
        '没有成功连接到服务器，请检查网络后重试。',
      );
    });

    test('空 description 使用默认文案', () {
      const emptyDescError = WebResourceError(
        errorCode: -99,
        description: '   ',
        errorType: WebResourceErrorType.unknown,
      );

      expect(
        shellCoreTestHooks.friendlyErrorMessage(emptyDescError),
        '请稍后重新加载页面。',
      );
    });
  });

  group('Accept 类型边界场景', () {
    test('空 acceptTypes 不接受图片也不仅接受图片', () {
      expect(shellCoreTestHooks.acceptsImages(<String>[]), isFalse);
      expect(shellCoreTestHooks.acceptsOnlyImages(<String>[]), isFalse);
    });

    test('仅空白字符的 acceptTypes 视为空', () {
      expect(shellCoreTestHooks.acceptsImages(<String>['  ', '']), isFalse);
      expect(shellCoreTestHooks.acceptsOnlyImages(<String>['  ', '']), isFalse);
    });

    test('image/* 通配符被识别为图片', () {
      expect(shellCoreTestHooks.isImageAcceptType('image/*'), isTrue);
      expect(shellCoreTestHooks.isImageAcceptType('image/png'), isTrue);
      expect(shellCoreTestHooks.isImageAcceptType('image/svg+xml'), isTrue);
    });

    test('非图片文件扩展名正确返回 false', () {
      expect(shellCoreTestHooks.isImageAcceptType('.pdf'), isFalse);
      expect(shellCoreTestHooks.isImageAcceptType('.apk'), isFalse);
      expect(shellCoreTestHooks.isImageAcceptType('application/pdf'), isFalse);
    });

    test('混合类型列表正确判断', () {
      expect(
        shellCoreTestHooks.acceptsImages(<String>['.pdf', '.png']),
        isTrue,
      );
      expect(
        shellCoreTestHooks.acceptsOnlyImages(<String>['.pdf', '.png']),
        isFalse,
      );
      expect(
        shellCoreTestHooks.acceptsImages(<String>['image/*', '.jpg']),
        isTrue,
      );
      expect(
        shellCoreTestHooks.acceptsOnlyImages(<String>['image/*', '.jpg']),
        isTrue,
      );
    });
  });

  group('boolValue 边界场景', () {
    test('null 与未知类型使用 defaultValue', () {
      expect(shellCoreTestHooks.boolValue(null), isFalse);
      expect(shellCoreTestHooks.boolValue(null, defaultValue: true), isTrue);
      expect(shellCoreTestHooks.boolValue(3.14), isFalse);
      expect(shellCoreTestHooks.boolValue(<int>[]), isFalse);
    });

    test('字符串 false/TRUE 正确解析', () {
      expect(shellCoreTestHooks.boolValue('false'), isFalse);
      expect(shellCoreTestHooks.boolValue('TRUE'), isTrue);
      expect(shellCoreTestHooks.boolValue('False'), isFalse);
    });

    test('int 非零值为 true', () {
      expect(shellCoreTestHooks.boolValue(-1), isTrue);
      expect(shellCoreTestHooks.boolValue(42), isTrue);
    });
  });

  group('MIME 类型扩展场景', () {
    test('所有已支持扩展名都能正确猜测', () {
      expect(shellCoreTestHooks.guessMimeType('FILE.PNG'), 'image/png');
      expect(shellCoreTestHooks.guessMimeType('a.JPG'), 'image/jpeg');
      expect(shellCoreTestHooks.guessMimeType('a.JPEG'), 'image/jpeg');
      expect(shellCoreTestHooks.guessMimeType('a.PDF'), 'application/pdf');
      expect(shellCoreTestHooks.guessMimeType('a.TXT'), 'text/plain');
    });

    test('无扩展名或未知扩展名返回 octet-stream', () {
      expect(
        shellCoreTestHooks.guessMimeType('binary'),
        'application/octet-stream',
      );
      expect(
        shellCoreTestHooks.guessMimeType('data.xyz'),
        'application/octet-stream',
      );
    });
  });

  group('URL 归一化边界场景', () {
    test('isNetworkUrl 对 null/空/非法值返回 false', () {
      expect(shellCoreTestHooks.isNetworkUrl(null), isFalse);
      expect(shellCoreTestHooks.isNetworkUrl(''), isFalse);
      expect(shellCoreTestHooks.isNetworkUrl('not a url'), isFalse);
    });

    test('路径归一化处理各种格式', () {
      expect(shellCoreTestHooks.normalizeComparablePath('/a/b/'), '/a/b');
      expect(shellCoreTestHooks.normalizeComparablePath('/a/b'), '/a/b');
      expect(shellCoreTestHooks.normalizeComparablePath('/'), '/');
      expect(shellCoreTestHooks.normalizeComparablePath('///'), '//');
    });

    test('默认端口映射覆盖 http/https/未知', () {
      expect(shellCoreTestHooks.defaultPortForScheme('http'), 80);
      expect(shellCoreTestHooks.defaultPortForScheme('https'), 443);
      expect(shellCoreTestHooks.defaultPortForScheme('ftp'), -1);
      expect(shellCoreTestHooks.defaultPortForScheme(''), -1);
    });

    test('URI 归一化保留查询参数并忽略大小写', () {
      expect(
        shellCoreTestHooks.normalizeComparableUri(
          Uri.parse('HTTP://EXAMPLE.COM:80/Path/?q=1'),
        ),
        'http://example.com:80/Path?q=1',
      );
    });
  });

  group('ShellEnvironment 配置验证', () {
    test('字段正确传递到全局环境', () {
      expect(_testEnvironment.appName, '测试应用');
      expect(_testEnvironment.appKey, 'test_app');
      expect(_testEnvironment.accentColor, const Color(0xFF3ED37B));
      expect(_testEnvironment.backgroundColor, const Color(0xFFFFFFFF));
      expect(_testEnvironment.textColor, const Color(0xFF1F2937));
      expect(_testEnvironment.mutedTextColor, const Color(0xFF6B7280));
      expect(_testEnvironment.initialUrl, 'example.com/login');
      expect(_testEnvironment.bootstrapConfigAsset, isNull);
      expect(_testEnvironment.preferredOrientations, isNull);
    });

    test('启动配置能解析首页地址与方向', () {
      final config = shellCoreTestHooks.parseBootstrapConfigString(
        '{"data":{"initialUrl":"https://example.com/bootstrap","preferredOrientations":["portraitUp","landscapeLeft"]}}',
      );

      expect(config, isNotNull);
      expect(config!.initialUrl, 'https://example.com/bootstrap');
      expect(
        config.preferredOrientations,
        <DeviceOrientation>[
          DeviceOrientation.portraitUp,
          DeviceOrientation.landscapeLeft,
        ],
      );
    });

    test('initialUrl 可选字段缺省时使用默认地址', () {
      const noUrlEnv = ShellEnvironment(
        appName: '无URL',
        appKey: 'no_url',
        accentColor: Color(0xFF000000),
        backgroundColor: Color(0xFFFFFFFF),
        textColor: Color(0xFF000000),
        mutedTextColor: Color(0xFF888888),
      );

      expect(noUrlEnv.initialUrl, isNull);
      shellCoreTestHooks.initializeEnvironment(noUrlEnv);
      expect(
        shellCoreTestHooks.initialUrl,
        'http://xszy.lzzneng.com/login.html',
      );

      // 恢复测试环境
      shellCoreTestHooks.initializeEnvironment(_testEnvironment);
    });

    test('https 地址直接使用不做修改', () {
      shellCoreTestHooks.initializeEnvironment(
        const ShellEnvironment(
          appName: 'HTTPS',
          appKey: 'https',
          accentColor: Color(0xFF000000),
          backgroundColor: Color(0xFFFFFFFF),
          textColor: Color(0xFF000000),
          mutedTextColor: Color(0xFF888888),
          initialUrl: 'https://app.example.com/start',
        ),
      );
      expect(
        shellCoreTestHooks.initialUrl,
        'https://app.example.com/start',
      );
      expect(
        shellCoreTestHooks.initialUri,
        Uri.parse('https://app.example.com/start'),
      );

      // 恢复测试环境
      shellCoreTestHooks.initializeEnvironment(_testEnvironment);
    });

    test('ShellEnvironment.copyWith 会按需覆盖字段', () {
      final copied = _testEnvironment.copyWith(
        appName: '复制应用',
        appKey: 'copied',
        initialUrl: 'https://copy.example.com',
        bootstrapConfigAsset: 'assets/config/bootstrap.json',
        preferredOrientations: <DeviceOrientation>[
          DeviceOrientation.landscapeRight,
        ],
      );

      expect(copied.appName, '复制应用');
      expect(copied.appKey, 'copied');
      expect(copied.accentColor, _testEnvironment.accentColor);
      expect(copied.backgroundColor, _testEnvironment.backgroundColor);
      expect(copied.textColor, _testEnvironment.textColor);
      expect(copied.mutedTextColor, _testEnvironment.mutedTextColor);
      expect(copied.initialUrl, 'https://copy.example.com');
      expect(copied.bootstrapConfigAsset, 'assets/config/bootstrap.json');
      expect(
        copied.preferredOrientations,
        <DeviceOrientation>[DeviceOrientation.landscapeRight],
      );
    });

    test('启动配置支持 bootstrapConfigUrl 和 upgradeConfigUrl 字段', () {
      final config = shellCoreTestHooks.parseBootstrapConfigString(
        '{"initialUrl":"https://example.com/home","bootstrapConfigUrl":" https://example.com/bootstrap.json ","upgradeConfigUrl":" https://example.com/upgrade.json ","orientations":["DeviceOrientation.portraitUp","invalid","portraitUp",null,"landscapeLeft"]}',
      );

      expect(config, isNotNull);
      expect(config!.bootstrapConfigUrl, ' https://example.com/bootstrap.json ');
      expect(config.upgradeConfigUrl, ' https://example.com/upgrade.json ');
      expect(
        config.preferredOrientations,
        <DeviceOrientation>[
          DeviceOrientation.portraitUp,
          DeviceOrientation.landscapeLeft,
        ],
      );
    });

    test('启动配置解析支持空数组、非法对象与非法 JSON', () {
      expect(
        shellCoreTestHooks.parseBootstrapConfigString(
          '{"preferredOrientations":[]}',
        )!.preferredOrientations,
        <DeviceOrientation>[],
      );
      expect(
        shellCoreTestHooks.parseBootstrapConfigString(
          '{"preferredOrientations":"portraitUp"}',
        )!.preferredOrientations,
        isNull,
      );
      expect(shellCoreTestHooks.parseBootstrapConfigString('['), isNull);
    });

    test('读取不存在的本地启动配置返回 null', () async {
      final config = await shellCoreTestHooks.loadDefaultBootstrapConfig(
        'assets/config/missing.json',
      );

      expect(config, isNull);
    });

    test('读取启动配置缓存成功、空值和非法 JSON 都符合预期', () async {
      SharedPreferences.setMockInitialValues(<String, Object>{
        'webshell_bootstrap_config_cache':
            '{"initialUrl":"https://cache.example.com"}',
      });
      final cached = await shellCoreTestHooks.loadCachedBootstrapConfig();
      expect(cached?.initialUrl, 'https://cache.example.com');

      SharedPreferences.setMockInitialValues(<String, Object>{});
      expect(await shellCoreTestHooks.loadCachedBootstrapConfig(), isNull);

      SharedPreferences.setMockInitialValues(<String, Object>{
        'webshell_bootstrap_config_cache': '{',
      });
      expect(await shellCoreTestHooks.loadCachedBootstrapConfig(), isNull);
    });

    test('获取启动配置支持成功、非法地址、非 200 和异常回退缓存', () async {
      SharedPreferences.setMockInitialValues(<String, Object>{
        'webshell_bootstrap_config_cache':
            '{"initialUrl":"https://cache.example.com/fallback"}',
      });
      final unavailableSocket = await ServerSocket.bind(
        InternetAddress.loopbackIPv4,
        0,
      );
      final unavailablePort = unavailableSocket.port;
      await unavailableSocket.close();

      final successUri = await _startJsonServer(
        (_) async => '{"data":{"initialUrl":"https://remote.example.com","upgradeConfigUrl":"https://remote.example.com/upgrade.json"}}',
      );
      final success = await _runWithRealHttpClient(() => shellCoreTestHooks.fetchBootstrapConfig(successUri.toString()));
      expect(success?.initialUrl, 'https://remote.example.com');
      expect(
        success?.upgradeConfigUrl,
        'https://remote.example.com/upgrade.json',
      );

      final prefs = await SharedPreferences.getInstance();
      expect(
        prefs.getString('webshell_bootstrap_config_cache'),
        contains('remote.example.com'),
      );

      final invalid = await shellCoreTestHooks.fetchBootstrapConfig('%%%');
      expect(invalid?.initialUrl, 'https://remote.example.com');

      final invalidUri = await shellCoreTestHooks.fetchBootstrapConfig('://');
      expect(invalidUri?.initialUrl, 'https://remote.example.com');

      final notFoundUri = await _startJsonServer((_) async => throw 404);
      final notFound = await _runWithRealHttpClient(() => shellCoreTestHooks.fetchBootstrapConfig(notFoundUri.toString()));
      expect(notFound?.initialUrl, 'https://remote.example.com');

      final failed = await _runWithRealHttpClient(
        () => shellCoreTestHooks.fetchBootstrapConfig(
          'http://127.0.0.1:$unavailablePort/config.json',
        ),
      );
      expect(failed?.initialUrl, 'https://remote.example.com');
    });

    test('读取启动配置缓存异常时返回 null', () async {
      final originalStore = SharedPreferencesStorePlatform.instance;
      SharedPreferencesStorePlatform.instance = _ThrowingPreferencesStore();
      addTearDown(() => SharedPreferencesStorePlatform.instance = originalStore);

      expect(await shellCoreTestHooks.loadCachedBootstrapConfig(), isNull);
    });
  });

  group('Bridge JS 协议格式', () {
    test('脚本包含版本号和 app-shell-ready 事件', () {
      final script = shellCoreTestHooks.buildAppShellBridgeScript();

      expect(script, contains('__nativeShellVersion'));
      expect(script, contains("'1.0.0'"));
      expect(script, contains('app-shell-ready'));
      expect(script, contains('isNativeShell: true'));
    });

    test('脚本包含通道守卫和去重逻辑', () {
      final script = shellCoreTestHooks.buildAppShellBridgeScript();

      expect(script, contains('AppShellBridge'));
      expect(script, contains("typeof channel.postMessage !== 'function'"));
      expect(script, contains('window.AppShell'));
    });

    test('脚本暴露全部 12 个 Action', () {
      final script = shellCoreTestHooks.buildAppShellBridgeScript();

      for (final action in <String>[
        'pickImage',
        'captureImage',
        'pickFile',
        'openExternal',
        'requestPermissions',
        'reloadPage',
        'goBack',
        'closeApp',
        'getDeviceInfo',
        'getNetworkStatus',
        'showToast',
        'setStatusBar',
      ]) {
        expect(script, contains(action), reason: 'Missing action: $action');
      }
    });

    test('bridge 响应包含 requestId/success/data 字段', () async {
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      final controller = WebViewController.fromPlatform(platformController);

      await shellCoreTestHooks.sendBridgeResponse(
        controller,
        requestId: 'test-123',
        success: true,
        data: <String, Object?>{'count': 5},
      );

      expect(platformController.javaScriptCalls, hasLength(1));
      final js = platformController.javaScriptCalls.first;
      expect(js, contains('__appShellReceiveResponse'));
      expect(js, contains('"requestId":"test-123"'));
      expect(js, contains('"success":true'));
      expect(js, contains('"count":5'));
    });

    test('bridge 失败响应包含 error 字段', () async {
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      final controller = WebViewController.fromPlatform(platformController);

      await shellCoreTestHooks.sendBridgeResponse(
        controller,
        requestId: 'err-456',
        success: false,
        error: 'Something went wrong',
      );

      final js = platformController.javaScriptCalls.first;
      expect(js, contains('"success":false'));
      expect(js, contains('"error":"Something went wrong"'));
    });
  });

  group('AndroidCompatibilityPlan describe 格式', () {
    test('fallback 策略描述包含必要字段', () {
      final fallbackPlan = AndroidCompatibilityPlan.fallback();
      final description = fallbackPlan.describe();

      expect(description, contains('modes='));
      expect(description, contains('wideViewport=true'));
      expect(description, contains('aggressiveRecovery=true'));
    });

    test('现代设备策略不建议激进恢复', () {
      final info = AndroidWebViewInfo(
        sdkInt: 34,
        manufacturer: 'Google',
        brand: 'google',
        model: 'Pixel Tablet',
        webViewVersionName: '120.0.0',
      );
      final plan = AndroidCompatibilityPlan.fromInfo(info);
      final description = plan.describe();

      expect(description, contains('texture-layer -> hybrid-composition'));
      expect(description, contains('aggressiveRecovery=false'));
    });

    test('F136A 设备策略建议更新 WebView', () {
      final info = AndroidWebViewInfo(
        sdkInt: 30,
        manufacturer: 'Yuanxuan',
        brand: 'YX',
        model: 'F136A',
        webViewVersionName: '109.0.1',
      );
      final plan = AndroidCompatibilityPlan.fromInfo(info);

      expect(plan.suggestWebViewUpdate, isTrue);
      expect(plan.prefersAggressiveRecovery, isTrue);
      expect(plan.renderModes.first, AndroidRenderMode.hybrid);
    });

    test('summary 对空制造商不重复空格', () {
      final info = AndroidWebViewInfo(
        sdkInt: 30,
        manufacturer: '',
        brand: '',
        model: 'TestDevice',
      );

      expect(info.summary, contains('device=TestDevice'));
      expect(info.summary, isNot(contains('device= ')));
    });

    test('summary 对空型号不显示 device 字段', () {
      final info = AndroidWebViewInfo(
        sdkInt: 30,
        manufacturer: '',
        brand: '',
        model: '',
      );

      expect(info.summary, isNot(contains('device=')));
      expect(info.summary, startsWith('sdk=30'));
    });
  });

  group('相机功能边界场景', () {
    test('权限拒绝且不带 controller 时静默返回 null', () async {
      cameraPermissionGranted = false;

      final result = await shellCoreTestHooks.pickCameraImage(ImagePicker());

      expect(result, isNull);
    });

    test('权限拒绝且 showPermissionAlert=false 时不弹窗', () async {
      cameraPermissionGranted = false;
      final platformController = _FakePlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      final controller = WebViewController.fromPlatform(platformController);

      final result = await shellCoreTestHooks.pickCameraImage(
        ImagePicker(),
        controller: controller,
      );

      expect(result, isNull);
      expect(platformController.javaScriptCalls, isEmpty);
    });

    test('相机返回 null 时结果也为 null', () async {
      fakeImagePickerPlatform.nextImage = null;

      final result = await shellCoreTestHooks.pickCameraImage(ImagePicker());

      expect(result, isNull);
    });
  });

  group('多文件序列化', () {
    test('多个 XFile 正确序列化为数组', () async {
      final tempDirectory = await Directory.systemTemp.createTemp('multi');
      final file1 = File('${tempDirectory.path}/a.png')
        ..writeAsBytesSync(<int>[1, 2]);
      final file2 = File('${tempDirectory.path}/b.jpg')
        ..writeAsBytesSync(<int>[3, 4]);
      addTearDown(() async {
        if (tempDirectory.existsSync()) {
          await tempDirectory.delete(recursive: true);
        }
      });

      final result = await shellCoreTestHooks.serializeXFiles(
        <XFile>[
          XFile(file1.path, name: 'a.png'),
          XFile(file2.path, name: 'b.jpg'),
        ],
        responseType: 'dataUrl',
      );

      expect(result, hasLength(2));
      expect(result[0]['name'], 'a.png');
      expect(result[0]['mimeType'], 'image/png');
      expect(result[1]['name'], 'b.jpg');
      expect(result[1]['mimeType'], 'image/jpeg');
    });

    test('空文件路径列表转换为空 URI 列表', () {
      final uris = shellCoreTestHooks.xFilesToUriStrings(<XFile>[]);
      expect(uris, isEmpty);
    });

    test('XFile 路径正确转换为 file:// URI', () {
      final uris = shellCoreTestHooks.xFilesToUriStrings(<XFile>[
        XFile('/tmp/a.txt'),
        XFile('/tmp/b.png'),
      ]);

      expect(uris, hasLength(2));
      expect(uris[0], 'file:///tmp/a.txt');
      expect(uris[1], 'file:///tmp/b.png');
    });
  });

  group('看门狗常量验证', () {
    test('旧相机兼容脚本包含 legacy 标记和重试逻辑', () {
      final script = shellCoreTestHooks.legacyCameraCompatScript;

      expect(script, contains('__appShellLegacyCameraCompatInstalled'));
      expect(script, contains('installLegacyCameraCompat'));
      expect(script, contains('wrappedOpenCamera'));
      expect(script, contains('capturePhoto'));
      expect(script, contains('setInterval'));
    });

    test('WebView 主版本解析能正确处理各种版本格式', () {
      expect(shellCoreTestHooks.parseWebViewMajorVersion('83'), 83);
      expect(shellCoreTestHooks.parseWebViewMajorVersion('122.0.6261'), 122);
      expect(
        shellCoreTestHooks.parseWebViewMajorVersion('not-a-version'),
        isNull,
      );
    });
  });

  group('Phase 2: 新增 Bridge Action', () {
    const deviceInfoChannel = MethodChannel(
      'dev.fluttercommunity.plus/device_info',
    );
    const connectivityChannel = MethodChannel(
      'dev.fluttercommunity.plus/connectivity',
    );

    setUp(() {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(deviceInfoChannel, (call) async {
            if (call.method == 'getDeviceInfo') {
              // 真实 platform channel 返回 Map<Object?, Object?>
              return <Object?, Object?>{
                'brand': 'TestBrand',
                'model': 'TestModel',
                'manufacturer': 'TestMfr',
                'product': 'TestProduct',
                'display': 'TestDisplay',
                'isPhysicalDevice': true,
                'version': <Object?, Object?>{
                  'sdkInt': 34,
                  'release': '14',
                  'baseOS': '',
                  'codename': 'REL',
                  'incremental': '12345',
                  'previewSdkInt': 0,
                  'securityPatch': '2024-01-01',
                },
                'board': 'test',
                'bootloader': 'test',
                'device': 'test',
                'fingerprint': 'test',
                'hardware': 'test',
                'host': 'test',
                'id': 'test',
                'tags': 'test',
                'type': 'test',
                'supportedAbis': <String>['arm64-v8a'],
                'supported32BitAbis': <String>[],
                'supported64BitAbis': <String>['arm64-v8a'],
                'systemFeatures': <String>[],
                'serialNumber': 'unknown',
                'isLowRamDevice': false,
                'freeDiskSize': 1024,
                'totalDiskSize': 2048,
                'physicalRamSize': 4096,
                'availableRamSize': 2048,
                'name': 'TestDevice',
                'displayMetrics': <Object?, Object?>{
                  'widthPx': 1080.0,
                  'heightPx': 2400.0,
                  'xDpi': 420.0,
                  'yDpi': 420.0,
                },
              };
            }
            return null;
          });

      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(connectivityChannel, (call) async {
            if (call.method == 'check') {
              return <String>['wifi'];
            }
            return null;
          });
    });

    tearDown(() {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(deviceInfoChannel, null);
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(connectivityChannel, null);
    });

    test('getDeviceInfo 在无平台插件时抛出可捕获异常', () async {
      // device_info_plus 内部序列化涉及 Map 类型严格匹配，
      // 真实覆盖通过 test_bridge.html 在设备端验收。
      // 此处验证 test hook 通道可达。
      try {
        await shellCoreTestHooks.getDeviceInfoFromBridge();
        // 如果 mock 正好能工作，也属正常
      } catch (e) {
        // MissingPluginException 或类型转换错误均可预期
        expect(e, isNotNull);
      }
    });

    test('getDeviceInfo 成功返回桥接字段', () async {
      final info = await shellCoreTestHooks.getDeviceInfoFromBridge();

      expect(info['platform'], 'android');
      expect(info['brand'], 'TestBrand');
      expect(info['model'], 'TestModel');
      expect(info['manufacturer'], 'TestMfr');
      expect(info['androidVersion'], '14');
      expect(info['sdkInt'], 34);
      expect(info['isPhysicalDevice'], isTrue);
      expect(info['display'], 'TestDisplay');
      expect(info['product'], 'TestProduct');
      expect(info['appName'], _testEnvironment.appName);
      expect(info['appKey'], _testEnvironment.appKey);
      expect(info['shellVersion'], isNotEmpty);
    });

    test('getNetworkStatus 返回 WiFi 状态', () async {
      final status = await shellCoreTestHooks.getNetworkStatusFromBridge();

      expect(status['connected'], isTrue);
      expect(status['type'], 'wifi');
    });

    test('getNetworkStatus 返回 none 时 connected 为 false', () async {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(connectivityChannel, (call) async {
            if (call.method == 'check') return <String>['none'];
            return null;
          });

      final status = await shellCoreTestHooks.getNetworkStatusFromBridge();

      expect(status['connected'], isFalse);
      expect(status['type'], 'none');
    });

    test('getNetworkStatus 返回 cellular 类型', () async {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(connectivityChannel, (call) async {
            if (call.method == 'check') return <String>['mobile'];
            return null;
          });

      final status = await shellCoreTestHooks.getNetworkStatusFromBridge();

      expect(status['connected'], isTrue);
      expect(status['type'], 'cellular');
    });

    test('setStatusBar 设置亮色图标不抛异常', () {
      expect(
        () => shellCoreTestHooks.setStatusBarFromBridge(
          <String, dynamic>{'style': 'light'},
        ),
        returnsNormally,
      );
    });

    test('setStatusBar 设置暗色图标和颜色不抛异常', () {
      expect(
        () => shellCoreTestHooks.setStatusBarFromBridge(
          <String, dynamic>{'style': 'dark', 'color': '#3ED37B'},
        ),
        returnsNormally,
      );
    });

    test('setStatusBar 无参数时使用默认值不抛异常', () {
      expect(
        () => shellCoreTestHooks.setStatusBarFromBridge(<String, dynamic>{}),
        returnsNormally,
      );
    });

    testWidgets('showToast 显示消息并自动消失', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Builder(
            builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  shellCoreTestHooks.showToastFromBridge(
                    context,
                    <String, dynamic>{
                      'message': '测试 Toast',
                      'duration': 'short',
                    },
                  );
                },
                child: const Text('触发'),
              );
            },
          ),
        ),
      );

      await tester.tap(find.text('触发'));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 100));

      expect(find.text('测试 Toast'), findsOneWidget);

      // 等待 Toast 消失
      await tester.pump(const Duration(milliseconds: 2500));
      await tester.pump(const Duration(milliseconds: 300));
      await tester.pumpAndSettle();

      expect(find.text('测试 Toast'), findsNothing);
    });

    testWidgets('showToast 空消息不触发 Toast', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Builder(
            builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  shellCoreTestHooks.showToastFromBridge(
                    context,
                    <String, dynamic>{'message': ''},
                  );
                },
                child: const Text('空消息'),
              );
            },
          ),
        ),
      );

      await tester.tap(find.text('空消息'));
      await tester.pump();

      // 空消息不应显示任何额外的 Toast overlay
      // 由 MaterialApp 自带一些 FadeTransition，检查无多余 Text
      expect(find.text(''), findsNothing);
    });

    testWidgets('主帧资源错误后 pageFinished 不会清空错误浮层', (tester) async {
      try {
        debugDefaultTargetPlatformOverride = TargetPlatform.android;

        await tester.pumpWidget(const MaterialApp(home: WebShellPage()));
        await tester.pump();

        final controller = fakeWebViewPlatform.createdControllers.last;
        final delegate =
            controller.delegate! as _FakePlatformNavigationDelegate;
        const failedUrl = 'https://example.com/login';

        delegate.onPageStarted?.call(failedUrl);
        await tester.pump();

        delegate.onWebResourceError?.call(
          const WebResourceError(
            errorCode: -2,
            description: 'net::ERR_INTERNET_DISCONNECTED',
            errorType: WebResourceErrorType.hostLookup,
            isForMainFrame: true,
            url: failedUrl,
          ),
        );
        await tester.pump();

        expect(find.text('网络连接失败'), findsOneWidget);
        expect(find.text('没有成功连接到服务器，请检查网络后重试。'), findsOneWidget);
        expect(
          find.byKey(const ValueKey<String>('fake-webview')),
          findsNothing,
        );

        delegate.onPageFinished?.call(failedUrl);
        await tester.pump();

        expect(find.text('网络连接失败'), findsOneWidget);
        expect(
          find.byKey(const ValueKey<String>('fake-webview')),
          findsNothing,
        );
      } finally {
        debugDefaultTargetPlatformOverride = null;
      }
    });

    testWidgets('checkVersion 在未配置升级地址时展示提示', (tester) async {
      shellCoreTestHooks.setupUpgradeConfigUrl('   ');

      await tester.pumpWidget(
        MaterialApp(
          home: Builder(
            builder: (context) => ElevatedButton(
              onPressed: () {
                unawaited(
                  shellCoreTestHooks.checkVersion(
                    context,
                    showNoUpdateToast: true,
                  ),
                );
              },
              child: const Text('检查升级'),
            ),
          ),
        ),
      );

      await tester.tap(find.text('检查升级'));
      await tester.pump();

      expect(find.text('未配置升级地址'), findsOneWidget);

      await tester.pump(const Duration(milliseconds: 2500));
      await tester.pump(const Duration(milliseconds: 300));
      await tester.pumpAndSettle();
    });

    testWidgets('checkVersion 在未获取到配置时展示提示', (tester) async {
      shellCoreTestHooks.setupUpgradeConfigUrl(
        'http://example.com/upgrade.json',
      );

      await tester.pumpWidget(
        MaterialApp(
          home: Builder(
            builder: (context) => ElevatedButton(
              onPressed: () {
                unawaited(
                  shellCoreTestHooks.checkVersion(
                    context,
                    showNoUpdateToast: true,
                  ),
                );
              },
              child: const Text('检查升级失败'),
            ),
          ),
        ),
      );

      await tester.tap(find.text('检查升级失败'));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 100));

      expect(find.text('未获取到版本配置'), findsOneWidget);

      await tester.pump(const Duration(milliseconds: 2500));
      await tester.pump(const Duration(milliseconds: 300));
      await tester.pumpAndSettle();
    });

    test('升级配置解析支持 data、平铺、isforce 和非法 JSON', () {
      final wrapped = shellCoreTestHooks.parseUpgradeConfigString(
        '{"data":{"versionName":"1.0.1","version":101,"isforce":1,"remark":"fix","filePath":"https://example.com/app.apk","fileSize":2048}}',
      );
      final flat = shellCoreTestHooks.parseUpgradeConfigString(
        '{"versionName":"1.0.2","version":102}',
      );

      expect(wrapped, isNotNull);
      expect(wrapped!.versionName, '1.0.1');
      expect(wrapped.isForce, 1);
      expect(flat?.version, 102);
      expect(shellCoreTestHooks.parseUpgradeConfigString('{'), isNull);
    });

    test('升级配置转换为 AppUpgradeVersion 时处理缺省值', () {
      expect(
        shellCoreTestHooks.convertUpgradeConfig(
          ShellUpgradeConfig(versionName: '1.0.0'),
        ),
        isNull,
      );
      expect(
        shellCoreTestHooks.convertUpgradeConfig(ShellUpgradeConfig(version: 1)),
        isNull,
      );

      final converted = shellCoreTestHooks.convertUpgradeConfig(
        ShellUpgradeConfig(
          versionName: '1.2.3',
          version: 123,
          isForce: 1,
          remark: 'important update',
          filePath: '',
          fileSize: 2048,
        ),
      );

      expect(converted, isNotNull);
      expect(converted!.versionName, '1.2.3');
      expect(converted.versionBuildNumber, 123);
      expect(converted.isForce, isTrue);
      expect(converted.updateContent, 'important update');
      expect(converted.downloadUrl, isNull);
      expect(converted.appStoreUrl, isNull);
      expect(converted.apkSize, 2048 * 1024);
      expect(converted.supportedMethods, hasLength(3));

      final convertedWithPath = shellCoreTestHooks.convertUpgradeConfig(
        ShellUpgradeConfig(
          versionName: '2.0.0',
          version: 200,
          filePath: 'https://example.com/app.apk',
        ),
      );
      expect(convertedWithPath?.downloadUrl, 'https://example.com/app.apk');
      expect(convertedWithPath?.appStoreUrl, 'https://example.com/app.apk');
    });

    test('获取升级配置支持成功、非法地址、非 200 和异常', () async {
      final successUri = await _startJsonServer(
        (_) async => '{"versionName":"2.0.0","version":200}',
      );

      final success = await _runWithRealHttpClient(() => shellCoreTestHooks.fetchUpgradeConfig(successUri.toString()));
      expect(success?.versionName, '2.0.0');

      expect(await shellCoreTestHooks.fetchUpgradeConfig('%%%'), isNull);

      final notFoundUri = await _startJsonServer((_) async => throw 404);
      expect(
        await _runWithRealHttpClient(() => shellCoreTestHooks.fetchUpgradeConfig(notFoundUri.toString())),
        isNull,
      );

      final unavailableSocket = await ServerSocket.bind(
        InternetAddress.loopbackIPv4,
        0,
      );
      final unavailablePort = unavailableSocket.port;
      await unavailableSocket.close();

      expect(
        await _runWithRealHttpClient(
          () => shellCoreTestHooks.fetchUpgradeConfig(
            'http://127.0.0.1:$unavailablePort/upgrade.json',
          ),
        ),
        isNull,
      );
    });

    test('applyBootstrapConfig 会用远程启动配置覆盖本地配置', () async {
      final bootstrapUri = await _startJsonServer(
        (_) async => '{"data":{"initialUrl":"https://remote.example.com/home","preferredOrientations":["portraitDown"],"upgradeConfigUrl":" https://remote.example.com/upgrade.json "}}',
      );
      shellCoreTestHooks.initializeEnvironment(
        const ShellEnvironment(
          appName: '远程配置应用',
          appKey: 'remote_app',
          accentColor: Color(0xFF3ED37B),
          backgroundColor: Color(0xFFFFFFFF),
          textColor: Color(0xFF1F2937),
          mutedTextColor: Color(0xFF6B7280),
          bootstrapConfigAsset: 'assets/config/remote_bootstrap.json',
        ),
      );
      assetContents['assets/config/remote_bootstrap.json'] = jsonEncode(
        <String, Object?>{
          'initialUrl': 'https://asset.example.com/home',
          'preferredOrientations': <String>['landscapeLeft'],
          'bootstrapConfigUrl': bootstrapUri.toString(),
          'upgradeConfigUrl': 'https://asset.example.com/upgrade.json',
        },
      );

      final upgradeUrl = await _runWithRealHttpClient(
        shellCoreTestHooks.applyBootstrapConfig,
      );

      expect(
        shellCoreTestHooks.configuredInitialUrl,
        'https://remote.example.com/home',
      );
      expect(
        shellCoreTestHooks.configuredPreferredOrientations,
        <DeviceOrientation>[DeviceOrientation.portraitDown],
      );
      expect(upgradeUrl, 'https://remote.example.com/upgrade.json');
      shellCoreTestHooks.initializeEnvironment(_testEnvironment);
    });

    test('applyBootstrapConfig 在远程失败时保留本地升级地址', () async {
      final unavailableSocket = await ServerSocket.bind(
        InternetAddress.loopbackIPv4,
        0,
      );
      final unavailablePort = unavailableSocket.port;
      await unavailableSocket.close();
      shellCoreTestHooks.initializeEnvironment(
        const ShellEnvironment(
          appName: '缓存回退应用',
          appKey: 'fallback_app',
          accentColor: Color(0xFF3ED37B),
          backgroundColor: Color(0xFFFFFFFF),
          textColor: Color(0xFF1F2937),
          mutedTextColor: Color(0xFF6B7280),
          bootstrapConfigAsset: 'assets/config/fallback_bootstrap.json',
        ),
      );
      assetContents['assets/config/fallback_bootstrap.json'] = jsonEncode(
        <String, Object?>{
          'initialUrl': 'https://asset.example.com/start',
          'bootstrapConfigUrl':
              'http://127.0.0.1:$unavailablePort/config.json',
          'upgradeConfigUrl': ' https://asset.example.com/upgrade.json ',
        },
      );

      final upgradeUrl = await _runWithRealHttpClient(
        shellCoreTestHooks.applyBootstrapConfig,
      );

      expect(upgradeUrl, 'https://asset.example.com/upgrade.json');
      shellCoreTestHooks.initializeEnvironment(_testEnvironment);
    });

    test('applyBootstrapConfig 遇到空 initialUrl 时保留环境默认地址', () async {
      shellCoreTestHooks.initializeEnvironment(
        _testEnvironment.copyWith(
          bootstrapConfigAsset: 'assets/config/keep_initial.json',
        ),
      );
      assetContents['assets/config/keep_initial.json'] =
          '{"initialUrl":"","preferredOrientations":[]}';

      await shellCoreTestHooks.applyBootstrapConfig();

      expect(
        shellCoreTestHooks.configuredInitialUrl,
        _testEnvironment.initialUrl,
      );
      shellCoreTestHooks.initializeEnvironment(_testEnvironment);
    });

    testWidgets('LaunchOverlay 配置品牌图片时渲染 Image', (tester) async {
      shellCoreTestHooks.initializeEnvironment(
        _testEnvironment.copyWith(
          splashImage: MemoryImage(Uint8List.fromList(kTransparentImage)),
        ),
      );
      addTearDown(() => shellCoreTestHooks.initializeEnvironment(_testEnvironment));

      await tester.pumpWidget(
        const MaterialApp(
          home: LaunchOverlay(progress: 10, hasMeasuredProgress: true),
        ),
      );

      expect(find.byType(Image), findsOneWidget);
      expect(find.byIcon(Icons.language_rounded), findsNothing);
    });

    test('getNetworkStatus 覆盖 ethernet、bluetooth、vpn 和 other', () async {
      const connectivityChannel = MethodChannel(
        'dev.fluttercommunity.plus/connectivity',
      );

      Future<void> setConnectivity(List<String> values) async {
        TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
            .setMockMethodCallHandler(connectivityChannel, (call) async {
              if (call.method == 'check') return values;
              return null;
            });
      }

      await setConnectivity(<String>['ethernet']);
      expect(
        (await shellCoreTestHooks.getNetworkStatusFromBridge())['type'],
        'ethernet',
      );

      await setConnectivity(<String>['bluetooth']);
      expect(
        (await shellCoreTestHooks.getNetworkStatusFromBridge())['type'],
        'bluetooth',
      );

      await setConnectivity(<String>['vpn']);
      expect(
        (await shellCoreTestHooks.getNetworkStatusFromBridge())['type'],
        'vpn',
      );

      await setConnectivity(<String>['other']);
      expect(
        (await shellCoreTestHooks.getNetworkStatusFromBridge())['type'],
        'unknown',
      );
    });

    testWidgets('showToast 支持 long 时长', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Builder(
            builder: (context) => ElevatedButton(
              onPressed: () {
                shellCoreTestHooks.showToastFromBridge(
                  context,
                  <String, dynamic>{'message': '长提示', 'duration': 'long'},
                );
              },
              child: const Text('长提示按钮'),
            ),
          ),
        ),
      );

      await tester.tap(find.text('长提示按钮'));
      await tester.pump(const Duration(milliseconds: 100));
      expect(find.text('长提示'), findsOneWidget);

      await tester.pump(const Duration(milliseconds: 2000));
      expect(find.text('长提示'), findsOneWidget);

      await tester.pump(const Duration(milliseconds: 1700));
      await tester.pumpAndSettle();
      expect(find.text('长提示'), findsNothing);
    });

    test('升级配置异步解析闭包可返回版本信息', () async {
      final version = await shellCoreTestHooks.resolveUpgradeConfig(
        ShellUpgradeConfig(
          versionName: '3.0.0',
          version: 300,
          filePath: 'https://example.com/app.apk',
        ),
      );

      expect(version?.versionName, '3.0.0');
      expect(version?.versionBuildNumber, 300);
      expect(version?.downloadUrl, 'https://example.com/app.apk');
    });
  });
}

const List<int> kTransparentImage = <int>[
  0x89,
  0x50,
  0x4E,
  0x47,
  0x0D,
  0x0A,
  0x1A,
  0x0A,
  0x00,
  0x00,
  0x00,
  0x0D,
  0x49,
  0x48,
  0x44,
  0x52,
  0x00,
  0x00,
  0x00,
  0x01,
  0x00,
  0x00,
  0x00,
  0x01,
  0x08,
  0x06,
  0x00,
  0x00,
  0x00,
  0x1F,
  0x15,
  0xC4,
  0x89,
  0x00,
  0x00,
  0x00,
  0x0A,
  0x49,
  0x44,
  0x41,
  0x54,
  0x78,
  0x9C,
  0x63,
  0x00,
  0x01,
  0x00,
  0x00,
  0x05,
  0x00,
  0x01,
  0x0D,
  0x0A,
  0x2D,
  0xB4,
  0x00,
  0x00,
  0x00,
  0x00,
  0x49,
  0x45,
  0x4E,
  0x44,
  0xAE,
  0x42,
  0x60,
  0x82,
];

class _RealHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context);
  }
}

class _ThrowingPreferencesStore extends SharedPreferencesStorePlatform {
  @override
  Future<bool> clear() async => true;

  @override
  Future<Map<String, Object>> getAll() async {
    throw Exception('shared preferences failed');
  }

  @override
  Future<bool> remove(String key) async => true;

  @override
  Future<bool> setValue(String valueType, String key, Object value) async =>
      true;
}

class _FakeUrlLauncherPlatform extends UrlLauncherPlatform {
  @override
  LinkDelegate? get linkDelegate => null;

  String? lastUrl;
  bool shouldSucceed = true;
  bool shouldThrow = false;

  @override
  Future<bool> launch(
    String url, {
    required bool useSafariVC,
    required bool useWebView,
    required bool enableJavaScript,
    required bool enableDomStorage,
    required bool universalLinksOnly,
    required Map<String, String> headers,
    String? webOnlyWindowName,
  }) async {
    lastUrl = url;
    if (shouldThrow) {
      throw Exception('launch failed');
    }
    return shouldSucceed;
  }
}

class _FakeImagePickerPlatform extends ImagePickerPlatform {
  XFile? nextImage;
  bool shouldThrow = false;
  ImageSource? lastSource;

  @override
  Future<XFile?> getImageFromSource({
    required ImageSource source,
    ImagePickerOptions options = const ImagePickerOptions(),
  }) async {
    lastSource = source;
    if (shouldThrow) {
      throw PlatformException(code: 'pick-failed');
    }
    return nextImage;
  }
}

class _FakeWebViewPlatform extends WebViewPlatform {
  final createdControllers = <_FakePlatformWebViewController>[];

  @override
  PlatformNavigationDelegate createPlatformNavigationDelegate(
    PlatformNavigationDelegateCreationParams params,
  ) {
    return _FakePlatformNavigationDelegate(params);
  }

  @override
  PlatformWebViewController createPlatformWebViewController(
    PlatformWebViewControllerCreationParams params,
  ) {
    final controller = _FakePlatformWebViewController(params);
    createdControllers.add(controller);
    return controller;
  }

  @override
  PlatformWebViewWidget createPlatformWebViewWidget(
    PlatformWebViewWidgetCreationParams params,
  ) {
    return _FakePlatformWebViewWidget(params);
  }

  @override
  PlatformWebViewCookieManager createPlatformCookieManager(
    PlatformWebViewCookieManagerCreationParams params,
  ) {
    return _FakePlatformWebViewCookieManager(params);
  }
}

class _FakePlatformWebViewCookieManager extends PlatformWebViewCookieManager {
  _FakePlatformWebViewCookieManager(super.params) : super.implementation();

  @override
  Future<bool> clearCookies() async => false;

  @override
  Future<void> setCookie(WebViewCookie cookie) async {}
}

class _FakePlatformNavigationDelegate extends PlatformNavigationDelegate {
  _FakePlatformNavigationDelegate(super.params) : super.implementation();

  NavigationRequestCallback? onNavigationRequest;
  PageEventCallback? onPageStarted;
  PageEventCallback? onPageFinished;
  HttpResponseErrorCallback? onHttpError;
  ProgressCallback? onProgress;
  WebResourceErrorCallback? onWebResourceError;
  UrlChangeCallback? onUrlChange;

  @override
  Future<void> setOnNavigationRequest(
    NavigationRequestCallback onNavigationRequest,
  ) async {
    this.onNavigationRequest = onNavigationRequest;
  }

  @override
  Future<void> setOnPageStarted(PageEventCallback onPageStarted) async {
    this.onPageStarted = onPageStarted;
  }

  @override
  Future<void> setOnPageFinished(PageEventCallback onPageFinished) async {
    this.onPageFinished = onPageFinished;
  }

  @override
  Future<void> setOnHttpError(HttpResponseErrorCallback onHttpError) async {
    this.onHttpError = onHttpError;
  }

  @override
  Future<void> setOnProgress(ProgressCallback onProgress) async {
    this.onProgress = onProgress;
  }

  @override
  Future<void> setOnWebResourceError(
    WebResourceErrorCallback onWebResourceError,
  ) async {
    this.onWebResourceError = onWebResourceError;
  }

  @override
  Future<void> setOnUrlChange(UrlChangeCallback onUrlChange) async {
    this.onUrlChange = onUrlChange;
  }
}

class _FakePlatformWebViewController extends PlatformWebViewController {
  _FakePlatformWebViewController(super.params) : super.implementation();

  final javaScriptCalls = <String>[];
  PlatformNavigationDelegate? delegate;
  bool throwOnRunJavaScript = false;
  bool canGoBackValue = false;
  bool didGoBack = false;
  Uri? lastLoadedUri;

  @override
  Future<void> addJavaScriptChannel(JavaScriptChannelParams params) async {}

  @override
  Future<void> clearCache() async {}

  @override
  Future<void> clearLocalStorage() async {}

  @override
  Future<bool> canGoBack() async => canGoBackValue;

  @override
  Future<void> enableZoom(bool enabled) async {}

  @override
  Future<void> goBack() async {
    didGoBack = true;
  }

  @override
  Future<void> loadRequest(LoadRequestParams params) async {
    lastLoadedUri = params.uri;
  }

  @override
  Future<void> reload() async {}

  @override
  Future<void> runJavaScript(String javaScript) async {
    if (throwOnRunJavaScript) {
      throw Exception('javascript failed');
    }
    javaScriptCalls.add(javaScript);
  }

  @override
  Future<Object> runJavaScriptReturningResult(String javaScript) async => '';

  @override
  Future<void> setBackgroundColor(Color color) async {}

  @override
  Future<void> setJavaScriptMode(JavaScriptMode javaScriptMode) async {}

  @override
  Future<void> setPlatformNavigationDelegate(
    PlatformNavigationDelegate delegate,
  ) async {
    this.delegate = delegate;
  }
}

class _FakePlatformWebViewWidget extends PlatformWebViewWidget {
  _FakePlatformWebViewWidget(super.params) : super.implementation();

  @override
  Widget build(BuildContext context) {
    return const SizedBox(key: ValueKey<String>('fake-webview'));
  }
}
