// 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.

// This test verifies different corner cases around calling functions in
// Dart.
import 'package:expect/expect.dart';
import 'package:vm/testing/il_matchers.dart';

abstract class Base {
  final String o;
  Base(this.o);

  String f(String v);
  String fOptional([String v = 'ok!']);
  String fNamed({String v = 'ok!'});
  int fIntInt(int a, int b);
  double fIntDouble(int a, double b);
  double fDoubleDouble(double a, double b);
  int fIntOptionalInt(int a, [int b = 0]);
  double fIntOptionalDouble(int a, [double b = 0.0]);
  double fDoubleOptionalDouble(double a, [double b = 0.0]);
  int fIntNamedInt(int a, {int b = 0});
  double fIntNamedDouble(int a, {double b = 0.0});
  double fDoubleNamedDouble(double a, {double b = 0.0});
}

class ChildSimple extends Base {
  ChildSimple(super.o);

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  String f(String v) {
    Expect.equals('ok', o.substring(0, 2));
    Expect.equals('ok', v.substring(0, 2));
    return o + v;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  String fOptional([String v = 'ok!']) {
    Expect.equals('ok', o.substring(0, 2));
    Expect.equals('ok', v.substring(0, 2));
    return o + v;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  String fNamed({String v = 'ok!'}) {
    Expect.equals('ok', o.substring(0, 2));
    Expect.equals('ok', v.substring(0, 2));
    return o + v;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  int fIntInt(int a, int b) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fIntDouble(int a, double b) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fDoubleDouble(double a, double b) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  int fIntOptionalInt(int a, [int b = 0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fIntOptionalDouble(int a, [double b = 0.0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fDoubleOptionalDouble(double a, [double b = 0.0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  int fIntNamedInt(int a, {int b = 0}) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fIntNamedDouble(int a, {double b = 0.0}) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fDoubleNamedDouble(double a, {double b = 0.0}) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }
}

class ChildConvertingParametersToOptional extends Base {
  ChildConvertingParametersToOptional(super.o);

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  String f([String v = 'ok!']) {
    Expect.equals('ok', o.substring(0, 2));
    Expect.equals('ok', v.substring(0, 2));
    return o + v;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  String fOptional([String v = 'ok!']) {
    Expect.equals('ok', o.substring(0, 2));
    Expect.equals('ok', v.substring(0, 2));
    return o + v;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  String fNamed({String v = 'ok!'}) {
    Expect.equals('ok', o.substring(0, 2));
    Expect.equals('ok', v.substring(0, 2));
    return o + v;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  int fIntInt(int a, [int b = 0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fIntDouble(int a, [double b = 0.0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fDoubleDouble(double a, [double b = 0.0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  int fIntOptionalInt([int a = 0, int b = 0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fIntOptionalDouble([int a = 0, double b = 0.0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fDoubleOptionalDouble([double a = 0.0, double b = 0.0]) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  int fIntNamedInt(int a, {int b = 0}) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fIntNamedDouble(int a, {double b = 0.0}) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }

  @override
  @pragma('vm:never-inline')
  @pragma('vm:testing:print-flow-graph')
  double fDoubleNamedDouble(double a, {double b = 0.0}) {
    Expect.equals('ok', o.substring(0, 2));
    return a + b;
  }
}

void testDirectCalls1(String str, int ia, int ib) {
  final da = ia.toDouble();
  final db = ib.toDouble();
  final child = ChildSimple(str);

  Expect.equals("ok+ok+", child.f(str));
  Expect.equals("ok+ok!", child.fOptional());
  Expect.equals("ok+ok+", child.fOptional(str));
  Expect.equals("ok+ok!", child.fNamed());
  Expect.equals("ok+ok+", child.fNamed(v: str));
  Expect.equals(142, child.fIntInt(ia, ib));
  Expect.equals(142, child.fIntDouble(ia, db));
  Expect.equals(142, child.fDoubleDouble(da, db));
  Expect.equals(42, child.fIntOptionalInt(ia));
  Expect.equals(142, child.fIntOptionalInt(ia, ib));
  Expect.equals(42, child.fIntOptionalDouble(ia));
  Expect.equals(142, child.fIntOptionalDouble(ia, db));
  Expect.equals(42, child.fDoubleOptionalDouble(da));
  Expect.equals(142, child.fDoubleOptionalDouble(da, db));
  Expect.equals(42, child.fIntNamedInt(ia));
  Expect.equals(142, child.fIntNamedInt(ia, b: ib));
  Expect.equals(42, child.fIntNamedDouble(ia));
  Expect.equals(142, child.fIntNamedDouble(ia, b: db));
  Expect.equals(42, child.fDoubleNamedDouble(da));
  Expect.equals(142, child.fDoubleNamedDouble(da, b: db));
}

void testDirectCalls2(String str, int ia, int ib) {
  final da = ia.toDouble();
  final db = ib.toDouble();
  final child = ChildConvertingParametersToOptional(str);

  Expect.equals("ok+ok!", child.f());
  Expect.equals("ok+ok+", child.f(str));
  Expect.equals("ok+ok!", child.fOptional());
  Expect.equals("ok+ok+", child.fOptional(str));
  Expect.equals("ok+ok!", child.fNamed());
  Expect.equals("ok+ok+", child.fNamed(v: str));
  Expect.equals(42, child.fIntInt(ia));
  Expect.equals(142, child.fIntInt(ia, ib));
  Expect.equals(42, child.fIntDouble(ia));
  Expect.equals(142, child.fIntDouble(ia, db));
  Expect.equals(42, child.fDoubleDouble(da));
  Expect.equals(142, child.fDoubleDouble(da, db));
  Expect.equals(0, child.fIntOptionalInt());
  Expect.equals(42, child.fIntOptionalInt(ia));
  Expect.equals(142, child.fIntOptionalInt(ia, ib));
  Expect.equals(0, child.fIntOptionalDouble());
  Expect.equals(42, child.fIntOptionalDouble(ia));
  Expect.equals(142, child.fIntOptionalDouble(ia, db));
  Expect.equals(0, child.fDoubleOptionalDouble());
  Expect.equals(42, child.fDoubleOptionalDouble(da));
  Expect.equals(142, child.fDoubleOptionalDouble(da, db));
  Expect.equals(42, child.fIntNamedInt(ia));
  Expect.equals(142, child.fIntNamedInt(ia, b: ib));
  Expect.equals(42, child.fIntNamedDouble(ia));
  Expect.equals(142, child.fIntNamedDouble(ia, b: db));
  Expect.equals(42, child.fDoubleNamedDouble(da));
  Expect.equals(142, child.fDoubleNamedDouble(da, b: db));
}

void testVirtualCalls(Base child, int ia, int ib) {
  final da = ia.toDouble();
  final db = ib.toDouble();

  Expect.equals("ok+ok+", child.f(child.o));
  Expect.equals("ok+ok!", child.fOptional());
  Expect.equals("ok+ok+", child.fOptional(child.o));
  Expect.equals("ok+ok!", child.fNamed());
  Expect.equals("ok+ok+", child.fNamed(v: child.o));
  Expect.equals(142, child.fIntInt(ia, ib));
  Expect.equals(142, child.fIntDouble(ia, db));
  Expect.equals(142, child.fDoubleDouble(da, db));
  Expect.equals(42, child.fIntOptionalInt(ia));
  Expect.equals(142, child.fIntOptionalInt(ia, ib));
  Expect.equals(42, child.fIntOptionalDouble(ia));
  Expect.equals(142, child.fIntOptionalDouble(ia, db));
  Expect.equals(42, child.fDoubleOptionalDouble(da));
  Expect.equals(142, child.fDoubleOptionalDouble(da, db));
  Expect.equals(42, child.fIntNamedInt(ia));
  Expect.equals(142, child.fIntNamedInt(ia, b: ib));
  Expect.equals(42, child.fIntNamedDouble(ia));
  Expect.equals(142, child.fIntNamedDouble(ia, b: db));
  Expect.equals(42, child.fDoubleNamedDouble(da));
  Expect.equals(142, child.fDoubleNamedDouble(da, b: db));
}

void runTests(
  List<String> args,
  List<void Function(String, int, int)> directCallsTests,
  List<Base Function(String)> childClassFactories,
) {
  final ia = args.length >= 1 ? int.parse(args[0]) : 42;
  final ib = args.length >= 2 ? int.parse(args[1]) : 100;
  final str = args.length >= 3 ? args[2] : 'ok+';
  for (var test in directCallsTests) {
    test(str, ia, ib);
  }
  for (var f in childClassFactories) {
    testVirtualCalls(f(str), ia, ib);
  }
}

final directCallsTests = [testDirectCalls1, testDirectCalls2];
final childClassFactories = [
  ChildSimple.new,
  ChildConvertingParametersToOptional.new,
];

void main(List<String> args) {
  runTests(args, directCallsTests, childClassFactories);
}

void _matchIL(
  FlowGraph graph, {
  required List<String?> parameters,
  bool argDesc = false,
}) {
  graph.match([
    match.block('Graph'),
    match.block('Function', [
      for (var i = 0; i < parameters.length; i++)
        if (parameters[i] != null)
          match.Parameter(index: i, location: parameters[i]),
      if (argDesc)
        match.Parameter(index: parameters.length, location: 'reg(cpu)'),
    ]),
  ]);
}

void matchIL$ChildSimple$f(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', 'stack(word)']);
}

void matchIL$ChildSimple$fOptional(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null], argDesc: true);
}

void matchIL$ChildSimple$fNamed(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null], argDesc: true);
}

void matchIL$ChildSimple$fIntInt(FlowGraph graph) {
  _matchIL(
    graph,
    parameters: [
      'reg(cpu)',
      is32BitConfiguration ? '(reg(cpu), reg(cpu))' : 'reg(cpu)',
      'stack(word)',
    ],
  );
}

void matchIL$ChildSimple$fIntDouble(FlowGraph graph) {
  _matchIL(
    graph,
    parameters: [
      'reg(cpu)',
      is32BitConfiguration ? '(reg(cpu), reg(cpu))' : 'reg(cpu)',
      'stack(word)',
    ],
  );
}

void matchIL$ChildSimple$fDoubleDouble(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', 'reg(fpu)', 'stack(word)']);
}

void matchIL$ChildSimple$fIntOptionalInt(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null, null], argDesc: true);
}

void matchIL$ChildSimple$fIntOptionalDouble(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null, null], argDesc: true);
}

void matchIL$ChildSimple$fDoubleOptionalDouble(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null, null], argDesc: true);
}

void matchIL$ChildSimple$fIntNamedInt(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null, null], argDesc: true);
}

void matchIL$ChildSimple$fIntNamedDouble(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null, null], argDesc: true);
}

void matchIL$ChildSimple$fDoubleNamedDouble(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null, null], argDesc: true);
}

void matchIL$ChildConvertingParametersToOptional$f(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null], argDesc: true);
}

void matchIL$ChildConvertingParametersToOptional$fOptional(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null], argDesc: true);
}

void matchIL$ChildConvertingParametersToOptional$fNamed(FlowGraph graph) {
  _matchIL(graph, parameters: ['reg(cpu)', null], argDesc: true);
}

void matchIL$ChildConvertingParametersToOptional$fIntInt(FlowGraph graph) {
  _matchIL(
    graph,
    parameters: [
      'reg(cpu)',
      is32BitConfiguration ? '(reg(cpu), reg(cpu))' : 'reg(cpu)',
      null,
    ],
    argDesc: true,
  );
}

void matchIL$ChildConvertingParametersToOptional$fIntDouble(FlowGraph graph) {
  _matchIL(
    graph,
    parameters: [
      'reg(cpu)',
      is32BitConfiguration ? '(reg(cpu), reg(cpu))' : 'reg(cpu)',
      null,
    ],
    argDesc: true,
  );
}

void matchIL$ChildConvertingParametersToOptional$fDoubleDouble(
  FlowGraph graph,
) {
  _matchIL(graph, parameters: ['reg(cpu)', 'reg(fpu)', null], argDesc: true);
}

void matchIL$ChildConvertingParametersToOptional$fIntOptionalInt(
  FlowGraph graph,
) {
  matchIL$ChildSimple$fIntOptionalInt(graph);
}

void matchIL$ChildConvertingParametersToOptional$fIntOptionalDouble(
  FlowGraph graph,
) {
  matchIL$ChildSimple$fIntOptionalDouble(graph);
}

void matchIL$ChildConvertingParametersToOptional$fDoubleOptionalDouble(
  FlowGraph graph,
) {
  matchIL$ChildSimple$fDoubleOptionalDouble(graph);
}

void matchIL$ChildConvertingParametersToOptional$fIntNamedInt(FlowGraph graph) {
  matchIL$ChildSimple$fIntNamedInt(graph);
}

void matchIL$ChildConvertingParametersToOptional$fIntNamedDouble(
  FlowGraph graph,
) {
  matchIL$ChildSimple$fIntNamedDouble(graph);
}

void matchIL$ChildConvertingParametersToOptional$fDoubleNamedDouble(
  FlowGraph graph,
) {
  matchIL$ChildSimple$fDoubleNamedDouble(graph);
}
