// desktop_vpn_bridge_m4m5_test.dart — M4/M5 功能单元测试
//
// 验收条件（tsk_xAQhC1xuCd8x）：
//   M4 URLTest：
//     1. ClashApiClient.extractUrltestResults 正确解析 /proxies 响应
//     2. extractUrltestResults 在无 URLTest 组时返回空列表
//     3. extractUrltestResults 跳过无 history 的成员
//     4. ClashApiClient.getGroupDelay 发送正确 GET 请求并解析结果
//     5. selectOutbound 通过 Clash API PUT /proxies/proxy 切换出口
//     6. getActiveOutbound 从 /proxies 读取 proxy.now
//     7. getActiveOutbound 在内核未运行时返回 'auto'
//   M5 Kill-switch：
//     8. applyKillSwitchToConfig(true)  设置 TUN strict_route=true
//     9. applyKillSwitchToConfig(false) 设置 TUN strict_route=false
//    10. 无 TUN inbound 时 applyKillSwitchToConfig 原样返回
//    11. setKillSwitch 保存 _killSwitchEnabled 并在 start 时注入 config
//   M5 退避重连：
//    12. 内核崩溃推 error → UI 不崩（error 事件流通）
//    13. 自动重连调度：error 后 _scheduleReconnect 在 1s 内触发（首次延迟）
//    14. stop() 取消自动重连（不再触发重连）
//    15. 重连成功后 reconnectAttempt 重置为 0

// ignore_for_file: avoid_print

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

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';

import 'package:pangolin_vpn/bridge/kernel_process.dart';
import 'package:pangolin_vpn/bridge/desktop_vpn_bridge.dart';
import 'package:pangolin_vpn/bridge/vpn_bridge.dart';

// ══════════════════════════════════════════════════════════════════
// FakeKernelProcess（复用自 kernel_process_test.dart，支持崩溃模拟）
// ══════════════════════════════════════════════════════════════════

class FakeKernelProcess implements KernelProcess {
  FakeKernelProcess({
    this.shouldFailSpawn = false,
    this.crashAfterMs,
  });

  final bool shouldFailSpawn;
  final int? crashAfterMs; // 若非 null，spawn 成功后在此延迟后模拟崩溃

  bool _running = false;
  String? lastConfigPath;
  int spawnCount = 0;
  int killCount = 0;
  ClashApiClient? _fakeClashApi;

  final _statusCtrl = StreamController<VpnStatus>.broadcast();
  final _logCtrl = StreamController<String>.broadcast();
  final _statsCtrl = StreamController<VpnStatsEvent>.broadcast();

  @override
  bool get isRunning => _running;

  @override
  ClashApiClient get clashApiClient =>
      _fakeClashApi ?? (throw StateError('FakeKernelProcess.clashApiClient: set fakeClashApi first'));

  set fakeClashApi(ClashApiClient c) => _fakeClashApi = c;

  @override
  Stream<String> get logStream => _logCtrl.stream;

  @override
  Stream<VpnStatus> get statusStream => _statusCtrl.stream;

  @override
  Stream<VpnStatsEvent> get statsStream => _statsCtrl.stream;

  @override
  Future<void> spawn(String configPath) async {
    lastConfigPath = configPath;
    spawnCount++;
    if (shouldFailSpawn) {
      _emitStatus(VpnStatus.error);
      throw Exception('fake spawn failure');
    }

    _running = true;
    _emitStatus(VpnStatus.connecting);
    await Future<void>.delayed(const Duration(milliseconds: 10));
    _emitStatus(VpnStatus.on);

    if (crashAfterMs != null) {
      Future<void>.delayed(Duration(milliseconds: crashAfterMs!)).then((_) {
        if (_running) {
          _running = false;
          _emitStatus(VpnStatus.error);
        }
      });
    }
  }

  @override
  Future<void> kill({Duration gracePeriod = const Duration(seconds: 5)}) async {
    killCount++;
    if (_running) {
      _running = false;
      _emitStatus(VpnStatus.off);
    }
  }

  void emitStats(VpnStatsEvent e) {
    if (!_statsCtrl.isClosed) _statsCtrl.add(e);
  }

  /// 手动触发崩溃（error 状态）
  void simulateCrash() {
    if (_running) {
      _running = false;
      _emitStatus(VpnStatus.error);
    }
  }

  void disposeFake() {
    _statusCtrl.close();
    _logCtrl.close();
    _statsCtrl.close();
  }

  void _emitStatus(VpnStatus s) {
    if (!_statusCtrl.isClosed) _statusCtrl.add(s);
  }
}

// ══════════════════════════════════════════════════════════════════
// TestBridge: 覆盖 writeConfig，避免写真实文件系统
// ══════════════════════════════════════════════════════════════════

class _TestBridge extends DesktopVpnBridge {
  _TestBridge(this.fakeKernel) : super(kernel: fakeKernel);

  final FakeKernelProcess fakeKernel;
  String? lastWrittenConfig;
  List<String> allWrittenConfigs = [];

  @override
  Future<String> writeConfig(String configJson) async {
    lastWrittenConfig = configJson;
    allWrittenConfigs.add(configJson);
    final tmp = Directory.systemTemp.createTempSync('pangolin_m4m5_test_');
    final f = File('${tmp.path}/config.json')..writeAsStringSync(configJson);
    return f.path;
  }

  @override
  void dispose() {
    fakeKernel.disposeFake();
    super.dispose();
  }
}

// ══════════════════════════════════════════════════════════════════
// 测试套件
// ══════════════════════════════════════════════════════════════════

void main() {
  // ── M4: ClashApiClient.extractUrltestResults ──────────────────
  group('M4 ClashApiClient.extractUrltestResults', () {
    test('1. 正确解析单 URLTest 组的成员延迟', () {
      final proxiesResp = {
        'proxies': {
          'auto-select': {
            'type': 'URLTest',
            'now': 'hk-1',
            'all': ['hk-1', 'sg-1'],
          },
          'hk-1': {
            'type': 'VMess',
            'alive': true,
            'history': [
              {'time': '2024-01-01T00:00:00Z', 'delay': 18},
            ],
          },
          'sg-1': {
            'type': 'VMess',
            'alive': true,
            'history': [
              {'time': '2024-01-01T00:00:00Z', 'delay': 54},
            ],
          },
        },
      };

      final results = ClashApiClient.extractUrltestResults(proxiesResp);

      expect(results.length, 2);
      final tags = results.map((r) => r.tag).toList();
      expect(tags, containsAll(['hk-1', 'sg-1']));
      final hk = results.firstWhere((r) => r.tag == 'hk-1');
      expect(hk.delayMs, 18);
      final sg = results.firstWhere((r) => r.tag == 'sg-1');
      expect(sg.delayMs, 54);
    });

    test('2. 无 URLTest 组时返回空列表', () {
      final proxiesResp = {
        'proxies': {
          'GLOBAL': {
            'type': 'Selector',
            'all': ['DIRECT'],
          },
          'DIRECT': {'type': 'Direct'},
        },
      };

      final results = ClashApiClient.extractUrltestResults(proxiesResp);
      expect(results, isEmpty);
    });

    test('3. 跳过无 history 的成员（不崩溃）', () {
      final proxiesResp = {
        'proxies': {
          'auto-select': {
            'type': 'URLTest',
            'all': ['ok-node', 'no-history-node'],
          },
          'ok-node': {
            'type': 'VMess',
            'history': [
              {'time': '2024-01-01T00:00:00Z', 'delay': 45},
            ],
          },
          'no-history-node': {
            'type': 'VMess',
            'history': <Map<String, dynamic>>[],
          },
        },
      };

      final results = ClashApiClient.extractUrltestResults(proxiesResp);
      // 只有 ok-node 有 history
      expect(results.length, 1);
      expect(results.first.tag, 'ok-node');
      expect(results.first.delayMs, 45);
    });

    test('4. proxies 为空 Map 时返回空列表', () {
      final results =
          ClashApiClient.extractUrltestResults({'proxies': <String, dynamic>{}});
      expect(results, isEmpty);
    });

    test('4b. 顶层无 proxies key 时返回空列表', () {
      final results = ClashApiClient.extractUrltestResults({});
      expect(results, isEmpty);
    });
  });

  // ── M4: ClashApiClient.getGroupDelay ─────────────────────────
  group('M4 ClashApiClient.getGroupDelay', () {
    test('5. 发送正确 GET 请求并解析 tag→delayMs', () async {
      http.Request? capturedReq;
      final mock = MockClient((req) async {
        capturedReq = req;
        return http.Response(
          jsonEncode({'hk-1': 18, 'sg-1': 54}),
          200,
        );
      });

      final client = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', httpClient: mock);
      final result = await client.getGroupDelay('auto-select');

      expect(capturedReq?.method, 'GET');
      expect(capturedReq?.url.path, '/group/auto-select/delay');
      expect(capturedReq?.url.queryParameters['url'],
          'http://www.gstatic.com/generate_204');
      expect(result['hk-1'], 18);
      expect(result['sg-1'], 54);

      client.dispose();
    });

    test('5b. 自定义 testUrl 和 timeoutMs', () async {
      http.Request? capturedReq;
      final mock = MockClient((req) async {
        capturedReq = req;
        return http.Response(jsonEncode(<String, dynamic>{}), 200);
      });

      final client = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', httpClient: mock);
      await client.getGroupDelay('auto-select',
          testUrl: 'http://example.com', timeoutMs: 5000);

      expect(capturedReq?.url.queryParameters['url'], 'http://example.com');
      expect(capturedReq?.url.queryParameters['timeout'], '5000');

      client.dispose();
    });

    test('5c. 非200状态码抛出 HttpException', () async {
      final mock =
          MockClient((_) async => http.Response('Not Found', 404));
      final client = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', httpClient: mock);

      await expectLater(
        client.getGroupDelay('auto-select'),
        throwsA(isA<HttpException>()),
      );
      client.dispose();
    });
  });

  // ── M4: selectOutbound & getActiveOutbound ───────────────────
  group('M4 selectOutbound & getActiveOutbound', () {
    test('6. selectOutbound 调用 PUT /proxies/proxy', () async {
      Map<String, dynamic>? sentBody;
      String? capturedTag;

      final fake = FakeKernelProcess();
      final mock = MockClient((req) async {
        if (req.method == 'PUT') {
          capturedTag = req.url.pathSegments.last;
          sentBody = jsonDecode(req.body) as Map<String, dynamic>;
          return http.Response('', 204);
        }
        // GET /connections (readiness)
        return http.Response(
            jsonEncode({'downloadTotal': 0, 'uploadTotal': 0, 'connections': []}),
            200);
      });

      fake.fakeClashApi = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', httpClient: mock);

      final bridge = _TestBridge(fake);
      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));

      await bridge.selectOutbound('sg-1');

      expect(capturedTag, 'proxy');
      expect(sentBody?['name'], 'sg-1');

      bridge.dispose();
    });

    test('6b. selectOutbound 用 auto-select 可切回 URLTest 自动模式', () async {
      String? selectedProxy;

      final fake = FakeKernelProcess();
      final mock = MockClient((req) async {
        if (req.method == 'PUT') {
          final body = jsonDecode(req.body) as Map<String, dynamic>;
          selectedProxy = body['name'] as String?;
          return http.Response('', 204);
        }
        return http.Response(
            jsonEncode({'downloadTotal': 0, 'uploadTotal': 0, 'connections': []}),
            200);
      });

      fake.fakeClashApi = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', httpClient: mock);

      final bridge = _TestBridge(fake);
      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));

      await bridge.selectOutbound('auto-select'); // 切回自动

      expect(selectedProxy, 'auto-select');
      bridge.dispose();
    });

    test('7. getActiveOutbound 在内核未运行时返回 "auto"', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);

      final active = await bridge.getActiveOutbound();
      expect(active, 'auto');

      bridge.dispose();
    });

    test('7b. getActiveOutbound 从 proxies[proxy][now] 读取', () async {
      final fake = FakeKernelProcess();
      final mock = MockClient((req) async {
        if (req.url.path == '/proxies') {
          return http.Response(
            jsonEncode({
              'proxies': {
                'proxy': {'type': 'Selector', 'now': 'sg-1'},
              },
            }),
            200,
          );
        }
        return http.Response(
            jsonEncode({'downloadTotal': 0, 'uploadTotal': 0, 'connections': []}),
            200);
      });

      fake.fakeClashApi = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', httpClient: mock);

      final bridge = _TestBridge(fake);
      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));

      final active = await bridge.getActiveOutbound();
      expect(active, 'sg-1');

      bridge.dispose();
    });
  });

  // ── M5: applyKillSwitchToConfig ──────────────────────────────
  group('M5 applyKillSwitchToConfig', () {
    const configWithTun = '''{
      "inbounds": [
        {"type": "tun", "tag": "tun-in", "strict_route": false}
      ],
      "outbounds": [{"type": "direct", "tag": "direct"}]
    }''';

    test('8. killSwitch=true 设置 TUN strict_route=true', () {
      final result =
          DesktopVpnBridge.applyKillSwitchToConfig(configWithTun, true);
      final decoded = jsonDecode(result) as Map<String, dynamic>;
      final inbounds = decoded['inbounds'] as List;
      final tun = inbounds.firstWhere((e) => e['type'] == 'tun') as Map;
      expect(tun['strict_route'], true);
    });

    test('9. killSwitch=false 设置 TUN strict_route=false', () {
      const configWithKsOn = '''{
        "inbounds": [{"type": "tun", "strict_route": true}]
      }''';
      final result =
          DesktopVpnBridge.applyKillSwitchToConfig(configWithKsOn, false);
      final decoded = jsonDecode(result) as Map<String, dynamic>;
      final inbounds = decoded['inbounds'] as List;
      final tun = inbounds.first as Map;
      expect(tun['strict_route'], false);
    });

    test('10. 无 TUN inbound 时原样返回（不抛异常）', () {
      const noTun = '{"inbounds": [{"type": "http", "listen_port": 7890}]}';
      final result = DesktopVpnBridge.applyKillSwitchToConfig(noTun, true);
      // 无 TUN，原样返回
      expect(jsonDecode(result)['inbounds'][0]['type'], 'http');
    });

    test('10b. 无效 JSON 原样返回（不抛异常）', () {
      const invalid = 'not-json';
      expect(() => DesktopVpnBridge.applyKillSwitchToConfig(invalid, true),
          returnsNormally);
    });

    test('10c. 保留非 TUN inbound 及其他 config 字段', () {
      const multi = '''{
        "log": {"level": "info"},
        "inbounds": [
          {"type": "mixed", "listen_port": 7890},
          {"type": "tun", "strict_route": false}
        ]
      }''';
      final result = DesktopVpnBridge.applyKillSwitchToConfig(multi, true);
      final decoded = jsonDecode(result) as Map<String, dynamic>;
      expect((decoded['log'] as Map)['level'], 'info');
      final inbounds = decoded['inbounds'] as List;
      expect(inbounds.length, 2);
      expect(inbounds[0]['type'], 'mixed'); // 非 TUN 保留不变
      expect(inbounds[1]['strict_route'], true);
    });
  });

  // ── M5: setKillSwitch 注入行为 ───────────────────────────────
  group('M5 setKillSwitch config injection', () {
    test('11. start 时默认 killSwitch=false 保持原 strict_route 值', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);

      const cfg = '{"inbounds":[{"type":"tun","strict_route":true}]}';
      await bridge.start(cfg);
      await Future<void>.delayed(const Duration(milliseconds: 30));

      // killSwitch=false → strict_route=false
      final written = jsonDecode(bridge.lastWrittenConfig!) as Map<String, dynamic>;
      final tun = (written['inbounds'] as List)
          .firstWhere((e) => e['type'] == 'tun') as Map;
      expect(tun['strict_route'], false);

      bridge.dispose();
    });

    test('11b. setKillSwitch(on: true) 后 start 注入 strict_route=true', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);

      await bridge.setKillSwitch(on: true);
      const cfg = '{"inbounds":[{"type":"tun","strict_route":false}]}';
      await bridge.start(cfg);
      await Future<void>.delayed(const Duration(milliseconds: 30));

      final written = jsonDecode(bridge.lastWrittenConfig!) as Map<String, dynamic>;
      final tun = (written['inbounds'] as List)
          .firstWhere((e) => e['type'] == 'tun') as Map;
      expect(tun['strict_route'], true);

      bridge.dispose();
    });

    test('11c. setKillSwitch(on: X) 相同值时不重复重载', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);

      const cfg = '{"inbounds":[{"type":"tun","strict_route":false}]}';
      await bridge.start(cfg);
      await Future<void>.delayed(const Duration(milliseconds: 30));
      final spawnsBefore = fake.spawnCount;

      await bridge.setKillSwitch(on: false); // 已经是 false，不触发重载
      await Future<void>.delayed(const Duration(milliseconds: 30));

      expect(fake.spawnCount, spawnsBefore); // spawn 次数不变

      bridge.dispose();
    });
  });

  // ── M5: 自动退避重连 ─────────────────────────────────────────
  group('M5 auto-reconnect with backoff', () {
    test('12. 内核崩溃推 error → UI 不崩，error 事件可订阅', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);
      final events = <VpnStatus>[];

      final sub = bridge.statusStream.listen(events.add);
      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));

      fake.simulateCrash(); // 模拟内核崩溃
      await Future<void>.delayed(const Duration(milliseconds: 10));

      expect(events, contains(VpnStatus.error));
      expect(() => bridge.dispose(), returnsNormally);

      await sub.cancel();
    });

    test('13. error 后在首次退避延迟内触发重连（spawn 次数 +1）', () async {
      final fake = FakeKernelProcess(crashAfterMs: 50);
      final bridge = _TestBridge(fake);

      await bridge.start('{"log":{}}');
      final spawnsAfterStart = fake.spawnCount;

      // 等待崩溃（50ms）+ 首次退避（1s）+ buffer
      await Future<void>.delayed(const Duration(milliseconds: 1200));

      expect(fake.spawnCount, greaterThan(spawnsAfterStart),
          reason: '应该触发至少一次自动重连');

      bridge.dispose();
    });

    test('14. stop() 之后不再触发自动重连', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);

      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));
      await bridge.stop(); // 先停止

      final spawnsAfterStop = fake.spawnCount;

      fake.simulateCrash(); // 此时已 stop，不应触发重连
      await Future<void>.delayed(const Duration(milliseconds: 1200));

      expect(fake.spawnCount, equals(spawnsAfterStop),
          reason: 'stop 后崩溃不应再触发自动重连');

      bridge.dispose();
    });

    test('15. stats 流在 on 状态下转发内核事件', () async {
      final fake = FakeKernelProcess();
      final bridge = _TestBridge(fake);
      final statsReceived = <VpnStatsEvent>[];

      final sub = bridge.statsStream.listen(statsReceived.add);
      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));

      const frame = VpnStatsEvent(
        uploadBytes: 2048,
        downloadBytes: 8192,
        uploadSpeed: 1024.0,
        downloadSpeed: 4096.0,
        urltestResults: [
          UrltestResult(tag: 'hk-1', delayMs: 18),
          UrltestResult(tag: 'sg-1', delayMs: 54),
        ],
      );
      fake.emitStats(frame);
      await Future<void>.delayed(const Duration(milliseconds: 10));

      expect(statsReceived, hasLength(1));
      expect(statsReceived[0].urltestResults, hasLength(2));
      expect(statsReceived[0].urltestResults[0].tag, 'hk-1');
      expect(statsReceived[0].urltestResults[0].delayMs, 18);

      await sub.cancel();
      bridge.dispose();
    });
  });

  // ── M4: UrltestResult / VpnStatsEvent 模型 ───────────────────
  group('M4 UrltestResult model', () {
    test('fromMap / toMap 往返', () {
      const r = UrltestResult(tag: 'hk-01', delayMs: 18);
      final map = r.toMap();
      final r2 = UrltestResult.fromMap(map);
      expect(r2.tag, 'hk-01');
      expect(r2.delayMs, 18);
    });

    test('VpnStatsEvent.fromMap 解析 urltestResults 列表', () {
      final map = {
        'uploadBytes': 100,
        'downloadBytes': 200,
        'uploadSpeed': 10.0,
        'downloadSpeed': 20.0,
        'urltestResults': [
          {'tag': 'hk-1', 'delayMs': 18},
          {'tag': 'sg-1', 'delayMs': 54},
        ],
      };
      final event = VpnStatsEvent.fromMap(map);
      expect(event.urltestResults.length, 2);
      expect(event.urltestResults[0].tag, 'hk-1');
      expect(event.urltestResults[1].delayMs, 54);
    });

    test('VpnStatsEvent.fromNativeMap 处理 Map<Object?, Object?>', () {
      final rawMap = <Object?, Object?>{
        'uploadBytes': 1024,
        'downloadBytes': 2048,
        'uploadSpeed': 512.0,
        'downloadSpeed': 1024.0,
        'urltestResults': <Object?>[
          <Object?, Object?>{'tag': 'jp-1', 'delayMs': 32},
        ],
      };
      final event = VpnStatsEvent.fromNativeMap(rawMap);
      expect(event.urltestResults.length, 1);
      expect(event.urltestResults[0].tag, 'jp-1');
    });
  });
}
