// Copyright (c) 2017, 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 'package:code_builder/code_builder.dart';
import 'package:test/test.dart';

import '../common.dart';

void main() {
  useDartfmt();

  test('should create a method', () {
    expect(
      Method((b) => b..name = 'foo'),
      equalsDart(r'''
        foo();
      '''),
    );
  });

  test('should create an async method', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..modifier = MethodModifier.async
              ..body = literalNull.code,
      ),
      equalsDart(r'''
        foo() async => null
      '''),
    );
  });

  test('should create an async* method', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..modifier = MethodModifier.asyncStar
              ..body = literalNull.code,
      ),
      equalsDart(r'''
        foo() async* => null
      '''),
    );
  });

  test('should create an sync* method', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..modifier = MethodModifier.syncStar
              ..body = literalNull.code,
      ),
      equalsDart(r'''
        foo() sync* => null
      '''),
    );
  });

  test('should create a lambda method implicitly', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'returnsTrue'
              ..returns = refer('bool')
              ..body = literalTrue.code,
      ),
      equalsDart(r'''
        bool returnsTrue() => true
      '''),
    );
  });

  test('should create a lambda method if the value is cast', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'returnsCastedValue'
              ..returns = refer('Foo')
              ..body = refer('bar').asA(refer('Foo')).code,
      ),
      equalsDart(r'''
        Foo returnsCastedValue() => (bar as Foo)
      '''),
    );
  });

  test('should create a normal method implicitly', () {
    expect(
      Method.returnsVoid(
        (b) =>
            b
              ..name = 'assignTrue'
              ..body = refer('topLevelFoo').assign(literalTrue).statement,
      ),
      equalsDart(r'''
        void assignTrue() {
          topLevelFoo = true;
        }
      '''),
    );
  });

  test('should create a getter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..external = true
              ..type = MethodType.getter,
      ),
      equalsDart(r'''
        external get foo;
      '''),
    );
  });

  test('should create a setter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..external = true
              ..requiredParameters.add(Parameter((b) => b..name = 'foo'))
              ..type = MethodType.setter,
      ),
      equalsDart(r'''
        external set foo(foo);
      '''),
    );
  });

  test('should create a method with a return type', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..returns = refer('String'),
      ),
      equalsDart(r'''
        String foo();
      '''),
    );
  });

  test('should create a method with a void return type', () {
    expect(
      Method.returnsVoid((b) => b..name = 'foo'),
      equalsDart(r'''
        void foo();
      '''),
    );
  });

  test('should create a method with a function type return type', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..returns = FunctionType(
                (b) =>
                    b
                      ..returnType = refer('String')
                      ..requiredParameters.addAll([refer('int')]),
              ),
      ),
      equalsDart(r'''
        String Function(int) foo();
      '''),
    );
  });

  test(
    'should create a function type with an optional positional parameter',
    () {
      expect(
        FunctionType(
          (b) =>
              b
                ..returnType = refer('String')
                ..optionalParameters.add(refer('int')),
        ),
        equalsDart(r'''
        String Function([int])
      '''),
      );
    },
  );

  test('should create a function type with a required '
      'and an optional positional parameter', () {
    expect(
      FunctionType(
        (b) =>
            b
              ..returnType = refer('String')
              ..requiredParameters.add(refer('int'))
              ..optionalParameters.add(refer('int')),
      ),
      equalsDart(r'''
        String Function(int, [int, ])
      '''),
    );
  });

  test('should create a function type without parameters', () {
    expect(
      FunctionType((b) => b..returnType = refer('String')),
      equalsDart(r'''
        String Function()
      '''),
    );
  });

  test('should create a function type with an optional named parameter', () {
    expect(
      FunctionType(
        (b) =>
            b
              ..returnType = refer('String')
              ..namedParameters['named'] = refer('int'),
      ),
      equalsDart(r'''
        String Function({int named})
      '''),
    );
  });

  test('should create a function type with a required '
      'and an optional named parameter', () {
    expect(
      FunctionType(
        (b) =>
            b
              ..returnType = refer('String')
              ..requiredParameters.add(refer('int'))
              ..namedParameters['named'] = refer('int'),
      ),
      equalsDart(r'''
        String Function(int, {int named, })
      '''),
    );
  });

  test('should create a function type with a required named parameter', () {
    expect(
      FunctionType(
        (b) =>
            b
              ..returnType = refer('String')
              ..namedRequiredParameters['named'] = refer('int'),
      ),
      equalsDart(r'''
        String Function({required int named})
      '''),
    );
  });

  test('should create a function type with a required named and an optional '
      'named parameter', () {
    expect(
      FunctionType(
        (b) =>
            b
              ..returnType = refer('String')
              ..namedRequiredParameters['named'] = refer('int')
              ..namedParameters['optional'] = refer('int'),
      ),
      equalsDart(r'''
        String Function({required int named, int optional, })
      '''),
    );
  });

  test('should create a typedef to a reference', () {
    expect(
      TypeDef(
        (b) =>
            b
              ..name = 'i32'
              ..definition = const Reference('int'),
      ),
      equalsDart(r'''
        typedef i32 = int;
      '''),
    );
  });

  test('should create a typedef to a function type', () {
    expect(
      TypeDef(
        (b) =>
            b
              ..name = 'MyMapper'
              ..definition = FunctionType(
                (b) =>
                    b
                      ..returnType = refer('String')
                      ..optionalParameters.add(refer('int')),
              ),
      ),
      equalsDart(r'''
        typedef MyMapper = String Function([int]);
      '''),
    );
  });

  test('should create a method with a nested function type return type', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..returns = FunctionType(
                (b) =>
                    b
                      ..returnType = FunctionType(
                        (b) =>
                            b
                              ..returnType = refer('String')
                              ..requiredParameters.add(refer('String')),
                      )
                      ..requiredParameters.addAll([refer('int')]),
              ),
      ),
      equalsDart(r'''
        String Function(String) Function(int) foo();
      '''),
    );
  });

  test('should create a method with a function type argument', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..requiredParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..type = FunctionType(
                          (b) =>
                              b
                                ..returnType = refer('String')
                                ..requiredParameters.add(refer('int')),
                        )
                        ..name = 'argument',
                ),
              ),
      ),
      equalsDart(r'''
          foo(String Function(int) argument);
        '''),
    );
  });

  test('should create a method with a nested function type argument', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..requiredParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..type = FunctionType(
                          (b) =>
                              b
                                ..returnType = FunctionType(
                                  (b) =>
                                      b
                                        ..returnType = refer('String')
                                        ..requiredParameters.add(
                                          refer('String'),
                                        ),
                                )
                                ..requiredParameters.add(refer('int')),
                        )
                        ..name = 'argument',
                ),
              ),
      ),
      equalsDart(r'''
          foo(String Function(String) Function(int) argument);
        '''),
    );
  });

  test('should create a method with generic types', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..types.add(refer('T')),
      ),
      equalsDart(r'''
        foo<T>();
      '''),
    );
  });

  test('should create an external method', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..external = true,
      ),
      equalsDart(r'''
        external foo();
      '''),
    );
  });

  test('should create a method with a body', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..body = const Code('return 1+ 2;'),
      ),
      equalsDart(r'''
        foo() {
          return 1 + 2;
        }
      '''),
    );
  });

  test('should create a lambda method (explicitly)', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..lambda = true
              ..body = const Code('1 + 2'),
      ),
      equalsDart(r'''
        foo() => 1 + 2
      '''),
    );
  });

  test('should create a method with a body with references', () {
    final $LinkedHashMap = refer('LinkedHashMap', 'dart:collection');
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..body = Code.scope((a) => 'return ${a($LinkedHashMap)}();'),
      ),
      equalsDart(r'''
        foo() {
          return LinkedHashMap();
        }
      '''),
    );
  });

  test('should create a method with a parameter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..requiredParameters.add(Parameter((b) => b.name = 'i')),
      ),
      equalsDart(r'''
        fib(i);
      '''),
    );
  });

  test('should create a method with an annotated parameter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..requiredParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..name = 'i'
                        ..annotations.add(refer('deprecated')),
                ),
              ),
      ),
      equalsDart(r'''
        fib(@deprecated i);
      '''),
    );
  });

  test('should create a method with a parameter with a type', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..requiredParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..name = 'i'
                        ..type = refer('int').type,
                ),
              ),
      ),
      equalsDart(r'''
        fib(int i);
      '''),
    );
  });

  test('should create a method with a covariant parameter with a type', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..requiredParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..name = 'i'
                        ..covariant = true
                        ..type = refer('int').type,
                ),
              ),
      ),
      equalsDart(r'''
        fib(covariant int i);
      '''),
    );
  });

  test('should create a method with a parameter with a generic type', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..types.add(
                TypeReference(
                  (b) =>
                      b
                        ..symbol = 'T'
                        ..bound = refer('Iterable'),
                ),
              )
              ..requiredParameters.addAll([
                Parameter(
                  (b) =>
                      b
                        ..name = 't'
                        ..type = refer('T'),
                ),
                Parameter(
                  (b) =>
                      b
                        ..name = 'x'
                        ..type = TypeReference(
                          (b) =>
                              b
                                ..symbol = 'X'
                                ..types.add(refer('T')),
                        ),
                ),
              ]),
      ),
      equalsDart(r'''
        foo<T extends Iterable>(T t, X<T> x, );
      '''),
    );
  });

  test('should create a method with an optional parameter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..optionalParameters.add(Parameter((b) => b.name = 'i')),
      ),
      equalsDart(r'''
        fib([i]);
      '''),
    );
  });

  test('should create a method with multiple optional parameters', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..optionalParameters.addAll([
                Parameter((b) => b.name = 'a'),
                Parameter((b) => b.name = 'b'),
              ]),
      ),
      equalsDart(r'''
        foo([a, b, ]);
      '''),
    );
  });

  test('should create a method with an optional parameter with a value', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..optionalParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..name = 'i'
                        ..defaultTo = const Code('0'),
                ),
              ),
      ),
      equalsDart(r'''
        fib([i = 0]);
      '''),
    );
  });

  test('should create a method with a named required parameter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..optionalParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..name = 'i'
                        ..named = true
                        ..required = true
                        ..type = refer('int').type,
                ),
              ),
      ),
      equalsDart(r'''
        fib({required int i});
      '''),
    );
  });

  test('should create a method with a named required covariant parameter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..optionalParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..name = 'i'
                        ..named = true
                        ..required = true
                        ..covariant = true
                        ..type = refer('int').type,
                ),
              ),
      ),
      equalsDart(r'''
        fib({required covariant int i});
      '''),
    );
  });

  test('should create a method with a named optional parameter', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..optionalParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..named = true
                        ..name = 'i',
                ),
              ),
      ),
      equalsDart(r'''
        fib({i});
      '''),
    );
  });

  test('should create a method with a named optional parameter with value', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'fib'
              ..optionalParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..named = true
                        ..name = 'i'
                        ..defaultTo = const Code('0'),
                ),
              ),
      ),
      equalsDart(r'''
        fib({i = 0});
      '''),
    );
  });

  test('should create a method with a mix of parameters', () {
    expect(
      Method(
        (b) =>
            b
              ..name = 'foo'
              ..requiredParameters.add(Parameter((b) => b..name = 'a'))
              ..optionalParameters.add(
                Parameter(
                  (b) =>
                      b
                        ..named = true
                        ..name = 'b',
                ),
              ),
      ),
      equalsDart(r'''
        foo(a, {b, });
      '''),
    );
  });

  test('should create a method as a closure', () {
    expect(
      Method(
        (b) =>
            b
              ..requiredParameters.add(Parameter((b) => b..name = 'a'))
              ..body = const Code(''),
      ).closure,
      equalsDart(r'''
        (a) { }
      '''),
    );
  });
}
