// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Helper methods and definitions used in the use_dwarf_stack_traces_flag tests.

import "dart:async";
import "dart:convert";
import "dart:io";

import 'package:native_stack_traces/native_stack_traces.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';

import 'use_flag_test_helper.dart';

/// Returns false if tests involving assembly snapshots should be run
/// and a String describing why the tests should be skipped otherwise.
Object get skipAssembly {
  // Currently there are no appropriate buildtools on the simulator trybots as
  // normally they compile to ELF and don't need them for compiling assembly
  // snapshots.
  if (isSimulator) {
    return "running on a simulated architecture";
  }
  return (Platform.isLinux || Platform.isMacOS || Platform.isWindows)
      ? false
      : "no process for assembling snapshots on this platform";
}

/// Returns false if tests involving MacOS universal binaries should be run
/// and a String describing why the tests should be skipped otherwise.
Object get skipUniversalBinary {
  final assemblySkipped = skipAssembly;
  if (assemblySkipped != false) return assemblySkipped;
  return Platform.isMacOS ? false : "only valid for MacOS";
}

typedef Trace = List<String>;

class DwarfTestOutput {
  final Trace trace;
  final int allocateObjectStart;
  final int allocateObjectEnd;

  DwarfTestOutput(this.trace, this.allocateObjectStart, this.allocateObjectEnd);
}

abstract class State {
  final DwarfTestOutput output;
  final DwarfTestOutput outputWithOppositeFlag;

  State(this.output, this.outputWithOppositeFlag);
}

class NonDwarfState extends State {
  NonDwarfState(super.output, super.outputWithOppositeFlag);

  void check() => expect(outputWithOppositeFlag.trace, equals(output.trace));
}

abstract class DwarfState<T> extends State {
  final T snapshot;
  final T debugInfo;
  DwarfState(
    super.output,
    super.outputWithOppositeFlag,
    this.snapshot,
    this.debugInfo,
  );

  String get description;

  Future<void> check(Trace trace, T t);

  Future<void> makeTests(Trace nonDwarfTrace) async {
    test(
      'Testing $description traces with separate debugging info',
      () async => await check(nonDwarfTrace, debugInfo),
    );

    test(
      'Testing $description traces with original snapshot',
      () async => await check(nonDwarfTrace, snapshot),
    );
  }
}

abstract class ElfState<T> extends DwarfState<T> {
  ElfState(
    super.output,
    super.outputWithOppositeFlag,
    super.snapshot,
    super.debugInfo,
  );

  @override
  String get description => 'ELF';
}

abstract class MultiArchDwarfState<T> extends DwarfState<T> {
  final T? singleArch;
  final T? multiArch;

  MultiArchDwarfState(
    super.output,
    super.outputWithOppositeFlag,
    super.snapshot,
    super.debugInfo, [
    this.singleArch,
    this.multiArch,
  ]);

  @override
  Future<void> makeTests(Trace nonDwarfTrace) async {
    await super.makeTests(nonDwarfTrace);

    test(
      'Testing $description single-architecture universal binary',
      () async {
        expect(singleArch, isNotNull);
        await check(nonDwarfTrace, singleArch!);
      },
      skip: skipUniversalBinary,
    );

    test(
      'Testing $description multi-architecture universal binary',
      () async {
        expect(multiArch, isNotNull);
        await check(nonDwarfTrace, multiArch!);
      },
      skip: skipUniversalBinary,
    );
  }
}

abstract class AssemblyState<T> extends MultiArchDwarfState<T> {
  AssemblyState(
    super.output,
    super.outputWithOppositeFlag,
    super.snapshot,
    super.debugInfo, [
    super.singleArch,
    super.multiArch,
  ]);

  @override
  String get description => 'assembly';
}

abstract class MachOState<T> extends MultiArchDwarfState<T> {
  MachOState(
    super.output,
    super.outputWithOppositeFlag,
    super.snapshot,
    super.debugInfo, [
    super.singleArch,
    super.multiArch,
  ]);

  @override
  String get description => 'Mach-O';
}

Future<void> runTests<T>(
  String tempPrefix,
  String scriptPath,
  Future<NonDwarfState> Function(String, String) runNonDwarf,
  Iterable<Future<DwarfState?> Function(String, String)> runDwarfs,
) async {
  if (!isAOTRuntime) {
    return; // Running in JIT: AOT binaries not available.
  }

  if (Platform.isAndroid) {
    return; // SDK tree and dart_bootstrap not available on the test device.
  }

  // These are the tools we need to be available to run on a given platform:
  if (!await testExecutable(genSnapshot)) {
    throw "Cannot run test as $genSnapshot not available";
  }
  if (!await testExecutable(dartPrecompiledRuntime)) {
    throw "Cannot run test as $dartPrecompiledRuntime not available";
  }
  if (!File(platformDill).existsSync()) {
    throw "Cannot run test as $platformDill does not exist";
  }

  await withTempDir(tempPrefix, (String tempDir) async {
    // We have to use the program in its original location so it can use
    // the dart:_internal library (as opposed to adding it as an OtherResources
    // option to the test).
    final scriptDill = path.join(tempDir, 'flag_program.dill');

    // Compile script to Kernel IR.
    await run(genKernel, <String>[
      '--aot',
      '--platform=$platformDill',
      '-o',
      scriptDill,
      scriptPath,
    ]);

    final nonDwarfState = await runNonDwarf(tempDir, scriptDill);
    final dwarfStates = [
      for (final f in runDwarfs) await f(tempDir, scriptDill),
    ];

    test('Testing symbolic traces', nonDwarfState.check);

    final nonDwarfTrace = nonDwarfState.output.trace;

    for (final dwarfState in dwarfStates.whereType<DwarfState>()) {
      dwarfState.makeTests(nonDwarfTrace);
    }
  });
}

void checkHeader(StackTraceHeader header) {
  // These should be all available.
  expect(header.isolateStart, isNotNull);
  expect(header.isolateDsoBase, isNotNull);
  expect(header.buildId, isNotNull);
  expect(header.os, isNotNull);
  expect(header.architecture, isNotNull);
  expect(header.usingSimulator, isNotNull);
  expect(header.compressedPointers, isNotNull);
}

void checkRootUnitAssumptions(
  DwarfTestOutput output1,
  DwarfTestOutput output2,
  Dwarf rootDwarf, {
  required PCOffset sampleOffset,
  bool matchingBuildIds = true,
}) {
  // We run the test program on the same host OS as the test, so any
  // PCOffset from the trace should have this information.
  expect(sampleOffset.os, isNotNull);
  expect(sampleOffset.architecture, isNotNull);
  expect(sampleOffset.usingSimulator, isNotNull);
  expect(sampleOffset.compressedPointers, isNotNull);

  expect(sampleOffset.os, equals(Platform.operatingSystem));
  final archString =
      '${sampleOffset.usingSimulator! ? 'SIM' : ''}'
      '${sampleOffset.architecture!.toUpperCase()}'
      '${sampleOffset.compressedPointers! ? 'C' : ''}';
  final baseBuildDir = path.basename(buildDir);
  expect(baseBuildDir, endsWith(archString));

  // Check that the build IDs exist in the traces and are the same.
  final buildId1 = buildId(output1.trace);
  expect(buildId1, isNotEmpty);
  print('Trace 1 build ID: "${buildId1}"');
  final buildId2 = buildId(output2.trace);
  expect(buildId2, isNotEmpty);
  print('Trace 2 build ID: "${buildId2}"');
  expect(buildId2, equals(buildId1));

  if (matchingBuildIds) {
    // The build ID in the traces should be the same as the DWARF build ID
    // when the ELF was generated by gen_snapshot.
    final dwarfBuildId = rootDwarf.buildId();
    expect(dwarfBuildId, isNotNull);
    print('Dwarf build ID: "${dwarfBuildId!}"');
    // We should never generate an all-zero build ID.
    expect(dwarfBuildId, isNot("00000000000000000000000000000000"));
    // This is a common failure case as well, when HashBitsContainer ends up
    // hashing over seemingly empty sections.
    expect(dwarfBuildId, isNot("01000000010000000100000001000000"));
    expect(buildId1, equals(dwarfBuildId));
    expect(buildId2, equals(dwarfBuildId));
  }

  final allocateObjectStart = output1.allocateObjectStart;
  final allocateObjectEnd = output1.allocateObjectEnd;
  expect(output2.allocateObjectStart, equals(allocateObjectStart));
  expect(output2.allocateObjectEnd, equals(allocateObjectEnd));

  checkAllocateObjectOffset(rootDwarf, allocateObjectStart);
  // The end of the bare instructions payload may be padded up to word size,
  // so check the maximum possible word size (64 bits) before the end.
  checkAllocateObjectOffset(rootDwarf, allocateObjectEnd - 8);
  // The end should be either in a different stub or not a stub altogether.
  checkAllocateObjectOffset(rootDwarf, allocateObjectEnd, expectedValue: false);
  // The byte before the start should also be in either a different stub or
  // not in a stub altogether.
  checkAllocateObjectOffset(
    rootDwarf,
    allocateObjectStart - 1,
    expectedValue: false,
  );
  // Check the midpoint of the stub, as the stub should be large enough that the
  // midpoint won't be in any possible padding.
  expect(
    allocateObjectEnd - allocateObjectStart,
    greaterThanOrEqualTo(16),
    reason: 'midpoint of stub may be in bare payload padding',
  );
  checkAllocateObjectOffset(
    rootDwarf,
    (allocateObjectStart + allocateObjectEnd) ~/ 2,
  );

  print("Successfully matched AllocateObject stub addresses");
}

void checkAllocateObjectOffset(
  Dwarf dwarf,
  int offset, {
  bool expectedValue = true,
}) {
  final pcOffset = PCOffset(offset, InstructionsSection.isolate);
  print('Offset of tested stub address is $pcOffset');
  final callInfo = dwarf.callInfoForPCOffset(
    pcOffset,
    includeInternalFrames: true,
  );
  print('Call info for tested stub address is $callInfo');
  final got =
      callInfo != null &&
      callInfo.length == 1 &&
      callInfo.single is StubCallInfo &&
      (callInfo.single as StubCallInfo).name.endsWith('stub AllocateObject');
  expect(
    got,
    equals(expectedValue),
    reason:
        'address is ${expectedValue ? 'not within' : 'within'} '
        'the AllocateObject stub',
  );
}

void checkTranslatedTrace(List<String> nonDwarfTrace, List<String> dwarfTrace) {
  final translatedStackFrames = onlySymbolicFrameLines(dwarfTrace);
  final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace);

  print('Stack frames from translated non-symbolic stack trace:');
  print(translatedStackFrames.join('\n'));

  print('Stack frames from original symbolic stack trace:');
  print(originalStackFrames.join('\n'));

  expect(translatedStackFrames, isNotEmpty);
  expect(originalStackFrames, isNotEmpty);

  // In symbolic mode, we don't store column information to avoid an increase
  // in size of CodeStackMaps. Thus, we need to strip any columns from the
  // translated non-symbolic stack to compare them via equality.
  final columnStrippedTranslated = removeColumns(translatedStackFrames);

  print('Stack frames from translated non-symbolic stack trace, no columns:');
  print(columnStrippedTranslated.join('\n'));

  expect(columnStrippedTranslated, equals(originalStackFrames));
}

Future<DwarfTestOutput> runTestProgram(
  String executable,
  List<String> args,
) async {
  final result = await runHelper(executable, args);

  if (result.exitCode == 0) {
    throw 'Command did not fail with non-zero exit code';
  }
  if (result.stdout.isEmpty) {
    throw 'Command did not print a stacktrace';
  }

  final stdoutLines = LineSplitter.split(result.stdout).toList();
  if (result.stdout.length < 2) {
    throw 'Command did not print both absolute addresses for stub range';
  }
  final start = int.parse(stdoutLines[0]);
  final end = int.parse(stdoutLines[1]);

  return DwarfTestOutput(
    LineSplitter.split(result.stderr).toList(),
    start,
    end,
  );
}

final _buildIdRE = RegExp(r"build_id: '([a-f\d]+)'");
String buildId(Iterable<String> lines) {
  for (final line in lines) {
    final match = _buildIdRE.firstMatch(line);
    if (match != null) {
      return match[1]!;
    }
  }
  return '';
}

final _symbolicFrameRE = RegExp(r'^#\d+\s+');

Iterable<String> onlySymbolicFrameLines(Iterable<String> lines) {
  return lines.where((line) => _symbolicFrameRE.hasMatch(line));
}

final _columnsRE = RegExp(r'[(](.*:\d+):\d+[)]');

Iterable<String> removeColumns(Iterable<String> lines) sync* {
  for (final line in lines) {
    final match = _columnsRE.firstMatch(line);
    if (match != null) {
      yield line.replaceRange(match.start, match.end, '(${match[1]!})');
    } else {
      yield line;
    }
  }
}

Iterable<int> parseUsingAddressRegExp(RegExp re, Iterable<String> lines) sync* {
  for (final line in lines) {
    final match = re.firstMatch(line);
    if (match != null) {
      yield int.parse(match[1]!, radix: 16);
    }
  }
}

final _absRE = RegExp(r'abs ([a-f\d]+)');

Iterable<int> absoluteAddresses(Iterable<String> lines) =>
    parseUsingAddressRegExp(_absRE, lines);

final _virtRE = RegExp(r'virt ([a-f\d]+)');

Iterable<int> explicitVirtualAddresses(Iterable<String> lines) =>
    parseUsingAddressRegExp(_virtRE, lines);

final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)');

Iterable<int> dsoBaseAddresses(Iterable<String> lines) =>
    parseUsingAddressRegExp(_dsoBaseRE, lines);

// We only list architectures supported by the current CpuType enum in
// pkg:native_stack_traces/src/macho.dart.
const machOArchNames = <String, String>{
  "ARM": "arm",
  "ARM64": "arm64",
  "IA32": "ia32",
  "X64": "x64",
};

String? get dartNameForCurrentArchitecture {
  for (final entry in machOArchNames.entries) {
    if (buildDir.endsWith(entry.key)) {
      return entry.value;
    }
  }
  return null;
}
