// Copyright (c) 2022, 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.

import 'dart:developer';

import 'package:expect/expect.dart';

import 'package:path/path.dart' as path;
import 'heap_snapshot_test.dart';
import 'use_flag_test_helper.dart';

@pragma('vm:entry-point')
class FooBar {
  @pragma('vm:entry-point')
  BarBaz bar;
  int value;
  @pragma('vm:entry-point')
  List<int> bar2;
  FooBar(this.bar, this.value, this.bar2);

  String toString() => 'FooBar($bar, $value, $bar2)';
}

@pragma('vm:entry-point')
class BarBaz {
  final int value;
  BarBaz(this.value);
  String toString() => 'BarBaz($value)';
}

@pragma('vm:entry-point')
var global;

main() async {
  if (const bool.fromEnvironment('dart.vm.product')) return;

  await withTempDir('heap_snapshot_test', (String dir) async {
    final file = path.join(dir, 'state1.heapsnapshot');

    // Closures are the example this is a regresion test for, so ensure there's
    // a reachable closure in the heapsnapshot.
    global = alwaysTrue ? FooBar(BarBaz(1), 2, List.filled(1, 3)) : null;
    NativeRuntime.writeHeapSnapshotToFile(file);
    Expect.equals('FooBar(BarBaz(1), 2, [3])', global.toString());

    final snapshot = loadHeapSnapshotFromFile(file);
    final classes = snapshot.classes;

    // Ensure all objects have same number of references as fields, except for
    // variable-length lists.
    final reachableObjects = findReachableObjects(snapshot);
    for (final object in reachableObjects) {
      final klass = snapshot.classes[object.classId];
      final uri = klass.libraryUri.toString();
      final fields = klass.fields;

      // Variable-length data may have more references than fields.
      if (uri == '') {
        // We don't verify non-user-visible objects.
      } else if (uri.startsWith('dart') &&
          [
            'Array',
            'Closure',
            'List',
            'Record',
          ].any((p) => klass.name.contains(p))) {
        Expect.isTrue(fields.length <= object.references.length);
      } else {
        // For objects with vm-defined layouts, this fails if a new field is
        // added but runtime/vm/raw_object_fields.cc is not updated accordingly.
        Expect.equals(fields.length, object.references.length, klass.name);
      }
    }

    // Specifically look at layout and references of [FooBar] class.
    final fooClass = classes.singleWhere((c) => c.name == 'FooBar');
    final barClass = classes.singleWhere((c) => c.name == 'BarBaz');
    final listClass = classes.singleWhere((c) => c.name == '_List');

    final barField = fooClass.fields.singleWhere((f) => f.name == 'bar');
    final bar2Field = fooClass.fields.singleWhere((f) => f.name == 'bar2');

    // There should be one instance of [FooBar].
    final fooObject = reachableObjects
        .where((object) => object.classId == fooClass.classId)
        .single;
    final fooObjectBarField =
        snapshot.objects[fooObject.references[barField.index]];
    final fooObjectBar2Field =
        snapshot.objects[fooObject.references[bar2Field.index]];
    Expect.equals(3, fooObject.references.length);
    Expect.equals(0, barField.index);
    Expect.equals(2, bar2Field.index);
    Expect.equals(barClass.classId, fooObjectBarField.classId);
    Expect.equals(listClass.classId, fooObjectBar2Field.classId);
    Expect.equals(
      listClass.fields.length + 1,
      fooObjectBar2Field.references.length,
    );
  });
}
