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

import '../../../tool/lsp_spec/codegen_dart.dart';
import '../../../tool/lsp_spec/generate_all.dart';
import '../../../tool/lsp_spec/meta_model.dart';
import 'matchers.dart';

void main() {
  defineReflectiveSuite(() {
    setUpAll(() {
      // Ensure any custom types like LSPAny are registered so that they can
      // be resolved.
      recordTypes(getCustomClasses());
    });
    defineReflectiveTests(MetaModelTest);
  });
}

List<LspEntity> readModel(Map<String, dynamic> model) =>
    LspMetaModelCleaner().cleanTypes(LspMetaModelReader().readMap(model).types);

@reflectiveTest
class MetaModelTest {
  void test_flagsNullableUndefinedValues() {
    var input = {
      'structures': [
        {
          'name': 'A',
          'properties': [
            {
              'name': 'canBeBoth',
              'type': {
                'kind': 'or',
                'items': [
                  {'kind': 'base', 'name': 'string'},
                  {'kind': 'base', 'name': 'null'},
                ],
              },
              'optional': true,
            },
            {
              'name': 'canBeNeither',
              'type': {'kind': 'base', 'name': 'string'},
            },
            {
              'name': 'canBeNull',
              'type': {
                'kind': 'or',
                'items': [
                  {'kind': 'base', 'name': 'string'},
                  {'kind': 'base', 'name': 'null'},
                ],
              },
            },
            {
              'name': 'canBeUndefined',
              'type': {'kind': 'base', 'name': 'string'},
              'optional': true,
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    var interface = output[0] as Interface;
    expect(interface.members, hasLength(4));
    for (var m in interface.members) {
      expect(m, const TypeMatcher<Field>());
    }
    var canBeBoth = interface.members[0] as Field,
        canBeNeither = interface.members[1] as Field,
        canBeNull = interface.members[2] as Field,
        canBeUndefined = interface.members[3] as Field;
    expect(canBeNeither.allowsNull, isFalse);
    expect(canBeNeither.allowsUndefined, isFalse);
    expect(canBeNull.allowsNull, isTrue);
    expect(canBeNull.allowsUndefined, isFalse);
    expect(canBeUndefined.allowsNull, isFalse);
    expect(canBeUndefined.allowsUndefined, isTrue);
    expect(canBeBoth.allowsNull, isTrue);
    expect(canBeBoth.allowsUndefined, isTrue);
  }

  void test_formatsCommentsCorrectly() {
    var input = {
      'structures': [
        {
          'name': 'A',
          'properties': [],
          'documentation': r"""
Describes the what this class in lots of words that wrap onto multiple lines that will need re-wrapping to format nicely when converted into Dart.

Blank lines should remain in-tact, as should:
  - Indented
  - Things

Some docs have:
- List items that are not indented

Sometimes after a blank line we'll have a note.

*Note* that something.""",
        },
      ],
    };
    var output = readModel(input);
    var interface = output[0] as Interface;
    expect(
      interface.comment,
      equals('''
Describes the what this class in lots of words that wrap onto multiple lines that will need re-wrapping to format nicely when converted into Dart.

Blank lines should remain in-tact, as should:
  - Indented
  - Things

Some docs have:
- List items that are not indented

Sometimes after a blank line we'll have a note.

*Note* that something.'''),
    );
  }

  void test_interface() {
    var input = {
      'structures': [
        {
          'name': 'SomeOptions',
          'properties': [
            {
              'name': 'options',
              'type': {
                'kind': 'array',
                'element': {'kind': 'reference', 'name': 'string'},
              },
              'optional': true,
              'documentation': 'Options used by something.',
            },
          ],
          'documentation': 'Some options.',
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.name, equals('SomeOptions'));
    expect(interface.comment, equals('Some options.'));
    expect(interface.baseTypes, hasLength(0));
    expect(interface.members, hasLength(1));
    expect(interface.members[0], const TypeMatcher<Field>());
    var field = interface.members[0] as Field;
    expect(field.name, equals('options'));
    expect(field.comment, equals('''Options used by something.'''));
    expect(field.allowsNull, isFalse);
    expect(field.allowsUndefined, isTrue);
    expect(field.type, isArrayOf(isSimpleType('string')));
  }

  void test_interface_fieldInlineUnnamedType() {
    var input = {
      'structures': [
        {
          'name': 'Capabilities',
          'properties': [
            {
              'name': 'textDoc',
              'type': {
                'kind': 'literal',
                'value': {
                  'properties': [
                    {
                      'name': 'deprecated',
                      'type': {'kind': 'base', 'name': 'bool'},
                      'optional': true,
                    },
                  ],
                },
              },
              'optional': true,
            },
          ],
          'documentation': 'Some options.',
        },
      ],
    };
    var output = readModel(input);
    // Length is two because we'll fabricate the type of textDoc.
    expect(output, hasLength(2));

    // Check there was a full fabricated interface for this type.
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.name, equals('CapabilitiesTextDoc'));
    expect(interface.members, hasLength(1));
    expect(interface.members[0], const TypeMatcher<Field>());
    var field = interface.members[0] as Field;
    expect(field.name, equals('deprecated'));
    expect(field.allowsNull, isFalse);
    expect(field.allowsUndefined, isTrue);
    expect(field.type, isSimpleType('bool'));
    expect(field.allowsUndefined, isTrue);

    expect(output[1], const TypeMatcher<Interface>());
    interface = output[1] as Interface;
    expect(interface.name, equals('Capabilities'));
    expect(interface.members, hasLength(1));
    expect(interface.members[0], const TypeMatcher<Field>());
    field = interface.members[0] as Field;
    expect(field.name, equals('textDoc'));
    expect(field.allowsNull, isFalse);
    expect(field.type, isSimpleType('CapabilitiesTextDoc'));
  }

  void test_interface_mapInMap() {
    var input = {
      'structures': [
        {
          'name': 'WorkspaceEdit',
          'properties': [
            {
              'name': 'changes',
              'type': {
                'kind': 'map',
                'key': {'kind': 'base', 'name': 'string'},
                'value': {
                  'kind': 'array',
                  'element': {'kind': 'reference', 'name': 'TextEdit'},
                },
              },
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.members, hasLength(1));
    var field = interface.members.first as Field;
    expect(field, const TypeMatcher<Field>());
    expect(field.name, equals('changes'));
    expect(
      field.type,
      isMapOf(isSimpleType('string'), isArrayOf(isSimpleType('TextEdit'))),
    );
  }

  void test_interface_multipleFields() {
    var input = {
      'structures': [
        {
          'name': 'SomeOptions',
          'properties': [
            {
              'name': 'options0',
              'type': {'kind': 'reference', 'name': 'LSPAny'},
              'documentation': 'Options0 used by something.',
            },
            {
              'name': 'options1',
              'type': {'kind': 'reference', 'name': 'LSPAny'},
              'documentation': 'Options1 used by something.',
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.members, hasLength(2));
    for (var i in [0, 1]) {
      expect(interface.members[i], const TypeMatcher<Field>());
      var field = interface.members[i] as Field;
      expect(field.name, equals('options$i'));
      expect(field.comment, equals('''Options$i used by something.'''));
    }
  }

  void test_literal_string() {
    var input = {
      'structures': [
        {
          'name': 'MyType',
          'properties': [
            {
              'name': 'kind',
              'type': {'kind': 'stringLiteral', 'value': 'one'},
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.name, equals('MyType'));
    expect(interface.members, hasLength(1));
    expect(interface.members[0], const TypeMatcher<Field>());
    var field = interface.members[0] as Field;
    expect(field.name, equals('kind'));
    expect(field.allowsNull, isFalse);
    expect(field.allowsUndefined, isFalse);
    expect(field.type, isLiteralOf(isSimpleType('string'), "'one'"));
  }

  void test_literal_union() {
    var input = {
      'structures': [
        {
          'name': 'MyType',
          'properties': [
            {
              'name': 'kind',
              'type': {
                'kind': 'or',
                'items': [
                  {'kind': 'stringLiteral', 'value': 'one'},
                  {'kind': 'stringLiteral', 'value': 'two'},
                ],
              },
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.name, equals('MyType'));
    expect(interface.members, hasLength(1));
    expect(interface.members[0], const TypeMatcher<Field>());
    var field = interface.members[0] as Field;
    expect(field.name, equals('kind'));
    expect(field.allowsNull, isFalse);
    expect(field.allowsUndefined, isFalse);
    expect(field.type, const TypeMatcher<LiteralUnionType>());
    var union = field.type as LiteralUnionType;
    expect(union.types, hasLength(2));
    expect(union.types[0], isLiteralOf(isSimpleType('string'), "'one'"));
    expect(union.types[1], isLiteralOf(isSimpleType('string'), "'two'"));
  }

  void test_namespace_constants() {
    var input = {
      'enumerations': [
        {
          'name': 'ResourceOperationKind',
          'type': {'kind': 'base', 'name': 'string'},
          'values': [
            {
              'name': 'Create',
              'value': 'create',
              'documentation': 'Supports creating new files and folders.',
            },
            {
              'name': 'Delete',
              'value': 'delete',
              'documentation': 'Supports deleting existing files and folders.',
            },
            {
              'name': 'Rename',
              'value': 'rename',
              'documentation': 'Supports renaming existing files and folders.',
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));

    expect(output[0], const TypeMatcher<LspEnum>());
    var namespace = output[0] as LspEnum;
    expect(namespace.constants, hasLength(3));
    var create = namespace.constants[0],
        delete = namespace.constants[1],
        rename = namespace.constants[2];
    expect(create.name, equals('Create'));
    expect(create.type, isSimpleType('ResourceOperationKind'));
    expect(create.comment, equals('Supports creating new files and folders.'));
    expect(rename.name, equals('Rename'));
    expect(rename.type, isSimpleType('ResourceOperationKind'));
    expect(
      rename.comment,
      equals('Supports renaming existing files and folders.'),
    );
    expect(delete.name, equals('Delete'));
    expect(delete.type, isSimpleType('ResourceOperationKind'));
    expect(
      delete.comment,
      equals('Supports deleting existing files and folders.'),
    );
  }

  void test_tuple_array() {
    var input = {
      'structures': [
        {
          'name': 'SomeInformation',
          'properties': [
            {
              'name': 'label',
              'type': {
                'kind': 'or',
                'items': [
                  {'kind': 'base', 'name': 'string'},
                  {
                    'kind': 'tuple',
                    'items': [
                      {'kind': 'base', 'name': 'number'},
                      {'kind': 'base', 'name': 'number'},
                    ],
                  },
                ],
              },
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.members, hasLength(1));
    var field = interface.members.first as Field;
    expect(field, const TypeMatcher<Field>());
    expect(field.name, equals('label'));
    expect(field.type, const TypeMatcher<UnionType>());
    var union = field.type as UnionType;
    expect(union.types, hasLength(2));
    expect(union.types[0], isArrayOf(isSimpleType('number')));
    expect(union.types[1], isSimpleType('string'));
  }

  void test_typeAlias() {
    var input = {
      'typeAliases': [
        {
          'name': 'DocumentSelector',
          'type': {
            'kind': 'array',
            'element': {'kind': 'reference', 'name': 'DocumentFilter'},
          },
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<TypeAlias>());
    var typeAlias = output[0] as TypeAlias;
    expect(typeAlias.name, equals('DocumentSelector'));
    expect(typeAlias.baseType, isArrayOf(isSimpleType('DocumentFilter')));
  }

  void test_typeAlias_unionOfUnnamedTypes() {
    var input = {
      'typeAliases': [
        {
          'name': 'NameOrLength',
          'type': {
            'kind': 'or',
            'items': [
              {
                'kind': 'literal',
                'value': {
                  'properties': [
                    {
                      'name': 'name',
                      'type': {'kind': 'base', 'name': 'string'},
                    },
                  ],
                },
              },
              {
                'kind': 'literal',
                'value': {
                  'properties': [
                    {
                      'name': 'length',
                      'type': {'kind': 'base', 'name': 'number'},
                    },
                  ],
                },
              },
            ],
          },
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(3));

    // Results should be the two inline interfaces followed by the type alias.

    expect(output[0], const TypeMatcher<Interface>());
    var interface1 = output[0] as Interface;
    expect(interface1.name, equals('NameOrLength1'));
    expect(interface1.members, hasLength(1));
    expect(interface1.members[0].name, equals('name'));

    expect(output[1], const TypeMatcher<Interface>());
    var interface2 = output[1] as Interface;
    expect(interface2.name, equals('NameOrLength2'));
    expect(interface2.members, hasLength(1));
    expect(interface2.members[0].name, equals('length'));

    expect(output[2], const TypeMatcher<TypeAlias>());
    var typeAlias = output[2] as TypeAlias;
    expect(typeAlias.name, equals('NameOrLength'));
    expect(typeAlias.baseType, const TypeMatcher<UnionType>());

    // The type alias should be a union of the two types above.
    var union = typeAlias.baseType as UnionType;
    expect(union.types, hasLength(2));
    expect(union.types[0], isSimpleType(interface1.name));
    expect(union.types[1], isSimpleType(interface2.name));
  }

  void test_union_includingLspAny_asOneType() {
    var input = {
      'structures': [
        {
          'name': 'SomeInformation',
          'properties': [
            {
              'name': 'label',
              'type': {
                'kind': 'or',
                'items': [
                  {'kind': 'base', 'name': 'string'},
                  {'kind': 'base', 'name': 'LSPAny'},
                ],
              },
            },
          ],
        },
      ],
    };
    var output = readModel(input);
    expect(output, hasLength(1));
    expect(output[0], const TypeMatcher<Interface>());
    var interface = output[0] as Interface;
    expect(interface.members, hasLength(1));
    var field = interface.members.first as Field;
    expect(field, const TypeMatcher<Field>());
    expect(field.name, equals('label'));
    expect(field.type, isSimpleType('LSPAny'));
  }
}
