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

import 'package:flutter_test/flutter_test.dart';
import 'package:smplayer/core/contracts/danmaku.dart';
import 'package:smplayer/core/contracts/script_widget.dart';
import 'package:smplayer/core/domain/errors.dart';
import 'package:smplayer/core/domain/ports/danmaku_sources_store.dart';
import 'package:smplayer/core/domain/ports/script_widget_remote_gateway.dart';
import 'package:smplayer/core/domain/ports/script_widget_repository.dart';
import 'package:smplayer/core/domain/ports/script_widget_runtime.dart';
import 'package:smplayer/core/domain/usecases/script_widget/script_widget_service.dart';

class _MemoryScriptWidgetRepository implements ScriptWidgetRepository {
  final Map<String, InstalledScriptWidget> widgets =
      <String, InstalledScriptWidget>{};
  final Map<String, ScriptWidgetSubscription> subscriptions =
      <String, ScriptWidgetSubscription>{};

  @override
  Future<void> deleteWidget(String widgetId) async {
    widgets.remove(widgetId);
  }

  @override
  Future<InstalledScriptWidget?> findWidget(String widgetId) async {
    return widgets[widgetId];
  }

  @override
  Future<List<ScriptWidgetSubscription>> listSubscriptions() async {
    return subscriptions.values.toList(growable: false);
  }

  @override
  Future<List<InstalledScriptWidget>> listWidgets() async {
    return widgets.values.toList(growable: false);
  }

  @override
  Future<void> upsertSubscription(ScriptWidgetSubscription subscription) async {
    subscriptions[subscription.url] = subscription;
  }

  @override
  Future<void> upsertWidget(InstalledScriptWidget widget) async {
    widgets[widget.manifest.id] = widget;
  }

  @override
  Future<void> replaceAllWidgets(List<InstalledScriptWidget> widgets) async {
    this.widgets
      ..clear()
      ..addEntries(
        widgets.map((widget) => MapEntry(widget.manifest.id, widget)),
      );
  }

  @override
  Future<void> replaceAllSubscriptions(
    List<ScriptWidgetSubscription> subscriptions,
  ) async {
    this.subscriptions
      ..clear()
      ..addEntries(
        subscriptions.map(
          (subscription) => MapEntry(subscription.url, subscription),
        ),
      );
  }
}

class _StaticScriptWidgetRuntime implements ScriptWidgetRuntime {
  _StaticScriptWidgetRuntime(this.manifest, {this.invocationResult});

  ScriptWidgetManifest manifest;
  final Object? invocationResult;
  final Set<String> loadedWidgetIds = <String>{};
  Map<String, Object?>? lastInvocationParams;
  Completer<void>? loadGate;
  int loadCount = 0;

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

  @override
  Future<Object?> invokeModuleFunction(
    String widgetId,
    String functionName,
    Map<String, Object?> params,
  ) async {
    lastInvocationParams = Map<String, Object?>.from(params);
    return invocationResult;
  }

  @override
  Future<ScriptWidgetManifest> inspectWidget(String scriptSource) async {
    return manifest;
  }

  @override
  bool isWidgetLoaded(String widgetId) => loadedWidgetIds.contains(widgetId);

  @override
  Future<ScriptWidgetManifest> loadWidget(String scriptSource) async {
    loadCount++;
    await loadGate?.future;
    loadedWidgetIds.add(manifest.id);
    return manifest;
  }

  @override
  Future<void> unloadWidget(String widgetId) async {
    loadedWidgetIds.remove(widgetId);
  }
}

class _UnusedRemoteGateway implements ScriptWidgetRemoteGateway {
  @override
  Future<String> fetchText(String url) {
    throw UnsupportedError('This test does not perform remote requests.');
  }
}

class _StaticRemoteGateway implements ScriptWidgetRemoteGateway {
  final String scriptSource;

  const _StaticRemoteGateway(this.scriptSource);

  @override
  Future<String> fetchText(String url) async => scriptSource;
}

class _MemoryDanmakuSourcesStore implements DanmakuSourcesStore {
  List<DanmakuSource> sources = <DanmakuSource>[];

  @override
  Future<List<DanmakuSource>> list() async {
    return List<DanmakuSource>.from(sources);
  }

  @override
  Future<void> replaceAll(List<DanmakuSource> sources) async {
    this.sources = List<DanmakuSource>.from(sources);
  }
}

void main() {
  test('视频契约兼容字段别名与宽松数值类型', () {
    final item = ScriptVideoItem.fromJson(<String, Object?>{
      'id': 42,
      'title': '示例影片',
      'poster_url': 'https://example.com/poster.jpg',
      'rating': '8.6',
      'duration': '3600',
    });
    final resource = ScriptVideoResource.fromJson(<String, Object?>{
      'url': 'https://example.com/video.m3u8',
      'headers': <String, Object?>{'Referer': 'https://example.com', 'X': 1},
    });

    expect(item.id, '42');
    expect(item.posterPath, 'https://example.com/poster.jpg');
    expect(item.rating, 8.6);
    expect(item.duration, 3600);
    expect(resource.customHeaders, <String, String>{
      'Referer': 'https://example.com',
      'X': '1',
    });
  });

  test('模块文本状态兼容 subTitle 并保留错误说明', () {
    final items = decodeVideoItems(<Object?>[
      <String, Object?>{
        'id': 'err_cf',
        'type': 'text',
        'title': '被 Cloudflare 拦截',
        'subTitle': '请稍后重试',
      },
    ]);

    expect(items, hasLength(1));
    expect(items.single.type, 'text');
    expect(items.single.title, '被 Cloudflare 拦截');
    expect(items.single.description, '请稍后重试');
  });

  test('模块列表兼容 JSON 字符串返回并从链接生成缺失 ID', () {
    final items = decodeVideoItems(
      jsonEncode(<Object?>[
        <String, Object?>{
          'type': 'link',
          'title': 91,
          'link': 'https://example.com/videos/91',
        },
      ]),
    );

    expect(items, hasLength(1));
    expect(items.single.id, 'https://example.com/videos/91');
    expect(items.single.title, '91');
  });

  test('模块返回 null 或不可识别文本时抛出明确错误', () {
    expect(
      () => decodeVideoItems(null),
      throwsA(
        isA<DomainError>().having(
          (error) => error.message,
          'message',
          '模块返回了无法识别的内容格式',
        ),
      ),
    );
    expect(() => decodeVideoItems('not-json'), throwsA(isA<DomainError>()));
    expect(decodeVideoItems(const <Object?>[]), isEmpty);
  });

  test('sectionMode 保留有效分组并使用回退标题', () {
    final sections = decodeVideoSections(
      <Object?>[
        <String, Object?>{
          'title': '热门',
          'items': <Object?>[
            <String, Object?>{'id': 'movie-1', 'title': '影片一'},
          ],
        },
        <String, Object?>{
          'data': <Object?>[
            <String, Object?>{'id': 'movie-2', 'title': '影片二'},
          ],
        },
        <String, Object?>{'title': '空分组', 'items': <Object?>[]},
      ],
      fallbackTitle: '模块内容',
      preserveSections: true,
    );

    expect(sections, hasLength(2));
    expect(sections[0].title, '热门');
    expect(sections[0].items.single.id, 'movie-1');
    expect(sections[1].title, '模块内容');
    expect(sections[1].items.single.id, 'movie-2');
  });

  test('模块调用补全选项默认值且调用参数优先', () async {
    const module = ScriptWidgetModule(
      title: '直播',
      functionName: 'getVideos',
      params: <ScriptWidgetParam>[
        ScriptWidgetParam(
          name: 'category',
          type: 'enumeration',
          enumOptions: <ScriptWidgetOption>[
            ScriptWidgetOption(value: 'jsonkawayi'),
            ScriptWidgetOption(value: 'jsonmihu'),
          ],
        ),
        ScriptWidgetParam(
          name: 'source',
          value: 'explicit',
          enumOptions: <ScriptWidgetOption>[
            ScriptWidgetOption(value: 'fallback'),
          ],
        ),
        ScriptWidgetParam(
          name: 'legacy',
          placeholders: <ScriptWidgetOption>[
            ScriptWidgetOption(value: 'legacy-first'),
          ],
        ),
      ],
    );
    const manifest = ScriptWidgetManifest(
      id: 'live-widget',
      title: '直播模块',
      modules: <ScriptWidgetModule>[module],
    );
    final timestamp = DateTime.utc(2026, 7, 13);
    final repository = _MemoryScriptWidgetRepository();
    repository.widgets[manifest.id] = InstalledScriptWidget(
      manifest: manifest,
      scriptSource: 'widget source',
      installedAt: timestamp,
      updatedAt: timestamp,
    );
    final runtime = _StaticScriptWidgetRuntime(
      manifest,
      invocationResult: const <Object?>[],
    );
    final service = ScriptWidgetService(
      repository: repository,
      remoteGateway: _UnusedRemoteGateway(),
      runtime: runtime,
    );

    await service.loadModuleSections(
      manifest.id,
      module,
      const <String, Object?>{},
    );
    expect(runtime.lastInvocationParams, <String, Object?>{
      'category': 'jsonkawayi',
      'source': 'explicit',
      'legacy': 'legacy-first',
    });

    await service.loadModuleSections(
      manifest.id,
      module,
      const <String, Object?>{'category': 'jsonmihu'},
    );
    expect(runtime.lastInvocationParams, <String, Object?>{
      'category': 'jsonmihu',
      'source': 'explicit',
      'legacy': 'legacy-first',
    });
  });

  test('远程订阅中的相对模块 URL 相对订阅地址解析', () async {
    final repository = _MemoryScriptWidgetRepository();
    final service = ScriptWidgetService(
      repository: repository,
      remoteGateway: _UnusedRemoteGateway(),
      runtime: _StaticScriptWidgetRuntime(
        const ScriptWidgetManifest(id: 'unused', title: 'Unused'),
      ),
    );
    final rawCatalog = jsonEncode(<String, Object?>{
      'title': '示例订阅',
      'widgets': <Object?>[
        <String, Object?>{
          'id': 'relative',
          'title': '相对模块',
          'url': '../widgets/relative.js',
        },
        <String, Object?>{
          'id': 'absolute',
          'title': '绝对模块',
          'url': 'https://cdn.example.com/absolute.js',
        },
      ],
    });

    final subscription = await service.importSubscriptionSource(
      rawCatalog,
      sourceId: 'https://example.com/catalogs/main/catalog.fwd',
    );

    expect(
      subscription.catalog.widgets[0].url,
      'https://example.com/catalogs/widgets/relative.js',
    );
    expect(
      subscription.catalog.widgets[1].url,
      'https://cdn.example.com/absolute.js',
    );
  });

  test('弹幕模块安装与启停会同步来源并通知宿主刷新', () async {
    final repository = _MemoryScriptWidgetRepository();
    final sourcesStore = _MemoryDanmakuSourcesStore();
    var changeNotificationCount = 0;
    final service = ScriptWidgetService(
      repository: repository,
      remoteGateway: _UnusedRemoteGateway(),
      runtime: _StaticScriptWidgetRuntime(
        const ScriptWidgetManifest(
          id: 'danmaku-widget',
          title: '脚本弹幕',
          modules: <ScriptWidgetModule>[
            ScriptWidgetModule(
              id: 'danmu',
              title: '弹幕',
              functionName: 'searchDanmu',
              type: 'danmu',
            ),
          ],
        ),
      ),
      danmakuSourcesStore: sourcesStore,
      onDanmakuSourcesChanged: () async {
        changeNotificationCount++;
      },
    );

    await service.installFromSource('widget source');
    expect(sourcesStore.sources.single.url, 'jsw://danmaku-widget');
    expect(sourcesStore.sources.single.enabled, isTrue);

    await service.setWidgetEnabled('danmaku-widget', false);
    expect(sourcesStore.sources.single.enabled, isFalse);
    expect(changeNotificationCount, 2);
  });

  test('并发首次调用只加载一次模块运行环境', () async {
    final timestamp = DateTime.utc(2026, 7, 12);
    final repository = _MemoryScriptWidgetRepository();
    repository.widgets['concurrent-widget'] = InstalledScriptWidget(
      manifest: const ScriptWidgetManifest(
        id: 'concurrent-widget',
        title: '并发模块',
      ),
      scriptSource: 'concurrent source',
      installedAt: timestamp,
      updatedAt: timestamp,
    );
    final runtime = _StaticScriptWidgetRuntime(
      const ScriptWidgetManifest(id: 'concurrent-widget', title: '并发模块'),
    )..loadGate = Completer<void>();
    final service = ScriptWidgetService(
      repository: repository,
      remoteGateway: _UnusedRemoteGateway(),
      runtime: runtime,
    );

    final firstInvocation = service.invoke(
      'concurrent-widget',
      'loadContent',
      const <String, Object?>{},
    );
    final secondInvocation = service.invoke(
      'concurrent-widget',
      'loadContent',
      const <String, Object?>{},
    );
    await Future<void>.delayed(Duration.zero);

    expect(runtime.loadCount, 1);
    runtime.loadGate!.complete();
    await Future.wait<Object?>(<Future<Object?>>[
      firstInvocation,
      secondInvocation,
    ]);
    expect(runtime.loadCount, 1);
  });

  test('运行环境失效后下一次调用会重新加载', () async {
    final timestamp = DateTime.utc(2026, 7, 12);
    final repository = _MemoryScriptWidgetRepository();
    repository.widgets['reload-widget'] = InstalledScriptWidget(
      manifest: const ScriptWidgetManifest(id: 'reload-widget', title: '重载模块'),
      scriptSource: 'reload source',
      installedAt: timestamp,
      updatedAt: timestamp,
    );
    final runtime = _StaticScriptWidgetRuntime(
      const ScriptWidgetManifest(id: 'reload-widget', title: '重载模块'),
    );
    final service = ScriptWidgetService(
      repository: repository,
      remoteGateway: _UnusedRemoteGateway(),
      runtime: runtime,
    );

    await service.invoke(
      'reload-widget',
      'loadContent',
      const <String, Object?>{},
    );
    runtime.loadedWidgetIds.clear();
    await service.invoke(
      'reload-widget',
      'loadContent',
      const <String, Object?>{},
    );

    expect(runtime.loadCount, 2);
  });

  test('远程更新返回不同模块 ID 时拒绝覆盖已安装模块', () async {
    final timestamp = DateTime.utc(2026, 7, 12);
    final repository = _MemoryScriptWidgetRepository();
    final installedWidget = InstalledScriptWidget(
      manifest: const ScriptWidgetManifest(id: 'original-widget', title: '原模块'),
      scriptSource: 'original source',
      sourceUrl: 'https://example.com/original.js',
      installedAt: timestamp,
      updatedAt: timestamp,
    );
    repository.widgets['original-widget'] = installedWidget;
    final runtime = _StaticScriptWidgetRuntime(
      const ScriptWidgetManifest(id: 'other-widget', title: '其他模块'),
    );
    final service = ScriptWidgetService(
      repository: repository,
      remoteGateway: const _StaticRemoteGateway('replacement source'),
      runtime: runtime,
    );

    await expectLater(
      service.refreshWidget('original-widget'),
      throwsA(isA<DomainError>()),
    );

    expect(repository.widgets['original-widget'], installedWidget);
    expect(repository.widgets.containsKey('other-widget'), isFalse);
    expect(runtime.loadCount, 0);
  });
}
