// 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 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';

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

@reflectiveTest
class ImplementationTest extends AbstractLspAnalysisServerTest {
  Future<void> test_class_excludesSelf() => _testMarkedContent('''
      abstract class ^[!A!] {}
      class B extends A {}
      class C extends A {}
    ''', expectResults: false);

  Future<void> test_class_excludesSuper() => _testMarkedContent('''
      abstract class [!A!] {}
      class ^B extends A {}
      class C extends A {}
    ''', expectResults: false);

  Future<void> test_class_sub() => _testMarkedContent('''
      abstract class ^A {}
      class /*[0*/B/*0]*/ extends A {}
      class /*[1*/C/*1]*/ extends A {}
    ''');

  Future<void> test_class_sub_underscore() => _testMarkedContent('''
      abstract class ^_ {}
      class /*[0*/B/*0]*/ extends _ {}
      class /*[1*/C/*1]*/ extends _ {}
    ''');

  Future<void> test_class_subSub() => _testMarkedContent('''
      abstract class ^A {}
      class /*[0*/B/*0]*/ extends A {}
      class /*[1*/C/*1]*/ extends A {}
      class /*[2*/D/*2]*/ extends B {}
      class /*[3*/E/*3]*/ extends B {}
      class /*[4*/F/*4]*/ extends E {}
    ''');

  Future<void> test_emptyResults() async {
    var content = '';

    await initialize();
    await openFile(mainFileUri, content);
    var res = await getImplementations(mainFileUri, startOfDocPos);

    expect(res, isEmpty);
  }

  Future<void> test_getter_declaring_implementedByDeclaring() =>
      _testMarkedContent('''
      abstract class A(final int val^ue);

      class B(final int /*[0*/value/*0]*/) implements A;
    ''');

  Future<void> test_getter_declaring_implementedByExplicit() =>
      _testMarkedContent('''
      abstract class A(final int va^lue);

      class B implements A {
        final int /*[0*/value/*0]*/;
        B(this.value);
      }

      class C implements A {
        int get /*[1*/value/*1]*/ => 0;
      }
    ''');

  Future<void> test_getter_explicit_implementedByDeclaring() =>
      _testMarkedContent('''
      abstract class A {
        int get c^ount;
      }

      class B(final int /*[0*/count/*0]*/) implements A;
      class C({required final int /*[1*/count/*1]*/}) implements A;
    ''');

  Future<void> test_getter_overriddenByField() => _testMarkedContent('''
      class B extends A {
        final String? [!a!] = null;
      }

      abstract class A {
        String? get a^;
      }
    ''');

  Future<void> test_getter_overriddenByField_underscore() =>
      _testMarkedContent('''
      class B extends A {
        final String? [!_!] = null;
      }

      abstract class A {
        String? get _^;
      }
    ''');

  Future<void> test_method_excludesClassesWithoutImplementations() =>
      _testMarkedContent('''
      abstract class A {
        void ^b() {}
      }

      class B extends A {}

      class [!E!] extends B {}
    ''', expectResults: false);

  Future<void> test_method_excludesSelf() => _testMarkedContent('''
      abstract class A {
        void ^[!b!]();
      }

      class B extends A {
        void b() {}
      }
    ''', expectResults: false);

  Future<void> test_method_excludesSuper() => _testMarkedContent('''
      abstract class A {
        void [!b!]();
      }

      class B extends A {
        void ^b() {}
      }
    ''', expectResults: false);

  Future<void> test_method_fromCallSite() => _testMarkedContent('''
      abstract class A {
        void b();
      }

      class B extends A {
        void /*[0*/b/*0]*/() {}
      }

      class C extends A {
        void /*[1*/b/*1]*/() {}
      }

      class D extends B {
        void /*[2*/b/*2]*/() {}
      }

      class E extends B {}

      class F extends E {
        void /*[3*/b/*3]*/() {}
      }

      void fromCallSite() {
        A e = new E();
        e.^b();
      }
    ''');

  Future<void> test_method_startOfParameterList() => _testMarkedContent('''
      abstract class A {
        void m^();
      }

      class B extends A {
        void [!m!]() {}
      }
    ''');

  Future<void> test_method_startOfTypeParameterList() => _testMarkedContent('''
      abstract class A {
        void m^<T>();
      }

      class B extends A {
        void [!m!]<T>() {}
      }
    ''');

  Future<void> test_method_sub() => _testMarkedContent('''
      abstract class A {
        void ^b();
      }

      class B extends A {
        void /*[0*/b/*0]*/() {}
      }

      class C extends A {
        void /*[1*/b/*1]*/() {}
      }
    ''');

  Future<void> test_method_sub_underscore() => _testMarkedContent('''
      abstract class A {
        void ^_();
      }

      class B extends A {
        void /*[0*/_/*0]*/() {}
      }

      class C extends A {
        void /*[1*/_/*1]*/() {}
      }
    ''');

  Future<void> test_method_subSub() => _testMarkedContent('''
      abstract class A {
        void ^b();
      }

      class B extends A {
        void /*[0*/b/*0]*/() {}
      }

      class D extends B {
        void /*[1*/b/*1]*/() {}
      }

      class E extends B {}

      class F extends E {
        void /*[2*/b/*2]*/() {}
      }
    ''');

  /// Check that implementations that come from mixins in other files return the
  /// correct location for the implementation.
  Future<void> test_mixins() async {
    var mixinsContent = r'''
import 'main.dart';

mixin MyMixin implements MyInterface {
  String get [!interfaceField!] => '';
}
''';
    var content = r'''
import 'other.dart';

class A with MyMixin {}
class B with MyMixin {}
class C implements MyInterface {
  String get [!interfaceField!] => '';
}

abstract class MyInterface {
  String get interf^aceField;
}
''';

    await _testMarkedContent(content, otherContent: mixinsContent);
  }

  Future<void> test_never_issue61420() => _testMarkedContent('''
      Ne^ver value = throw '';
    ''', expectResults: false);

  Future<void> test_nonDartFile() async {
    newFile(pubspecFilePath, simplePubspecContent);
    await initialize();

    var res = await getImplementations(pubspecFileUri, startOfDocPos);
    expect(res, isEmpty);
  }

  Future<void> test_setter_explicit_implementedByDeclaring() =>
      _testMarkedContent('''
      abstract class A {
        set c^ount(int _);
      }

      class B(var int /*[0*/count/*0]*/) implements A;
      class C({required var int /*[1*/count/*1]*/}) implements A;
    ''');

  /// Parses [content] using as [TestCode] and invokes the
  /// `textDocument/implementations` command at the marked position to verify
  /// the marked regions are returned as implementations.
  ///
  /// If [otherContent] is provided, will be written as `other.dart` alongside
  /// the file and any marked regions will also be verified.
  ///
  /// If [expectResults] is `false` then expects none of the ranges marked with
  /// `/*[0*/brackets/*0]*/` to be present in the results instead.
  Future<void> _testMarkedContent(
    String content, {
    String? otherContent,
    bool expectResults = true,
  }) async {
    var otherFilePath = join(projectFolderPath, 'lib', 'other.dart');
    var otherFileUri = pathContext.toUri(otherFilePath);
    var code = TestCode.parse(content);
    var otherCode = otherContent != null ? TestCode.parse(otherContent) : null;
    if (otherCode != null) {
      newFile(otherFilePath, otherCode.code);
    }
    newFile(mainFilePath, code.code);

    await initialize();

    var res = await getImplementations(mainFileUri, code.position.position);

    var expectedLocations = [
      for (final range in code.ranges)
        Location(uri: mainFileUri, range: range.range),
      if (otherCode != null)
        for (final range in otherCode.ranges)
          Location(uri: otherFileUri, range: range.range),
    ];

    if (expectResults) {
      expect(expectedLocations, isNotEmpty);
      expect(res, unorderedEquals(expectedLocations));
    } else {
      for (var location in expectedLocations) {
        expect(res, isNot(contains(location)));
      }
    }
  }
}
