// vpn_bridge_mock_test.dart — VpnBridgeMock 单元测试
//
// 验收条件（任务 tsk_iP5-_Ztq5THx）：
//   1. start → connecting → on 事件序列完整流到 Dart 侧
//   2. stop  → off 事件
//   3. stats 流在 on 状态下周期推帧（每 statsInterval）
//   4. selectOutbound 切换时产生 connecting → on 重连序列
//   5. getStatus / getActiveOutbound / setKillSwitch 返回正确值

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:pangolin_vpn/bridge/vpn_bridge.dart';
import 'package:pangolin_vpn/bridge/vpn_bridge_mock.dart';

void main() {
  // 测试用缩短延迟，避免测试耗时过长
  const connectDelay = Duration(milliseconds: 80);
  const statsInterval = Duration(milliseconds: 60);

  VpnBridgeMock newMock() => VpnBridgeMock(
        connectDelay: connectDelay,
        statsInterval: statsInterval,
      );

  // ── 1. start → connecting → on → stop → off ─────────────────
  group('status event sequence', () {
    test('start emits connecting immediately', () async {
      final mock = newMock();
      final events = <VpnStatus>[];
      final sub = mock.statusStream.listen(events.add);

      await mock.start('{}');

      expect(events, [VpnStatus.connecting]);

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

    test('start then emits on after connectDelay', () async {
      final mock = newMock();
      final events = <VpnStatus>[];
      final sub = mock.statusStream.listen(events.add);

      await mock.start('{}');
      await Future<void>.delayed(connectDelay + const Duration(milliseconds: 20));

      expect(events, containsAllInOrder([VpnStatus.connecting, VpnStatus.on]));

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

    test('stop after on emits off', () async {
      final mock = newMock();
      final events = <VpnStatus>[];
      final sub = mock.statusStream.listen(events.add);

      await mock.start('{}');
      await Future<void>.delayed(connectDelay + const Duration(milliseconds: 20));
      await mock.stop();

      expect(events.last, VpnStatus.off);

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

    test('full sequence: start→connecting→on→stop→off', () async {
      final mock = newMock();
      final events = <VpnStatus>[];
      final sub = mock.statusStream.listen(events.add);

      await mock.start('{}');
      await Future<void>.delayed(connectDelay + const Duration(milliseconds: 20));
      await mock.stop();

      expect(events, containsAllInOrder([
        VpnStatus.connecting,
        VpnStatus.on,
        VpnStatus.off,
      ]));

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

    test('stop before on transitions: connecting → off (no on emitted)', () async {
      final mock = newMock();
      final events = <VpnStatus>[];
      final sub = mock.statusStream.listen(events.add);

      await mock.start('{}');
      // Stop before connectDelay expires
      await mock.stop();

      // Should be: connecting, off — no 'on'
      expect(events, containsAllInOrder([VpnStatus.connecting, VpnStatus.off]));
      expect(events, isNot(contains(VpnStatus.on)));

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

  // ── 2. stats 流 ─────────────────────────────────────────────
  group('stats stream', () {
    test('stats stream emits frames after on transition', () async {
      final mock = newMock();
      final stats = <VpnStatsEvent>[];
      final sub = mock.statsStream.listen(stats.add);

      await mock.start('{}');
      // Wait for on + at least 2 stats ticks
      await Future<void>.delayed(
        connectDelay + statsInterval * 2 + const Duration(milliseconds: 20),
      );

      expect(stats, isNotEmpty);
      expect(stats.first.uploadSpeed, greaterThan(0));
      expect(stats.first.downloadSpeed, greaterThan(0));
      expect(stats.first.downloadBytes, greaterThan(0));

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

    test('stats stream stops after stop()', () async {
      final mock = newMock();
      final stats = <VpnStatsEvent>[];
      final sub = mock.statsStream.listen(stats.add);

      await mock.start('{}');
      await Future<void>.delayed(connectDelay + statsInterval * 2 + const Duration(milliseconds: 20));
      final countBeforeStop = stats.length;

      await mock.stop();
      await Future<void>.delayed(statsInterval * 3);
      final countAfterStop = stats.length;

      expect(countBeforeStop, greaterThan(0));
      // No new frames after stop
      expect(countAfterStop, equals(countBeforeStop));

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

    test('stats have urltestResults', () async {
      final mock = newMock();
      final stats = <VpnStatsEvent>[];
      final sub = mock.statsStream.listen(stats.add);

      await mock.start('{}');
      await Future<void>.delayed(connectDelay + statsInterval + const Duration(milliseconds: 20));

      expect(stats.first.urltestResults, isNotEmpty);
      expect(stats.first.urltestResults.first.tag, isNotEmpty);
      expect(stats.first.urltestResults.first.delayMs, greaterThan(0));

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

  // ── 3. selectOutbound ────────────────────────────────────────
  group('selectOutbound', () {
    test('selectOutbound while on triggers reconnect sequence', () async {
      final mock = newMock();
      final events = <VpnStatus>[];
      final sub = mock.statusStream.listen(events.add);

      await mock.start('{}');
      await Future<void>.delayed(connectDelay + const Duration(milliseconds: 20));

      // Clear previous events for clarity
      events.clear();

      await mock.selectOutbound('jp-01');
      await Future<void>.delayed(connectDelay + const Duration(milliseconds: 20));

      expect(events, containsAllInOrder([VpnStatus.connecting, VpnStatus.on]));

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

  // ── 4. MethodChannel stub methods ────────────────────────────
  group('stub methods', () {
    test('getStatus returns current status', () async {
      final mock = newMock();
      expect(await mock.getStatus(), VpnStatus.off);

      await mock.start('{}');
      expect(await mock.getStatus(), VpnStatus.connecting);

      await Future<void>.delayed(connectDelay + const Duration(milliseconds: 20));
      expect(await mock.getStatus(), VpnStatus.on);

      await mock.stop();
      expect(await mock.getStatus(), VpnStatus.off);

      mock.dispose();
    });

    test('getActiveOutbound returns default then updated tag', () async {
      final mock = newMock();
      expect(await mock.getActiveOutbound(), 'auto');

      await mock.selectOutbound('hk-01');
      expect(await mock.getActiveOutbound(), 'hk-01');

      mock.dispose();
    });

    test('setKillSwitch completes without error', () async {
      final mock = newMock();
      await expectLater(mock.setKillSwitch(on: true), completes);
      await expectLater(mock.setKillSwitch(on: false), completes);
      mock.dispose();
    });
  });

  // ── 5. VpnStatsEvent schema round-trip ───────────────────────
  group('VpnStatsEvent schema', () {
    test('fromMap / toMap round-trip', () {
      final original = const VpnStatsEvent(
        uploadBytes: 1024,
        downloadBytes: 4096,
        uploadSpeed: 512.0,
        downloadSpeed: 2048.0,
        urltestResults: [
          UrltestResult(tag: 'hk-01', delayMs: 18),
          UrltestResult(tag: 'jp-01', delayMs: 32),
        ],
      );
      // toMap() returns Map<String, dynamic> — same type as fromMap expects
      final decoded = VpnStatsEvent.fromMap(original.toMap());

      expect(decoded.uploadBytes, original.uploadBytes);
      expect(decoded.downloadBytes, original.downloadBytes);
      expect(decoded.uploadSpeed, original.uploadSpeed);
      expect(decoded.downloadSpeed, original.downloadSpeed);
      expect(decoded.urltestResults.length, 2);
      expect(decoded.urltestResults[0].tag, 'hk-01');
      expect(decoded.urltestResults[1].delayMs, 32);
    });

    test('VpnStatus.fromString handles all known values', () {
      expect(VpnStatus.fromString('off'), VpnStatus.off);
      expect(VpnStatus.fromString('connecting'), VpnStatus.connecting);
      expect(VpnStatus.fromString('on'), VpnStatus.on);
      expect(VpnStatus.fromString('error'), VpnStatus.error);
      expect(VpnStatus.fromString('unknown'), VpnStatus.error); // fallback
    });

    test('VpnStatus.toNativeString round-trips', () {
      for (final s in VpnStatus.values) {
        expect(VpnStatus.fromString(s.toNativeString()), s);
      }
    });
  });
}
