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

const bootstrapAssetMatrix = <String, List<String>>{
  'linux': ['oto-linux-x64.tar.gz', 'oto-linux-arm64.tar.gz'],
  'macos': ['oto-macos-x64.tar.gz', 'oto-macos-arm64.tar.gz'],
  'windows': ['oto-windows-x64.zip', 'oto-windows-arm64.zip'],
};

Iterable<String> allSupportedBootstrapAssets() sync* {
  for (final assets in bootstrapAssetMatrix.values) {
    yield* assets;
  }
}

class _BootstrapRunResult {
  final int exitCode;
  final String stdout;
  final String stderr;

  /// fake curl에 요청된 asset basename. curl이 호출되지 않으면 null.
  final String? requestedAsset;
  final String? configContent;

  const _BootstrapRunResult({
    required this.exitCode,
    required this.stdout,
    required this.stderr,
    this.requestedAsset,
    this.configContent,
  });
}

/// fake uname/curl/oto로 bootstrap script를 실행해 결과를 반환한다.
///
/// [unameS]: `uname -s` 반환값 (e.g. "Linux", "Darwin", "FreeBSD")
/// [unameM]: `uname -m` 반환값 (e.g. "x86_64", "aarch64", "s390x")
/// [provideTarGz]: true이면 curl이 fake tar.gz를 -o 경로에 제공한다.
///                 unsupported 케이스처럼 curl 미호출이 예상되면 false로 설정한다.
Future<_BootstrapRunResult> _runBootstrapWithFakeUname(
  File scriptFile, {
  required String unameS,
  required String unameM,
  bool provideTarGz = true,
  String serverUrl = 'https://server.example.com',
  String socketUrl = 'tcp://socket.example.com:18080',
  String releaseBaseUrl = 'https://example.com/release',
  String agentId = 'test-agent',
  String token = 'test-token-secret',
  int fakeOtoVersionExitCode = 0,
  bool passConnectionArgs = true,
  bool passAgentId = true,
}) async {
  final tmpDir = await Directory.systemTemp.createTemp('oto_matrix_test_');
  try {
    final tempHome = Directory('${tmpDir.path}/home');
    await tempHome.create(recursive: true);

    final fakeBinDir = Directory('${tmpDir.path}/bin');
    await fakeBinDir.create(recursive: true);

    // fake uname: -s → unameS, -m → unameM, 그 외는 시스템 uname 위임
    final fakeUname = File('${fakeBinDir.path}/uname');
    await fakeUname.writeAsString('''#!/bin/sh
case "\$1" in
  -s) echo "$unameS" ;;
  -m) echo "$unameM" ;;
  *) /usr/bin/uname "\$@" ;;
esac
''');
    await Process.run('chmod', ['+x', fakeUname.path]);

    final fakeHostname = File('${fakeBinDir.path}/hostname');
    await fakeHostname.writeAsString('''#!/bin/sh
echo "fake-device"
''');
    await Process.run('chmod', ['+x', fakeHostname.path]);

    // curl 호출 시 요청된 asset basename을 기록할 파일
    final assetLog = File('${tmpDir.path}/requested_asset.txt');

    // supported 조합에서 curl이 제공할 fake tar.gz (oto binary 포함)
    String fakeTarGzPath = '';
    if (provideTarGz) {
      final fakeOtoSrc = Directory('${tmpDir.path}/fake_oto_src');
      await fakeOtoSrc.create(recursive: true);
      final fakeOtoBin = File('${fakeOtoSrc.path}/oto');
      await fakeOtoBin.writeAsString('''#!/bin/sh
if [ "\$1" = "--version" ]; then
  echo "fake-oto-version"
  exit $fakeOtoVersionExitCode
fi
while true; do sleep 1; done
''');
      await Process.run('chmod', ['+x', fakeOtoBin.path]);
      final tarGz = File('${tmpDir.path}/fake.tar.gz');
      await Process.run('tar', [
        '-czf',
        tarGz.path,
        '-C',
        fakeOtoSrc.path,
        'oto',
      ]);
      fakeTarGzPath = tarGz.path;
    }

    // fake curl: URL basename을 assetLog에 기록하고, provideTarGz이면 -o 경로에 복사
    final fakeCurl = File('${fakeBinDir.path}/curl');
    final copyLine = provideTarGz
        ? 'if [ -n "\$out_file" ]; then cp "$fakeTarGzPath" "\$out_file"; fi'
        : '';
    await fakeCurl.writeAsString('''#!/bin/sh
url=""
out_file=""
while [ \$# -gt 0 ]; do
  case "\$1" in
    -o) out_file="\$2"; shift 2 ;;
    -*) shift ;;
    *)  url="\$1"; shift ;;
  esac
done
asset=\$(basename "\$url")
echo "\$asset" >> "${assetLog.path}"
$copyLine
''');
    await Process.run('chmod', ['+x', fakeCurl.path]);

    final env = Map<String, String>.from(Platform.environment)
      ..['PATH'] = '${fakeBinDir.path}:${Platform.environment['PATH'] ?? ''}'
      ..['HOME'] = tempHome.path;
    if (!passConnectionArgs) {
      env['OTO_BOOTSTRAP_SERVER_URL'] = serverUrl;
      env['OTO_BOOTSTRAP_SOCKET_URL'] = socketUrl;
      env['OTO_BOOTSTRAP_RELEASE_BASE_URL'] = releaseBaseUrl;
    }

    final args = <String>[
      scriptFile.absolute.path,
      if (passConnectionArgs) ...[
        '--server-url',
        serverUrl,
      ],
      if (passAgentId) ...[
        '--agent-id',
        agentId,
      ],
      '--token',
      token,
      if (passConnectionArgs) ...[
        '--release-base-url',
        releaseBaseUrl,
      ],
    ];

    final result = await Process.run('bash', args, environment: env);

    String? requestedAsset;
    if (await assetLog.exists()) {
      final raw = (await assetLog.readAsString()).trim();
      if (raw.isNotEmpty) requestedAsset = raw.split('\n').first.trim();
    }
    final configFile = File('${tempHome.path}/.oto/agent/config.yaml');
    final configContent = await configFile.exists()
        ? await configFile.readAsString()
        : null;

    // background로 실행된 fake oto 프로세스 종료
    final pidFile = File('${tempHome.path}/.oto/agent/oto-agent.pid');
    if (await pidFile.exists()) {
      final pidStr = (await pidFile.readAsString()).trim();
      final pid = int.tryParse(pidStr);
      if (pid != null) Process.killPid(pid);
    }

    return _BootstrapRunResult(
      exitCode: result.exitCode,
      stdout: result.stdout.toString(),
      stderr: result.stderr.toString(),
      requestedAsset: requestedAsset,
      configContent: configContent,
    );
  } finally {
    await tmpDir.delete(recursive: true);
  }
}

void main() {
  final scriptFile = File('assets/script/shell/oto_agent_bootstrap.sh');
  final rootReadmeFile = File('../../README.md');

  group('Linux Bootstrap Script Contract Tests', () {
    late String content;

    setUpAll(() async {
      expect(
        await scriptFile.exists(),
        isTrue,
        reason: 'Bootstrap script file must exist',
      );
      content = await scriptFile.readAsString();
    });

    test('should contain required flags and optional connection overrides', () {
      expect(content, contains('--token'));
      expect(content, contains('--agent-id'));
      expect(content, isNot(contains('--enrollment-token')));
      expect(content, contains('--server-url'));
      expect(content, contains('--release-base-url'));
    });

    test('should contain optional/default flags and paths', () {
      expect(content, contains('--agent-alias'));
      expect(content, contains('--install-dir'));
      expect(content, contains('--config-path'));
      expect(content, contains('--workspace-root'));
      expect(content, contains('--log-dir'));
      expect(content, contains('--no-background'));
      expect(content, contains('.oto/bin'));
      expect(content, contains('.oto/agent/config.yaml'));
    });

    test(
      'should support all Unix asset names via dynamic os/arch combination',
      () {
        expect(content, contains('"linux"'));
        expect(content, contains('"macos"'));
        expect(content, contains('"x64"'));
        expect(content, contains('"arm64"'));
        expect(content, contains(r'oto-${os_name}-${arch_name}.tar.gz'));
      },
    );

    test('should detect OS with uname -s and arch with uname -m', () {
      expect(content, contains('uname -s'));
      expect(content, contains('uname -m'));
      expect(content, contains('Linux'));
      expect(content, contains('Darwin'));
    });

    test('should fail with unsupported OS message', () {
      expect(content, contains('Unsupported OS:'));
    });

    test('should fail with unsupported architecture message', () {
      expect(content, contains('Unsupported architecture:'));
    });

    test(
      'should keep README bootstrap asset matrix aligned with test fixture',
      () async {
        expect(
          await rootReadmeFile.exists(),
          isTrue,
          reason: 'Root README must document bootstrap asset names',
        );
        final readmeContent = await rootReadmeFile.readAsString();

        for (final asset in allSupportedBootstrapAssets()) {
          expect(readmeContent, contains(asset));
        }
      },
    );

    test('should write expected keys to configuration yaml', () {
      expect(content, contains('agent:'));
      expect(content, contains('id:'));
      expect(content, contains('alias:'));
      expect(content, contains('enrollment_token:'));
      expect(content, contains('server:'));
      expect(content, contains('url:'));
      expect(content, contains('runtime:'));
      expect(content, contains('install_dir:'));
      expect(content, contains('workspace_root:'));
      expect(content, contains('log_dir:'));
    });

    test('should contain agent runtime commands and background behavior', () {
      expect(content, contains('agent run'));
      expect(content, contains('--config'));
      expect(content, contains('nohup'));
      expect(content, contains('oto-agent.pid'));
    });

    test('should contain binary verification step', () {
      expect(content, contains('Verifying OTO binary...'));
      expect(content, contains('--version'));
      expect(content, contains('Error: OTO binary verification failed.'));
    });
  });

  group('Bootstrap Script OS/Arch Matrix Tests', () {
    const testCases = [
      (unameS: 'Linux', unameM: 'x86_64', expected: 'oto-linux-x64.tar.gz'),
      (unameS: 'Linux', unameM: 'aarch64', expected: 'oto-linux-arm64.tar.gz'),
      (unameS: 'Darwin', unameM: 'x86_64', expected: 'oto-macos-x64.tar.gz'),
      (unameS: 'Darwin', unameM: 'arm64', expected: 'oto-macos-arm64.tar.gz'),
    ];

    for (final tc in testCases) {
      test('selects ${tc.expected} for ${tc.unameS}/${tc.unameM}', () async {
        final r = await _runBootstrapWithFakeUname(
          scriptFile,
          unameS: tc.unameS,
          unameM: tc.unameM,
          provideTarGz: true,
        );
        expect(r.exitCode, 0, reason: 'stderr: ${r.stderr}');
        expect(r.requestedAsset, tc.expected);
      });
    }

    test(
      'uses served bootstrap defaults so command only needs token',
      () async {
        final r = await _runBootstrapWithFakeUname(
          scriptFile,
          unameS: 'Darwin',
          unameM: 'arm64',
          provideTarGz: true,
          passConnectionArgs: false,
          passAgentId: false,
        );
        expect(r.exitCode, 0, reason: 'stderr: ${r.stderr}');
        expect(r.requestedAsset, 'oto-macos-arm64.tar.gz');
        expect(r.configContent, matches(RegExp(r'id: "oto-[0-9a-f]{32}"')));
        expect(r.configContent, matches(RegExp(r'alias: ".+"')));
        expect(r.configContent, contains('url: "https://server.example.com"'));
        expect(
          r.configContent,
          contains('socket_url: "tcp://socket.example.com:18080"'),
        );
      },
    );

    test('fails before download for unsupported OS (FreeBSD)', () async {
      const token = 'secret-os-token-789';
      final r = await _runBootstrapWithFakeUname(
        scriptFile,
        unameS: 'FreeBSD',
        unameM: 'x86_64',
        provideTarGz: false,
        token: token,
      );
      expect(r.exitCode, isNot(0));
      expect(r.stderr, contains('Unsupported OS:'));
      expect(
        r.requestedAsset,
        isNull,
        reason: 'download must not start for unsupported OS',
      );
      expect(r.stdout, isNot(contains(token)));
      expect(r.stderr, isNot(contains(token)));
    });

    test(
      'fails before download for unsupported arch on Linux (s390x)',
      () async {
        const token = 'secret-arch-token-456';
        final r = await _runBootstrapWithFakeUname(
          scriptFile,
          unameS: 'Linux',
          unameM: 's390x',
          provideTarGz: false,
          token: token,
        );
        expect(r.exitCode, isNot(0));
        expect(r.stderr, contains('Unsupported architecture:'));
        expect(
          r.requestedAsset,
          isNull,
          reason: 'download must not start for unsupported arch',
        );
        expect(r.stdout, isNot(contains(token)));
        expect(r.stderr, isNot(contains(token)));
      },
    );

    test(
      'fails before download for unsupported arch on macOS (i386)',
      () async {
        const token = 'secret-arch-token-123';
        final r = await _runBootstrapWithFakeUname(
          scriptFile,
          unameS: 'Darwin',
          unameM: 'i386',
          provideTarGz: false,
          token: token,
        );
        expect(r.exitCode, isNot(0));
        expect(r.stderr, contains('Unsupported architecture:'));
        expect(
          r.requestedAsset,
          isNull,
          reason: 'download must not start for unsupported arch',
        );
        expect(r.stdout, isNot(contains(token)));
        expect(r.stderr, isNot(contains(token)));
      },
    );

    test('fails when OTO binary verification fails', () async {
      const token = 'secret-verification-token-999';
      final r = await _runBootstrapWithFakeUname(
        scriptFile,
        unameS: 'Linux',
        unameM: 'x86_64',
        provideTarGz: true,
        token: token,
        fakeOtoVersionExitCode: 1,
      );
      expect(r.exitCode, isNot(0));
      expect(r.stderr, contains('Error: OTO binary verification failed.'));
      expect(r.stdout, isNot(contains(token)));
      expect(r.stderr, isNot(contains(token)));
    });

    test('escapes YAML double-quoted config values', () async {
      const agentId = r'agent-"quoted-\id';
      const token = r'token-"quoted-\secret';
      final r = await _runBootstrapWithFakeUname(
        scriptFile,
        unameS: 'Linux',
        unameM: 'x86_64',
        provideTarGz: true,
        agentId: agentId,
        token: token,
      );

      expect(r.exitCode, 0, reason: 'stderr: ${r.stderr}');
      expect(r.configContent, isNotNull);
      expect(r.configContent!, contains(r'id: "agent-\"quoted-\\id"'));
      expect(
        r.configContent!,
        contains(r'enrollment_token: "token-\"quoted-\\secret"'),
      );

      final config = AgentConfig.fromYamlContent(r.configContent!);
      expect(config.agent.id, agentId);
      expect(config.agent.enrollmentToken, token);
    });
  });

  group('Bootstrap Config Fixture', () {
    // Mirrors the exact YAML template the bootstrap script writes at runtime
    // (oto_agent_bootstrap.sh lines 141-152). If the script or AgentConfig
    // parser ever diverge on key names, this test will catch it.
    const bootstrapScriptGeneratedYaml = '''
agent:
  id: "agent-test-001"
  alias: "test-agent-alias"
  enrollment_token: "test-enrollment-token"
server:
  url: "https://oto-core.example.com"
runtime:
  install_dir: "/home/user/.oto/bin"
  workspace_root: "/home/user/.oto/workspace"
  log_dir: "/home/user/.oto/agent/log"
''';

    const bootstrapScriptGeneratedYamlEmptyAlias = '''
agent:
  id: "agent-test-002"
  alias: ""
  enrollment_token: "enrollment-token-xyz"
server:
  url: "https://oto-core.example.com"
runtime:
  install_dir: "/home/user/.oto/bin"
  workspace_root: "/home/user/.oto/workspace"
  log_dir: "/home/user/.oto/agent/log"
''';

    test('AgentConfig parses bootstrap-generated server section config', () {
      final config = AgentConfig.fromYamlContent(bootstrapScriptGeneratedYaml);
      expect(config.agent.id, 'agent-test-001');
      expect(config.agent.alias, 'test-agent-alias');
      expect(config.agent.enrollmentToken, 'test-enrollment-token');
      expect(config.server.url, 'https://oto-core.example.com');
      expect(config.runtime.installDir, '/home/user/.oto/bin');
      expect(config.runtime.workspaceRoot, '/home/user/.oto/workspace');
      expect(config.runtime.logDir, '/home/user/.oto/agent/log');
    });

    test('AgentConfig parses bootstrap-generated config with empty alias', () {
      final config = AgentConfig.fromYamlContent(
        bootstrapScriptGeneratedYamlEmptyAlias,
      );
      expect(config.agent.id, 'agent-test-002');
      expect(config.agent.alias, '');
      expect(config.agent.enrollmentToken, 'enrollment-token-xyz');
      expect(config.server.url, 'https://oto-core.example.com');
      expect(config.runtime.installDir, '/home/user/.oto/bin');
      expect(config.runtime.workspaceRoot, '/home/user/.oto/workspace');
      expect(config.runtime.logDir, '/home/user/.oto/agent/log');
    });
  });

  group('Linux Bootstrap Script Functional & Regressions Tests', () {
    test('should fail when release base URL is not HTTPS', () async {
      final result = await Process.run('bash', [
        scriptFile.absolute.path,
        '--server-url', 'https://server.example.com',
        '--token', 'secret-token-123',
        '--release-base-url', 'http://example.com/release', // HTTP URL
      ]);

      expect(result.exitCode, isNot(0));
      expect(
        result.stderr.toString(),
        contains('--release-base-url must use https://'),
      );
      expect(
        result.stdout.toString(),
        isNot(contains('Downloading OTO agent')),
      );
      // enrollment token이 출력에 노출되지 않는지 확인
      expect(result.stdout.toString(), isNot(contains('secret-token-123')));
      expect(result.stderr.toString(), isNot(contains('secret-token-123')));
    });

    test(
      'should write PID to standard agent state path when custom config-path is specified',
      () async {
        final tempDir = await Directory.systemTemp.createTemp(
          'oto_bootstrap_test_',
        );
        try {
          final tempHome = Directory('${tempDir.path}/home');
          await tempHome.create(recursive: true);

          // 1. 가짜 OTO 실행 파일 생성
          final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src');
          await fakeOtoSource.create(recursive: true);
          final fakeOtoFile = File('${fakeOtoSource.path}/oto');
          await fakeOtoFile.writeAsString('''#!/bin/sh
if [ "\$1" = "--version" ]; then
  echo "fake-oto-version"
  exit 0
fi
while true; do sleep 1; done
''');
          await Process.run('chmod', ['+x', fakeOtoFile.path]);

          // 2. 가짜 tar.gz 아카이브 생성
          final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz');
          await Process.run('tar', [
            '-czf',
            fakeTarGz.path,
            '-C',
            fakeOtoSource.path,
            'oto',
          ]);

          // 3. 가짜 curl 스크립트 생성
          final fakeBinDir = Directory('${tempDir.path}/bin');
          await fakeBinDir.create(recursive: true);
          final fakeCurl = File('${fakeBinDir.path}/curl');
          await fakeCurl.writeAsString('''#!/bin/sh
out_file=""
while [ \$# -gt 0 ]; do
  if [ "\$1" = "-o" ]; then
    out_file="\$2"
    break
  fi
  shift
done
if [ -n "\$out_file" ]; then
  cp "${fakeTarGz.path}" "\$out_file"
fi
''');
          await Process.run('chmod', ['+x', fakeCurl.path]);

          // 4. custom config path 지정
          final customConfigDir = Directory('${tempDir.path}/custom_config');
          await customConfigDir.create(recursive: true);
          final customConfigPath = '${customConfigDir.path}/my-config.yaml';

          // PATH 환경변수에 fakeBinDir 등록, HOME을 tempHome으로 설정
          final env = Map<String, String>.from(Platform.environment);
          env['PATH'] = '${fakeBinDir.path}:${env['PATH']}';
          env['HOME'] = tempHome.path;

          final result = await Process.run('bash', [
            scriptFile.absolute.path,
            '--server-url',
            'https://server.example.com',
            '--token',
            'secret-token-123',
            '--release-base-url',
            'https://example.com/release',
            '--config-path',
            customConfigPath,
          ], environment: env);

          expect(
            result.exitCode,
            0,
            reason: 'Script failed with: ${result.stderr}',
          );

          // 5. 검증
          final configFile = File(customConfigPath);
          expect(await configFile.exists(), isTrue);

          final expectedPidFile = File(
            '${tempHome.path}/.oto/agent/oto-agent.pid',
          );
          expect(
            await expectedPidFile.exists(),
            isTrue,
            reason: 'PID file should exist in standard agent state path',
          );

          final unexpectedPidFile = File(
            '${customConfigDir.path}/oto-agent.pid',
          );
          expect(
            await unexpectedPidFile.exists(),
            isFalse,
            reason: 'PID file should not exist in custom config path directory',
          );

          final pidStr = await expectedPidFile.readAsString();
          final pid = int.tryParse(pidStr.trim());
          if (pid != null) {
            Process.killPid(pid);
          }
        } finally {
          await tempDir.delete(recursive: true);
        }
      },
    );

    test('should support --edge-url as alias compatibility', () async {
      final tempDir = await Directory.systemTemp.createTemp(
        'oto_bootstrap_alias_test_',
      );
      try {
        final tempHome = Directory('${tempDir.path}/home');
        await tempHome.create(recursive: true);

        // 1. 가짜 OTO 실행 파일 생성
        final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src');
        await fakeOtoSource.create(recursive: true);
        final fakeOtoFile = File('${fakeOtoSource.path}/oto');
        await fakeOtoFile.writeAsString('''#!/bin/sh
if [ "\$1" = "--version" ]; then
  echo "fake-oto-version"
  exit 0
fi
while true; do sleep 1; done
''');
        await Process.run('chmod', ['+x', fakeOtoFile.path]);

        // 2. 가짜 tar.gz 아카이브 생성
        final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz');
        await Process.run('tar', [
          '-czf',
          fakeTarGz.path,
          '-C',
          fakeOtoSource.path,
          'oto',
        ]);

        // 3. 가짜 curl 스크립트 생성
        final fakeBinDir = Directory('${tempDir.path}/bin');
        await fakeBinDir.create(recursive: true);
        final fakeCurl = File('${fakeBinDir.path}/curl');
        await fakeCurl.writeAsString('''#!/bin/sh
out_file=""
while [ \$# -gt 0 ]; do
  if [ "\$1" = "-o" ]; then
    out_file="\$2"
    break
  fi
  shift
done
if [ -n "\$out_file" ]; then
  cp "${fakeTarGz.path}" "\$out_file"
fi
''');
        await Process.run('chmod', ['+x', fakeCurl.path]);

        final customConfigPath = '${tempDir.path}/alias-config.yaml';

        final env = Map<String, String>.from(Platform.environment);
        env['PATH'] = '${fakeBinDir.path}:${env['PATH']}';
        env['HOME'] = tempHome.path;

        final result = await Process.run('bash', [
          scriptFile.absolute.path,
          '--edge-url',
          'https://edge.example.com',
          '--token',
          'secret-token-123',
          '--release-base-url',
          'https://example.com/release',
          '--config-path',
          customConfigPath,
        ], environment: env);

        expect(
          result.exitCode,
          0,
          reason: 'Script failed with: \${result.stderr}',
        );

        final configFile = File(customConfigPath);
        expect(await configFile.exists(), isTrue);
        final configContent = await configFile.readAsString();
        expect(configContent, contains('server:'));
        expect(configContent, contains('url: "https://edge.example.com"'));

        final expectedPidFile = File(
          '${tempHome.path}/.oto/agent/oto-agent.pid',
        );
        expect(await expectedPidFile.exists(), isTrue);
        final pidStr = await expectedPidFile.readAsString();
        final pid = int.tryParse(pidStr.trim());
        if (pid != null) {
          Process.killPid(pid);
        }
      } finally {
        await tempDir.delete(recursive: true);
      }
    });
  });
}
