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

import 'package:test/test.dart';
import 'package:oto/oto/agent/agent_config.dart';
import 'package:oto/oto/agent/edge_registration_client.dart';

const _host = '127.0.0.1';
const _token = 'oto-smoke-token';
const _nodeId = 'oto-smoke-node';
const _nodeAlias = 'oto-smoke';

void main() {
  test('OTO Dart proto-socket client registers with iop Edge', () async {
    final edgePort = await _freePort();
    final metricsPort = await _freePort();
    final workDir = await Directory.systemTemp.createTemp('oto-iop-smoke-');
    final config = File('${workDir.path}/edge.yaml');
    await config
        .writeAsString(_edgeConfig(edgePort, metricsPort, workDir.path));

    final edge = await Process.start(
      'go',
      [
        'run',
        './apps/edge/cmd/edge',
        'serve',
        '--config',
        config.path,
      ],
      workingDirectory: '../iop',
      environment: {
        'GOCACHE': '${workDir.path}/gocache',
      },
    );

    final output = StringBuffer();
    final stdoutSub = edge.stdout
        .transform(systemEncoding.decoder)
        .listen(output.write, onError: output.write);
    final stderrSub = edge.stderr
        .transform(systemEncoding.decoder)
        .listen(output.write, onError: output.write);

    try {
      await _waitForPort(_host, edgePort, edge, output);

      final agentConfig = AgentConfig(
        agent: const AgentIdentityConfig(
          id: _nodeId,
          alias: _nodeAlias,
          enrollmentToken: _token,
        ),
        edge: EdgeConnectionConfig(
          url: '$_host:$edgePort',
        ),
        runtime: AgentRuntimeConfig(
          installDir: '${workDir.path}/install',
          workspaceRoot: '${workDir.path}/workspace',
          logDir: '${workDir.path}/log',
        ),
      );

      final client = EdgeRegistrationClient();
      final result = await client.register(agentConfig);

      expect(result.accepted, isTrue);
      expect(result.nodeId, _nodeId);
      expect(result.alias, _nodeAlias);
      expect(result.runtimeConfig, isNotNull);
      expect(result.runtimeConfig!['concurrency'], 1);
      expect(
          result.runtimeConfig!['workspaceRoot'], '${workDir.path}/workspace');
    } finally {
      edge.kill(ProcessSignal.sigterm);
      await edge.exitCode.timeout(
        const Duration(seconds: 5),
        onTimeout: () {
          edge.kill(ProcessSignal.sigkill);
          return edge.exitCode;
        },
      );
      await stdoutSub.cancel();
      await stderrSub.cancel();
      await workDir.delete(recursive: true);
    }
  }, timeout: const Timeout(Duration(seconds: 45)));
}

Future<int> _freePort() async {
  final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
  final port = socket.port;
  await socket.close();
  return port;
}

Future<void> _waitForPort(
  String host,
  int port,
  Process process,
  StringBuffer output,
) async {
  final deadline = DateTime.now().add(const Duration(seconds: 20));
  while (DateTime.now().isBefore(deadline)) {
    final exit = await _tryExitCode(process);
    if (exit != null) {
      fail('iop Edge exited before listening (code $exit):\n$output');
    }
    try {
      final socket = await Socket.connect(
        host,
        port,
        timeout: const Duration(milliseconds: 200),
      );
      await socket.close();
      return;
    } catch (_) {
      await Future<void>.delayed(const Duration(milliseconds: 100));
    }
  }
  fail('timed out waiting for iop Edge on $host:$port:\n$output');
}

Future<int?> _tryExitCode(Process process) {
  return process.exitCode
      .timeout(
        Duration.zero,
        onTimeout: () => -1,
      )
      .then((code) => code == -1 ? null : code);
}

String _edgeConfig(int edgePort, int metricsPort, String root) {
  return '''
edge:
  id: "oto-smoke-edge"
  name: "OTO Smoke Edge"

server:
  listen: "$_host:$edgePort"

tls:
  enabled: false

logging:
  level: "error"
  pretty: false

metrics:
  port: $metricsPort

openai:
  enabled: false

a2a:
  enabled: false

console:
  adapter: "oto"
  target: "pipeline"
  session_id: "default"
  background: false
  timeout_sec: 30

nodes:
  - id: "$_nodeId"
    alias: "$_nodeAlias"
    token: "$_token"
    adapters:
      cli:
        enabled: false
    runtime:
      concurrency: 1
      workspace_root: "$root/workspace"
''';
}
