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

import 'package:fjs/fjs.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
    show ExternalLibrary;
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:smplayer/core/contracts/error.dart';
import 'package:smplayer/core/domain/errors.dart';
import 'package:smplayer/core/infra/script_widget/quickjs_widget_runtime.dart';
import 'package:smplayer/core/infra/script_widget/safe_script_http_client.dart';
import 'package:smplayer/core/infra/script_widget/widget_host_bridge.dart';

const String _widgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-test',
  title: 'Runtime Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function inspectHost(params) {
  const document = Widget.html.load(
    '<main><a class="item" href="/detail">  示例内容  </a></main>'
  );
  const item = document('.item').first();
  return {
    value: params.value,
    storedValue: Widget.storage.get('seed'),
    text: item.text().trim(),
    href: item.attr('href')
  };
}

async function loadDetail(link) {
  return { id: 'detail', title: link };
}
''';


Future<bool> _tryInitFjs() async {
  final environmentPath = Platform.environment['FJS_LIBRARY_PATH'];
  final candidatePaths = <String>[
    if (environmentPath != null && environmentPath.isNotEmpty) environmentPath,
    'build/fjs_native/release/libfjs.dylib',
    'build/fjs_native/release/libfjs.so',
    'build/fjs_native/release/fjs.dll',
  ];
  for (final path in candidatePaths) {
    if (!File(path).existsSync()) continue;
    try {
      await LibFjs.init(externalLibrary: ExternalLibrary.open(path));
      return true;
    } on StateError {
      return true; 
    } catch (_) {}
  }
  try {
    await LibFjs.init();
    return true;
  } on StateError {
    return true;
  } catch (_) {
    return false;
  }
}

Future<void> main() async {
  TestWidgetsFlutterBinding.ensureInitialized();

  final fjsReady = await _tryInitFjs();

  setUp(() {
    SharedPreferences.setMockInitialValues(<String, Object>{
      'smplayer.script_widget.storage.runtime-test': jsonEncode(
        <String, Object?>{'seed': 'persisted'},
      ),
    });
  });

  group(
    'QuickJsWidgetRuntime',
    () {
      test('加载元数据并调用宿主 storage 与 HTML API', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);

        final manifest = await runtime.loadWidget(_widgetScript);
        final result = await runtime.invokeModuleFunction(
          manifest.id,
          'inspectHost',
          <String, Object?>{'value': 7},
        );

        expect(manifest.title, 'Runtime Test');
        expect(result, <String, Object?>{
          'value': 7,
          'storedValue': 'persisted',
          'text': '示例内容',
          'href': '/detail',
        });
      });

      test('ForwardWidgets 原始参数调用保持字符串参数', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_widgetScript);

        final result = await runtime.invokeModuleFunction(
          'runtime-test',
          'loadDetail',
          <String, Object?>{
            '__forwardRawArgument': 'https://example.com/detail/42',
            'link': 'ignored wrapper value',
          },
        );

        expect(result, <String, Object?>{
          'id': 'detail',
          'title': 'https://example.com/detail/42',
        });
      });

      test('卸载模块后拒绝继续调用', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_widgetScript);
        await runtime.unloadWidget('runtime-test');

        await expectLater(
          runtime.invokeModuleFunction(
            'runtime-test',
            'inspectHost',
            <String, Object?>{},
          ),
          throwsA(isA<DomainError>()),
        );
      });

      test('DOM 助手覆盖 closest、顶层 text 与 console.debug', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_domHelpersWidgetScript);

        final result = await runtime.invokeModuleFunction(
          'runtime-dom-test',
          'inspectDomHelpers',
          <String, Object?>{},
        );

        expect(result, <String, Object?>{
          'honeypotAncestorClass': 'col-lg-8',
          'noAncestorForNormal': true,
          'topLevelText': '你好世界',
          'consoleDebugCallable': true,
        });
      });


      test('Widget.dom 句柄 API 支持 parse/select/text/attr/html', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_domHandleWidgetScript);

        final result = await runtime.invokeModuleFunction(
          'runtime-dom-handle-test',
          'inspectDomHandles',
          <String, Object?>{},
        );

        expect(result, <String, Object?>{
          'itemText': '示例',
          'itemHref': '/detail',
          'scopedCount': 1,
          'outerScopedCount': 2,
          'docHtmlContainsItem': true,
          'missingAttr': null,
        });
      });

      test(
        '模块使用 statusCode 校验 http 响应时可正常拿到抓取的 HTML',
        () async {
          final httpClient = _StubbedScriptHttpClient(
            responseHtmlByUrl: <String, String>{
              'https://sample.local/list': _sampleFakeListHtml,
            },
          );
          final runtime = QuickJsWidgetRuntime(
            hostBridge: WidgetHostBridge(httpClient: httpClient),
          );
          addTearDown(runtime.dispose);
          await runtime.loadWidget(_httpStatusCodeWidgetScript);

          final rawItems = await runtime.invokeModuleFunction(
            'runtime-http-test',
            'fetchItems',
            <String, Object?>{},
          );

          expect(rawItems, <Map<String, Object?>>[
            <String, Object?>{
              'title': '正常条目',
              'link': 'https://sample.local/normal',
            },
          ]);
        },
      );

      test('模块超时后运行环境被卸载，宿主 Future 迟到完成不会出错', () async {
        final httpClient = _StubbedScriptHttpClient(
          responseHtmlByUrl: <String, String>{
            'https://sample.local/list': _sampleFakeListHtml,
          },
          responseDelay: const Duration(milliseconds: 150),
        );
        final runtime = QuickJsWidgetRuntime(
          hostBridge: WidgetHostBridge(httpClient: httpClient),
          invocationTimeout: const Duration(milliseconds: 50),
        );
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_httpStatusCodeWidgetScript);

        await expectLater(
          runtime.invokeModuleFunction(
            'runtime-http-test',
            'fetchItems',
            <String, Object?>{},
          ),
          throwsA(
            isA<DomainError>().having(
              (error) => error.code,
              'code',
              ErrorCode.serverTimeout,
            ),
          ),
        );


        await Future<void>.delayed(const Duration(milliseconds: 250));
        expect(runtime.isWidgetLoaded('runtime-http-test'), isFalse);
      });

      test('async 函数返回 List<Map> 时得到规范化后的 Dart 集合', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_asyncReturnShapesWidgetScript);

        final result = await runtime.invokeModuleFunction(
          'runtime-async-shapes-test',
          'returnList',
          <String, Object?>{},
        );

        expect(result, isA<List<Object?>>());
        expect(result, <Map<String, Object?>>[
          <String, Object?>{'title': '正常', 'link': '/a'},
          <String, Object?>{'title': '也是正常', 'link': '/b'},
        ]);
      });

      test('async 函数显式返回 null 时上层拿到 Dart null', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_asyncReturnShapesWidgetScript);

        final result = await runtime.invokeModuleFunction(
          'runtime-async-shapes-test',
          'returnNull',
          <String, Object?>{},
        );

        expect(result, isNull);
      });

      test('async 函数抛异常时映射为 DomainError.internalError', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_asyncReturnShapesWidgetScript);

        await expectLater(
          runtime.invokeModuleFunction(
            'runtime-async-shapes-test',
            'throwError',
            <String, Object?>{},
          ),
          throwsA(
            isA<DomainError>().having(
              (error) => error.code,
              'code',
              ErrorCode.internalError,
            ),
          ),
        );
      });


      test('脚本尾部使用 CommonJS 导出时仍能安装并调用', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);

        final manifest = await runtime.loadWidget(_commonJsExportsWidgetScript);
        expect(manifest.id, 'runtime-commonjs-test');
        expect(manifest.modules, hasLength(1));

        final result = await runtime.invokeModuleFunction(
          manifest.id,
          'reportEnvironment',
          <String, Object?>{},
        );
        expect(result, <String, Object?>{
          'hasModule': true,
          'hasExports': true,
          'hasGlobal': true,
          'hasSelf': true,
          'exportedTitle': 'CommonJS Test',
        });
      });


      test('Widget.storage 支持 Web Storage 风格的 getItem/setItem 别名', () async {
        final runtime = QuickJsWidgetRuntime();
        addTearDown(runtime.dispose);
        await runtime.loadWidget(_storageAliasWidgetScript);

        final result = await runtime.invokeModuleFunction(
          'runtime-storage-alias-test',
          'roundTrip',
          <String, Object?>{},
        );
        expect(result, <String, Object?>{
          'writtenValue': 'session-token-42',
          'readBackValue': 'session-token-42',
          'valueAfterRemoval': null,
        });
      });


      test(
        '死循环脚本在 invocationTimeout 后被终止并卸载',
        () async {
          final runtime = QuickJsWidgetRuntime(
            invocationTimeout: const Duration(seconds: 2),
          );
          addTearDown(runtime.dispose);
          await runtime.loadWidget(_infiniteLoopWidgetScript);

          await expectLater(
            runtime.invokeModuleFunction(
              'runtime-watchdog-test',
              'spinForever',
              <String, Object?>{},
            ),
            throwsA(
              isA<DomainError>().having(
                (error) => error.code,
                'code',
                ErrorCode.serverTimeout,
              ),
            ),
          );
          expect(runtime.isWidgetLoaded('runtime-watchdog-test'), isFalse);
        },
        timeout: const Timeout(Duration(seconds: 15)),
      );
    },
    skip: fjsReady
        ? false
        : '未找到 fjs 原生库（libfjs）。构建方式见本文件 _tryInitFjs 注释。',
  );

  group('normalizeQuickJsValue', () {
    test(
      'Map<dynamic, dynamic> 与 List<dynamic> 递归规范化为 Map<String, dynamic>',
      () {
        final normalized = normalizeQuickJsValue(<dynamic, dynamic>{
          'items': <dynamic>[
            <dynamic, dynamic>{'id': 'foo', 'value': 1},
            <dynamic, dynamic>{'id': 'bar', 'value': 2},
          ],
          'total': 2,
        });

        expect(normalized, isA<Map<String, dynamic>>());
        final map = normalized! as Map<String, dynamic>;
        expect(map['total'], 2);
        final items = (map['items']! as List<Object?>)
            .cast<Map<String, dynamic>>();
        expect(items, hasLength(2));
        expect(items.first, <String, dynamic>{'id': 'foo', 'value': 1});
      },
    );

    test('原始类型与 null 保持不变', () {
      expect(normalizeQuickJsValue(null), isNull);
      expect(normalizeQuickJsValue('hello'), 'hello');
      expect(normalizeQuickJsValue(42), 42);
      expect(normalizeQuickJsValue(true), true);
    });

    test('非字符串键会被转为字符串键，避免下游 Map 类型转换失败', () {
      final normalized = normalizeQuickJsValue(<dynamic, dynamic>{
        1: 'one',
        2: 'two',
      });

      expect(normalized, <String, dynamic>{'1': 'one', '2': 'two'});
    });
  });
}

const String _domHelpersWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-dom-test',
  title: 'DOM Helpers Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function inspectDomHelpers() {
  const document = Widget.html.load(
    '<section>'
    + '<article class="videos-text-align" data-id="normal">'
    + '  <span class="title">正常</span>'
    + '</article>'
    + '<div class="col-lg-8">'
    + '  <article class="videos-text-align" data-id="honeypot">'
    + '    <span class="title">蜜罐</span>'
    + '  </article>'
    + '</div>'
    + '</section>'
  );
  const normalTitle = document('.videos-text-align').eq(0).find('.title');
  const honeypotTitle = document('.videos-text-align').eq(1).find('.title');

  const honeypotAncestor = honeypotTitle.closest('.col-lg-8');
  const normalAncestor = normalTitle.closest('.col-lg-8');

  const paragraphDocument = Widget.html.load('<p>你好世界</p>');
  const topLevelText = paragraphDocument.text().trim();

  var consoleDebugCallable = false;
  try {
    console.debug('探针');
    consoleDebugCallable = true;
  } catch (error) {}

  return {
    honeypotAncestorClass: honeypotAncestor.attr('class'),
    noAncestorForNormal: normalAncestor.length === 0,
    topLevelText: topLevelText,
    consoleDebugCallable: consoleDebugCallable
  };
}
''';

const String _domHandleWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-dom-handle-test',
  title: 'DOM Handle Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function inspectDomHandles() {
  const docHandle = Widget.dom.parse(
    '<main>'
    + '<div class="wrap"><a class="item" href="/detail">示例</a></div>'
    + '<a class="item" href="/other">其他</a>'
    + '</main>'
  );
  const wrapHandles = Widget.dom.select(docHandle, '.wrap');
  const scopedItems = Widget.dom.select(wrapHandles[0], '.item');
  const allItems = Widget.dom.select(docHandle, '.item');
  return {
    itemText: Widget.dom.text(scopedItems[0]),
    itemHref: Widget.dom.attr(scopedItems[0], 'href'),
    scopedCount: scopedItems.length,
    outerScopedCount: allItems.length,
    docHtmlContainsItem: Widget.dom.html(docHandle).indexOf('class="item"') >= 0,
    missingAttr: Widget.dom.attr(scopedItems[0], 'data-missing')
  };
}
''';

const String _asyncReturnShapesWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-async-shapes-test',
  title: 'Async Return Shapes Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function returnList() {
  return [
    { title: '正常', link: '/a' },
    { title: '也是正常', link: '/b' }
  ];
}

async function returnNull() {
  return null;
}

async function throwError() {
  throw new Error('模块脚本内的显式错误');
}
''';

const String _commonJsExportsWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-commonjs-test',
  title: 'CommonJS Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: [
    {
      title: '环境探测',
      functionName: 'reportEnvironment',
      params: []
    }
  ]
};

async function reportEnvironment() {
  return {
    hasModule: typeof module === 'object' && module !== null,
    hasExports: typeof exports === 'object' && exports !== null,
    hasGlobal: typeof global === 'object' && global !== null,
    hasSelf: typeof self === 'object' && self !== null,
    exportedTitle: module.exports.metadata ? module.exports.metadata.title : ''
  };
}

module.exports = {
  metadata: WidgetMetadata,
  reportEnvironment: reportEnvironment
};
''';

const String _storageAliasWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-storage-alias-test',
  title: 'Storage Alias Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function roundTrip() {
  Widget.storage.setItem('token', 'session-token-42');
  const written = Widget.storage.getItem('token');
  Widget.storage.removeItem('token');
  return {
    writtenValue: 'session-token-42',
    readBackValue: written,
    valueAfterRemoval: Widget.storage.getItem('token') || null
  };
}
''';

const String _infiniteLoopWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-watchdog-test',
  title: 'Watchdog Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function spinForever() {
  var counter = 0;
  while (true) {
    counter = (counter + 1) % 1000003;
    if (counter < 0) break;
  }
  return counter;
}
''';

const String _httpStatusCodeWidgetScript = r'''
const WidgetMetadata = {
  id: 'runtime-http-test',
  title: 'HTTP StatusCode Test',
  version: '1.0.0',
  requiredVersion: '0.0.1',
  modules: []
};

async function fetchItems() {
  const response = await Widget.http.get('https://sample.local/list');
  if (!response || response.statusCode !== 200) {
    throw new Error('意料之外的响应：' + (response && response.statusCode));
  }
  const document = Widget.html.load(response.data);
  return Array.from(document('.videos-text-align')).map(function(element) {
    const wrapper = document(element);
    const link = wrapper.find('a').attr('href');
    if (!link) return null;
    if (wrapper.closest('.col-lg-8').length > 0) return null;
    return {
      title: wrapper.find('.title').text().trim(),
      link: link
    };
  }).filter(function(item) { return item != null; });
}
''';

const String _sampleFakeListHtml =
    '<div>'
    '<div class="videos-text-align">'
    '<a href="https://sample.local/normal"></a>'
    '<span class="title">正常条目</span>'
    '</div>'
    '<div class="col-lg-8">'
    '<div class="videos-text-align">'
    '<a href="https://sample.local/honeypot"></a>'
    '<span class="title">蜜罐</span>'
    '</div>'
    '</div>'
    '</div>';

class _StubbedScriptHttpClient extends SafeScriptHttpClient {
  final Map<String, String> responseHtmlByUrl;
  final Duration responseDelay;

  _StubbedScriptHttpClient({
    required this.responseHtmlByUrl,
    this.responseDelay = Duration.zero,
  });

  @override
  Future<SafeScriptHttpResponse> request({
    required Uri uri,
    String method = 'GET',
    Map<String, String> headers = const <String, String>{},
    Map<String, Object?> queryParameters = const <String, Object?>{},
    Object? body,
    bool followRedirects = true,
  }) async {
    if (responseDelay > Duration.zero) {
      await Future<void>.delayed(responseDelay);
    }
    final resolvedUrl = uri.toString();
    final responseHtml = responseHtmlByUrl[resolvedUrl];
    if (responseHtml == null) {
      throw StateError('未注册 HTTP mock：$resolvedUrl');
    }
    return SafeScriptHttpResponse(
      data: responseHtml,
      text: responseHtml,
      statusCode: 200,
      reasonPhrase: 'OK',
      headers: const <String, String>{'content-type': 'text/html'},
    );
  }
}
