// Copyright (c) 2019, 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:async';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(OutlineTest);
  });
}

@reflectiveTest
class OutlineTest extends AbstractLspAnalysisServerTest {
  Future<void> test_afterChange() async {
    var initialContent = 'class A {}';
    var updatedContent = 'class B {}';
    await initialize(initializationOptions: {'outline': true});

    var outlineUpdateBeforeChange = waitForOutline(mainFileUri);
    await openFile(mainFileUri, initialContent);
    var outlineBeforeChange = await outlineUpdateBeforeChange;

    var outlineUpdateAfterChange = waitForOutline(mainFileUri);
    await replaceFile(1, mainFileUri, updatedContent);
    var outlineAfterChange = await outlineUpdateAfterChange;

    expect(outlineBeforeChange, isNotNull);
    expect(outlineBeforeChange.children, hasLength(1));
    expect(outlineBeforeChange.children![0].element.name, equals('A'));

    expect(outlineAfterChange, isNotNull);
    expect(outlineAfterChange.children, hasLength(1));
    expect(outlineAfterChange.children![0].element.name, equals('B'));
  }

  Future<void> test_extensions() async {
    failTestOnErrorDiagnostic = false;

    var initialContent = '''
extension StringExtensions on String {}
extension on String {}
extension on {}
    ''';
    await initialize(initializationOptions: {'outline': true});

    var outlineUpdate = waitForOutline(mainFileUri);
    await openFile(mainFileUri, initialContent);
    var outline = await outlineUpdate;

    expect(outline, isNotNull);
    expect(outline.children, hasLength(3));
    expect(outline.children![0].element.name, equals('StringExtensions'));
    expect(outline.children![1].element.name, equals('extension on String'));
    expect(outline.children![2].element.name, equals('<unnamed extension>'));
  }

  Future<void> test_initial() async {
    var content = '''
/// a
class A {
  /// b
  b() {
    /// c
    c() {}
  }

  /// d
  num get d => 1;
}
''';
    await initialize(initializationOptions: {'outline': true});

    var outlineNotification = waitForOutline(mainFileUri);
    await openFile(mainFileUri, content);
    var outline = await outlineNotification;

    expect(outline, isNotNull);

    // Root node is entire document
    expect(
      outline.range,
      equals(
        Range(
          start: Position(line: 0, character: 0),
          end: Position(line: 11, character: 0),
        ),
      ),
    );
    expect(outline.children, hasLength(1));

    // class A
    var classA = outline.children![0];
    expect(classA.element.name, equals('A'));
    expect(classA.element.kind, equals('CLASS'));
    expect(
      classA.element.range,
      equals(
        Range(
          start: Position(line: 1, character: 6),
          end: Position(line: 1, character: 7),
        ),
      ),
    );
    expect(
      classA.range,
      equals(
        Range(
          start: Position(line: 0, character: 0),
          end: Position(line: 10, character: 1),
        ),
      ),
    );
    expect(
      classA.codeRange,
      equals(
        Range(
          start: Position(line: 1, character: 0),
          end: Position(line: 10, character: 1),
        ),
      ),
    );
    expect(classA.children, hasLength(2));

    // b()
    var methodB = classA.children![0];
    expect(methodB.element.name, equals('b'));
    expect(methodB.element.kind, equals('METHOD'));
    expect(
      methodB.element.range,
      equals(
        Range(
          start: Position(line: 3, character: 2),
          end: Position(line: 3, character: 3),
        ),
      ),
    );
    expect(
      methodB.range,
      equals(
        Range(
          start: Position(line: 2, character: 2),
          end: Position(line: 6, character: 3),
        ),
      ),
    );
    expect(
      methodB.codeRange,
      equals(
        Range(
          start: Position(line: 3, character: 2),
          end: Position(line: 6, character: 3),
        ),
      ),
    );
    expect(methodB.children, hasLength(1));

    // c()
    var methodC = methodB.children![0];
    expect(methodC.element.name, equals('c'));
    expect(methodC.element.kind, equals('FUNCTION'));
    expect(
      methodC.element.range,
      equals(
        Range(
          start: Position(line: 5, character: 4),
          end: Position(line: 5, character: 5),
        ),
      ),
    );
    // TODO(dantup): This one seems to be excluding its dartdoc?
    // should be line 4 for the starting range.
    // https://github.com/dart-lang/sdk/issues/39746
    expect(
      methodC.range,
      equals(
        Range(
          start: Position(line: 5, character: 4),
          end: Position(line: 5, character: 10),
        ),
      ),
    );
    expect(
      methodC.codeRange,
      equals(
        Range(
          start: Position(line: 5, character: 4),
          end: Position(line: 5, character: 10),
        ),
      ),
    );
    expect(methodC.children, isNull);

    // num get d
    var fieldD = classA.children![1];
    expect(fieldD.element.name, equals('d'));
    expect(fieldD.element.kind, equals('GETTER'));
    expect(
      fieldD.element.range,
      equals(
        Range(
          start: Position(line: 9, character: 10),
          end: Position(line: 9, character: 11),
        ),
      ),
    );
    expect(
      fieldD.range,
      equals(
        Range(
          start: Position(line: 8, character: 2),
          end: Position(line: 9, character: 17),
        ),
      ),
    );
    expect(
      fieldD.codeRange,
      equals(
        Range(
          start: Position(line: 9, character: 2),
          end: Position(line: 9, character: 17),
        ),
      ),
    );
    expect(fieldD.children, isNull);
  }

  /// As an optimization, when a file is opened but does not contain any changes
  /// from what was on disk, we skip analysis (in onOverlayCreated).
  /// We still need to ensure that notifications like Outline (which are
  /// triggered by analysis results and only sent for open files) are sent to
  /// the client.
  Future<void> test_openedWithoutChanges() async {
    var content = r'''
class A {}
''';

    // Create the file on disk so that opening the file won't re-trigger
    // analysis.
    newFile(mainFilePath, content);

    // Track when outlines arrive.
    Outline? mainOutline;
    unawaited(
      waitForOutline(mainFileUri).then((outline) => mainOutline = outline),
    );

    await Future.wait([
      initialize(initializationOptions: {'outline': true}),
      waitForAnalysisComplete(),
    ]);
    await pumpEventQueue(times: 5000);
    expect(mainOutline, isNull); // Shouldn't be sent yet, file is not open.

    await openFile(mainFileUri, content);
    await pumpEventQueue(times: 5000);
    expect(mainOutline, isNotNull); // Should have been sent now.
  }
}
