// Copyright (c) 2020, 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 an enum', () {
    expect(
      Enum(
        (b) =>
            b
              ..name = 'E'
              ..values.addAll([
                EnumValue((b) => b..name = 'a'),
                EnumValue((b) => b..name = 'b'),
              ]),
      ),
      equalsDart(r'''
      enum E {
        a,
        b
      }
    '''),
    );
  });

  test('should create an enum with annotations', () {
    expect(
      Enum(
        (b) =>
            b
              ..annotations.addAll([
                refer('deprecated'),
                refer(
                  'Deprecated',
                ).call([literalString('This is an old enum')]),
              ])
              ..name = 'V'
              ..values.addAll([EnumValue((b) => b..name = 'x')]),
      ),
      equalsDart(r'''
      @deprecated
      @Deprecated('This is an old enum')
      enum V {
        x
      }
    '''),
    );
  });

  test('should create an enum with annotated values', () {
    expect(
      Enum(
        (b) =>
            b
              ..name = 'Status'
              ..values.addAll([
                EnumValue(
                  (b) =>
                      b
                        ..name = 'okay'
                        ..annotations.addAll([
                          refer('deprecated'),
                          refer(
                            'Deprecated',
                          ).call([literalString('use Good instead')]),
                        ]),
                ),
                EnumValue(
                  (b) =>
                      b
                        ..name = 'good'
                        ..annotations.addAll([
                          refer('JsonKey').call([literalString('good')]),
                        ]),
                ),
              ]),
      ),
      equalsDart(r'''
      enum Status {
        @deprecated
        @Deprecated('use Good instead')
        okay,
        @JsonKey('good')
        good
      }
    '''),
    );
  });

  test('should create an enum which mixes in and implements specs', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..implements.addAll(const [
              Reference('InterfaceA'),
              Reference('InterfaceB'),
            ])
            ..mixins.addAll(const [Reference('Mixin1'), Reference('Mixin2')])
            ..values.addAll([
              EnumValue((v) => v..name = 'a'),
              EnumValue((v) => v..name = 'b'),
              EnumValue((v) => v..name = 'c'),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum with Mixin1, Mixin2 implements InterfaceA, InterfaceB {
      a,
      b,
      c
    }
    '''),
    );
  });

  test('should create an enum which targets a named constructor', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..constructors.addAll([
              Constructor((c) => c..constant = true),
              Constructor(
                (c) =>
                    c
                      ..constant = true
                      ..name = 'named',
              ),
            ])
            ..values.addAll([
              EnumValue((v) => v..name = 'a'),
              EnumValue(
                (v) =>
                    v
                      ..name = 'b'
                      ..constructorName = 'named',
              ),
              EnumValue((v) => v..name = 'c'),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum {
      a,
      b.named(),
      c;

      const MyEnum();

      const MyEnum.named();
    }
    '''),
    );
  });

  test('should create an enum which targets a redirecting constructor', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..constructors.addAll([
              Constructor((c) => c..constant = true),
              Constructor(
                (c) =>
                    c
                      ..constant = true
                      ..name = 'redirect'
                      ..initializers.add(refer('this').call([]).code),
              ),
            ])
            ..values.addAll([
              EnumValue((v) => v..name = 'a'),
              EnumValue(
                (v) =>
                    v
                      ..name = 'b'
                      ..constructorName = 'redirect',
              ),
              EnumValue((v) => v..name = 'c'),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum {
      a,
      b.redirect(),
      c;

      const MyEnum();

      const MyEnum.redirect() : this();
    }
    '''),
    );
  });

  test(
    'should create an enum which targets a redirecting factory constructor',
    () {
      final myEnum = Enum(
        (b) =>
            b
              ..name = 'MyEnum'
              ..constructors.addAll([
                Constructor((c) => c..constant = true),
                Constructor(
                  (c) =>
                      c
                        ..constant = true
                        ..factory = true
                        ..name = 'redirect'
                        ..redirect = refer('MyOtherEnum.named')
                        ..optionalParameters.addAll([
                          Parameter(
                            (p) =>
                                p
                                  ..type = refer('int?')
                                  ..name = 'myInt',
                          ),
                          Parameter(
                            (p) =>
                                p
                                  ..type = refer('String?')
                                  ..name = 'myString',
                          ),
                        ]),
                ),
              ])
              ..values.addAll([
                EnumValue((v) => v..name = 'a'),
                EnumValue(
                  (v) =>
                      v
                        ..name = 'b'
                        ..constructorName = 'redirect'
                        ..arguments.addAll([
                          literalNum(1),
                          literalString('abc'),
                        ]),
                ),
                EnumValue(
                  (v) =>
                      v
                        ..name = 'c'
                        ..constructorName = 'redirect',
                ),
              ]),
      );
      expect(
        myEnum,
        equalsDart('''
    enum MyEnum {
      a,
      b.redirect(1, 'abc'),
      c.redirect();

      const MyEnum();

      const factory MyEnum.redirect([
          int? myInt,
          String? myString,
      ]) = MyOtherEnum.named;
    }
    '''),
      );
    },
  );

  test('should create an enum which targets an unnamed constructor', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..constructors.add(
              Constructor(
                (c) =>
                    c
                      ..constant = true
                      ..optionalParameters.addAll([
                        Parameter(
                          (p) =>
                              p
                                ..toThis = true
                                ..name = 'myInt',
                        ),
                        Parameter(
                          (p) =>
                              p
                                ..toThis = true
                                ..name = 'myString',
                        ),
                      ]),
              ),
            )
            ..fields.addAll([
              Field(
                (f) =>
                    f
                      ..modifier = FieldModifier.final$
                      ..type = refer('int?')
                      ..name = 'myInt',
              ),
              Field(
                (f) =>
                    f
                      ..modifier = FieldModifier.final$
                      ..type = refer('String?')
                      ..name = 'myString',
              ),
            ])
            ..values.addAll([
              EnumValue((v) => v..name = 'a'),
              EnumValue(
                (v) =>
                    v
                      ..name = 'b'
                      ..arguments.addAll([literalNum(1), literalString('abc')]),
              ),
              EnumValue((v) => v..name = 'c'),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum {
      a,
      b(1, 'abc'),
      c;

      const MyEnum([
          this.myInt,
          this.myString,
      ]);

      final int? myInt;

      final String? myString;
    }
    '''),
    );
  });

  test('should create an enum with generics', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..types.add(const Reference('T'))
            ..constructors.add(
              Constructor(
                (c) =>
                    c
                      ..constant = true
                      ..requiredParameters.add(
                        Parameter(
                          (p) =>
                              p
                                ..toThis = true
                                ..name = 'value',
                        ),
                      ),
              ),
            )
            ..fields.add(
              Field(
                (f) =>
                    f
                      ..modifier = FieldModifier.final$
                      ..type = refer('T')
                      ..name = 'value',
              ),
            )
            ..values.addAll([
              EnumValue(
                (v) =>
                    v
                      ..name = 'a'
                      ..types.add(const Reference('int'))
                      ..arguments.add(literalNum(123)),
              ),
              EnumValue(
                (v) =>
                    v
                      ..name = 'b'
                      ..types.add(const Reference('String'))
                      ..arguments.add(literalString('abc')),
              ),
              EnumValue(
                (v) =>
                    v
                      ..name = 'c'
                      ..types.add(const Reference('MyEnum'))
                      ..arguments.add(refer('MyEnum').property('a')),
              ),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum<T> {
      a<int>(123),
      b<String>('abc'),
      c<MyEnum>(MyEnum.a);

      const MyEnum(this.value);

      final T value;
    }
    '''),
    );
  });

  test('should create an enum with fields', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..constructors.add(
              Constructor(
                (c) =>
                    c
                      ..constant = true
                      ..optionalParameters.add(
                        Parameter(
                          (p) =>
                              p
                                ..toThis = true
                                ..name = 'myInt',
                        ),
                      ),
              ),
            )
            ..fields.addAll([
              Field(
                (f) =>
                    f
                      ..modifier = FieldModifier.final$
                      ..type = refer('int?')
                      ..name = 'myInt',
              ),
              Field(
                (f) =>
                    f
                      ..static = true
                      ..modifier = FieldModifier.constant
                      ..type = refer('String')
                      ..name = 'myString'
                      ..assignment = literalString('abc').code,
              ),
            ])
            ..values.addAll([
              EnumValue((v) => v..name = 'a'),
              EnumValue((v) => v..name = 'b'),
              EnumValue((v) => v..name = 'c'),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum {
      a,
      b,
      c;

      const MyEnum([this.myInt]);

      final int? myInt;

      static const String myString = 'abc';
    }
    '''),
    );
  });

  test('should create an enum with methods', () {
    final myEnum = Enum(
      (b) =>
          b
            ..name = 'MyEnum'
            ..methods.addAll([
              Method(
                (m) =>
                    m
                      ..returns = refer('int')
                      ..type = MethodType.getter
                      ..name = 'myInt'
                      ..body = literalNum(123).code,
              ),
              Method(
                (m) =>
                    m
                      ..returns = refer('Iterable<String>')
                      ..name = 'myStrings'
                      ..modifier = MethodModifier.syncStar
                      ..body = Block.of(const [
                        Code("yield 'a';"),
                        Code("yield 'b';"),
                        Code("yield 'c';"),
                      ]),
              ),
            ])
            ..values.addAll([
              EnumValue((v) => v..name = 'a'),
              EnumValue((v) => v..name = 'b'),
              EnumValue((v) => v..name = 'c'),
            ]),
    );
    expect(
      myEnum,
      equalsDart('''
    enum MyEnum {
      a,
      b,
      c;

      int get myInt => 123;

      Iterable<String> myStrings() sync* {
        yield 'a';
        yield 'b';
        yield 'c';
      }
    }
    '''),
    );
  });

  test(
    'should create an enum which named and unnamed constructor parameters',
    () {
      final myEnum = Enum(
        (b) =>
            b
              ..name = 'MyEnum'
              ..constructors.add(
                Constructor(
                  (c) =>
                      c
                        ..constant = true
                        ..requiredParameters.addAll([
                          Parameter(
                            (p) =>
                                p
                                  ..toThis = true
                                  ..name = 'myInt',
                          ),
                        ])
                        ..optionalParameters.addAll([
                          Parameter(
                            (p) =>
                                p
                                  ..toThis = true
                                  ..named = true
                                  ..required = true
                                  ..name = 'myString',
                          ),
                        ]),
                ),
              )
              ..fields.addAll([
                Field(
                  (f) =>
                      f
                        ..modifier = FieldModifier.final$
                        ..type = refer('int?')
                        ..name = 'myInt',
                ),
                Field(
                  (f) =>
                      f
                        ..modifier = FieldModifier.final$
                        ..type = refer('String?')
                        ..name = 'myString',
                ),
              ])
              ..values.addAll([
                EnumValue((v) => v..name = 'a'),
                EnumValue(
                  (v) =>
                      v
                        ..name = 'b'
                        ..arguments.addAll([literalNum(1)])
                        ..namedArguments.addAll({
                          'myString': literalString('abc'),
                        }),
                ),
                EnumValue((v) => v..name = 'c'),
              ]),
      );
      expect(
        myEnum,
        equalsDart('''
    enum MyEnum {
      a,
      b(1, myString: 'abc'),
      c;

      const MyEnum(
          this.myInt,
          {required this.myString, }
      );

      final int? myInt;

      final String? myString;
    }
    '''),
      );
    },
  );
}
