// kernel_process_test.dart — DesktopKernelProcess / DesktopVpnBridge 单元测试
//
// 验收条件（tsk_SLCsjNgtmng3）：
//   1. ClashApiClient 构造参数正确存储
//   2. ClashApiClient.getConnections 请求正确（headers、解析）
//   3. ClashApiClient.selectProxy 发送 PUT + 正确 body
//   4. ClashApiClient.closeAllConnections 发送 DELETE
//   5. generateClashApiPort 生成合法高位端口（49152-65535）
//   6. generateClashApiSecret 生成 64 位 hex
//   7. DesktopVpnBridge.injectClashApi 注入 Clash API 段（随机端口+secret）
//   8. DesktopVpnBridge.injectClashApi 尊重已有 clash_api 配置
//   9. DesktopVpnBridge + FakeKernelProcess: start 触发 connecting→on 事件序列
//  10. DesktopVpnBridge: stop 触发 off
//  11. DesktopVpnBridge: statsStream 转发 kernel 事件
//  12. 意外退出推 error 状态（UI 不崩）
//  13. spawn 失败推 error 状态并抛异常

// 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: implements KernelProcess，不依赖真实子进程
// ══════════════════════════════════════════════════════════════════

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

  final bool shouldFailSpawn;
  final bool simulateUnexpectedExit;

  bool _running = false;
  String? lastConfigPath;
  int killCalls = 0;

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

  @override
  bool get isRunning => _running;

  @override
  ClashApiClient get clashApiClient =>
      throw UnimplementedError('FakeKernelProcess.clashApiClient not needed in tests');

  @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;
    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 (simulateUnexpectedExit) {
      Future<void>.delayed(const Duration(milliseconds: 60)).then((_) {
        if (_running) {
          _running = false;
          _emitStatus(VpnStatus.error);
        }
      });
    }
  }

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

  // 测试辅助：向外注入统计帧
  void emitStats(VpnStatsEvent e) {
    if (!_statsCtrl.isClosed) _statsCtrl.add(e);
  }

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

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

// ══════════════════════════════════════════════════════════════════
// 测试用 DesktopVpnBridge：覆盖 writeConfig，避免写真实文件系统
// ══════════════════════════════════════════════════════════════════

class _TestBridge extends DesktopVpnBridge {
  _TestBridge(FakeKernelProcess kernel)
      : fake = kernel,
        super(kernel: kernel);

  final FakeKernelProcess fake;
  String? lastWrittenConfig;

  @override
  Future<String> writeConfig(String configJson) async {
    lastWrittenConfig = configJson;
    // 写到 systemTemp，避免权限问题（测试后清理）
    final tmp = Directory.systemTemp.createTempSync('pangolin_test_');
    final f = File('${tmp.path}/config.json')
      ..writeAsStringSync(configJson);
    return f.path;
  }

  @override
  void dispose() {
    fake.disposeFake();
  }
}

// ══════════════════════════════════════════════════════════════════
// 单元测试
// ══════════════════════════════════════════════════════════════════

void main() {
  // ── 1. ClashApiClient ─────────────────────────────────────────
  group('ClashApiClient', () {
    test('stores baseUrl and secret', () {
      const url = 'http://127.0.0.1:51234';
      const secret = 'my-secret';
      final c = ClashApiClient(baseUrl: url, secret: secret);
      expect(c.baseUrl, url);
      expect(c.secret, secret);
      c.dispose();
    });

    test('getConnections sends Authorization header', () async {
      final requests = <http.Request>[];
      final mock = MockClient((req) async {
        requests.add(req);
        return http.Response(
          jsonEncode({'downloadTotal': 100, 'uploadTotal': 50, 'connections': []}),
          200,
        );
      });

      final c = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090',
          secret: 'tok',
          httpClient: mock);
      await c.getConnections();

      expect(requests, hasLength(1));
      expect(requests[0].headers['Authorization'], 'Bearer tok');
      expect(requests[0].url.path, '/connections');
      c.dispose();
    });

    test('getConnections parses downloadTotal and uploadTotal', () async {
      final mock = MockClient((_) async => http.Response(
            jsonEncode({'downloadTotal': 12345, 'uploadTotal': 6789, 'connections': []}),
            200,
          ));

      final c = ClashApiClient(baseUrl: 'http://127.0.0.1:9090', httpClient: mock);
      final data = await c.getConnections();

      expect(data['downloadTotal'], 12345);
      expect(data['uploadTotal'], 6789);
      c.dispose();
    });

    test('getConnections throws HttpException on non-200', () async {
      final mock = MockClient((_) async => http.Response('Unauthorized', 401));
      final c = ClashApiClient(
          baseUrl: 'http://127.0.0.1:9090', secret: 'bad', httpClient: mock);

      await expectLater(c.getConnections(), throwsA(isA<HttpException>()));
      c.dispose();
    });

    test('getProxies sends correct GET request', () async {
      final mock = MockClient((req) async {
        expect(req.method, 'GET');
        expect(req.url.path, '/proxies');
        return http.Response(jsonEncode({'proxies': {}}), 200);
      });
      final c = ClashApiClient(baseUrl: 'http://127.0.0.1:9090', httpClient: mock);
      await c.getProxies();
      c.dispose();
    });

    test('selectProxy sends PUT with correct body', () async {
      Map<String, dynamic>? body;
      final mock = MockClient((req) async {
        expect(req.method, 'PUT');
        expect(req.url.path, '/proxies/my-group');
        body = jsonDecode(req.body) as Map<String, dynamic>;
        return http.Response('', 204);
      });
      final c = ClashApiClient(baseUrl: 'http://127.0.0.1:9090', httpClient: mock);
      await c.selectProxy('my-group', 'sg-01');
      expect(body?['name'], 'sg-01');
      c.dispose();
    });

    test('closeAllConnections sends DELETE', () async {
      final mock = MockClient((req) async {
        expect(req.method, 'DELETE');
        expect(req.url.path, '/connections');
        return http.Response('', 204);
      });
      final c = ClashApiClient(baseUrl: 'http://127.0.0.1:9090', httpClient: mock);
      await c.closeAllConnections();
      c.dispose();
    });
  });

  // ── 2. 随机生成工具函数 ───────────────────────────────────────
  group('Port and secret generators', () {
    test('generateClashApiPort is in range 49152–65535', () {
      for (var i = 0; i < 30; i++) {
        final p = generateClashApiPort();
        expect(p, greaterThanOrEqualTo(49152));
        expect(p, lessThanOrEqualTo(65535));
      }
    });

    test('generateClashApiSecret is 64 lowercase hex chars', () {
      final s = generateClashApiSecret();
      expect(s.length, 64);
      expect(RegExp(r'^[0-9a-f]+$').hasMatch(s), isTrue);
    });

    test('generateClashApiSecret produces unique values', () {
      final a = generateClashApiSecret();
      final b = generateClashApiSecret();
      expect(a, isNot(equals(b)));
    });
  });

  // ── 3. DesktopVpnBridge.injectClashApi ───────────────────────
  group('DesktopVpnBridge.injectClashApi', () {
    test('injects clash_api when absent', () {
      const cfg = '{"log":{"level":"info"}}';
      final (json, port, secret) = DesktopVpnBridge.injectClashApi(cfg);

      final decoded = jsonDecode(json) as Map<String, dynamic>;
      final exp = decoded['experimental'] as Map<String, dynamic>;
      final api = exp['clash_api'] as Map<String, dynamic>;

      final ctrl = api['external_controller'] as String;
      expect(ctrl, startsWith('127.0.0.1:'));
      expect(int.parse(ctrl.split(':')[1]),
          allOf(greaterThanOrEqualTo(49152), lessThanOrEqualTo(65535)));
      expect(api['secret'], isA<String>());
      expect((api['secret'] as String).length, 64);

      expect(port, greaterThanOrEqualTo(49152));
      expect(secret.length, 64);
    });

    test('respects existing clash_api config', () {
      const existing = '{'
          '"experimental":{"clash_api":{"external_controller":"127.0.0.1:12345","secret":"abc"}}'
          '}';
      final (json, port, secret) = DesktopVpnBridge.injectClashApi(existing);

      // Should return original JSON unchanged
      expect(json, equals(existing));
      expect(port, 12345);
      expect(secret, 'abc');
    });

    test('preserves other config fields', () {
      const cfg = '{"log":{"level":"info"},"dns":{"servers":[]}}';
      final (json, _, _) = DesktopVpnBridge.injectClashApi(cfg);
      final decoded = jsonDecode(json) as Map<String, dynamic>;

      expect((decoded['log'] as Map)['level'], 'info');
      expect(decoded['dns'], isA<Map>());
    });

    test('throws FormatException on invalid JSON', () {
      expect(
        () => DesktopVpnBridge.injectClashApi('not json'),
        throwsA(isA<FormatException>()),
      );
    });

    test('preserves existing experimental fields when injecting clash_api', () {
      const cfg = '{"experimental":{"cache_file":{"enabled":true}}}';
      final (json, _, _) = DesktopVpnBridge.injectClashApi(cfg);
      final decoded = jsonDecode(json) as Map<String, dynamic>;
      final exp = decoded['experimental'] as Map<String, dynamic>;

      // cache_file should be preserved
      expect(exp.containsKey('cache_file'), isTrue);
      // clash_api should be injected
      expect(exp.containsKey('clash_api'), isTrue);
    });
  });

  // ── 4. DesktopVpnBridge + FakeKernelProcess ──────────────────
  group('DesktopVpnBridge with FakeKernelProcess', () {
    _TestBridge makeBridge({
      bool failSpawn = false,
      bool unexpectedExit = false,
    }) {
      final fake = FakeKernelProcess(
        shouldFailSpawn: failSpawn,
        simulateUnexpectedExit: unexpectedExit,
      );
      return _TestBridge(fake);
    }

    test('start emits connecting then on', () async {
      final bridge = makeBridge();
      final events = <VpnStatus>[];
      final sub = bridge.statusStream.listen(events.add);

      await bridge.start('{"log":{"level":"info"}}');
      await Future<void>.delayed(const Duration(milliseconds: 30));

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

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

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

      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 20));
      await bridge.stop();

      expect(events.last, VpnStatus.off);
      expect((bridge.fake).killCalls, 1);

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

    test('getStatus returns on while running', () async {
      final bridge = makeBridge();
      expect(await bridge.getStatus(), VpnStatus.off);

      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 20));
      expect(await bridge.getStatus(), VpnStatus.on);

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

      bridge.dispose();
    });

    test('statsStream relays events from kernel', () async {
      final bridge = makeBridge();
      final stats = <VpnStatsEvent>[];
      final sub = bridge.statsStream.listen(stats.add);

      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 20));

      const frame = VpnStatsEvent(
        uploadBytes: 1024,
        downloadBytes: 4096,
        uploadSpeed: 512.0,
        downloadSpeed: 2048.0,
        urltestResults: [],
      );
      bridge.fake.emitStats(frame);
      await Future<void>.delayed(const Duration(milliseconds: 10));

      expect(stats, hasLength(1));
      expect(stats[0].uploadBytes, 1024);
      expect(stats[0].downloadSpeed, 2048.0);

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

    test('unexpected kernel exit emits error, UI does not crash', () async {
      final bridge = makeBridge(unexpectedExit: true);
      final events = <VpnStatus>[];
      final sub = bridge.statusStream.listen(events.add);

      await bridge.start('{"log":{}}');
      await Future<void>.delayed(const Duration(milliseconds: 120));

      expect(events, contains(VpnStatus.error));
      // dispose must not throw
      expect(() => bridge.dispose(), returnsNormally);

      await sub.cancel();
    });

    test('failed spawn emits error and throws', () async {
      final bridge = makeBridge(failSpawn: true);
      final events = <VpnStatus>[];
      final sub = bridge.statusStream.listen(events.add);

      await expectLater(
        () => bridge.start('{"log":{}}'),
        throwsA(anything),
      );
      expect(events, contains(VpnStatus.error));

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

    test('injectClashApi runs during start, enriched JSON written', () async {
      final bridge = makeBridge();
      await bridge.start('{"log":{"level":"info"}}');

      final written = jsonDecode(bridge.lastWrittenConfig!) as Map<String, dynamic>;
      final exp = written['experimental'] as Map<String, dynamic>?;
      expect(exp, isNotNull);
      expect(exp!.containsKey('clash_api'), isTrue);

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

  // ── 5. KernelProcess interface ───────────────────────────────
  group('KernelProcess interface', () {
    test('FakeKernelProcess satisfies KernelProcess', () {
      // 静态类型检查：FakeKernelProcess implements KernelProcess
      final KernelProcess kp = FakeKernelProcess();
      expect(kp.isRunning, isFalse);
      expect(kp.logStream, isA<Stream<String>>());
      expect(kp.statusStream, isA<Stream<VpnStatus>>());
      expect(kp.statsStream, isA<Stream<VpnStatsEvent>>());
    });
  });
}
