// Copyright (c) 2024, 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(ImportTest);
  });
}

@reflectiveTest
class ImportTest extends AbstractLspAnalysisServerTest {
  /// A helper for creating files for multi-level-parts tests.
  ({Uri uri, String filePath, TestCode code}) createFile(
    String filename,
    String content,
  ) {
    assert(filename.endsWith('.dart'));
    var filePath = join(projectFolderPath, 'lib', filename);
    var code = TestCode.parse(content);
    newFile(filePath, code.code);
    return (uri: toUri(filePath), filePath: filePath, code: code);
  }

  Future<void> test_annotationConstructor() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
class A {
  const A();
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

@A^()
void foo() {}
'''),
    );
  }

  Future<void> test_annotationConstructor_prefixed() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
class A {
  const A();
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart' as p;!]

@p.A^()
void foo() {}
'''),
    );
  }

  Future<void> test_annotationVariable() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
const value = 42;
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

@value^
void foo() {}
'''),
    );
  }

  Future<void> test_annotationVariable_prefixed() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
const value = 42;
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart' as p;!]

@p.value^
void foo() {}
'''),
    );
  }

  Future<void> test_constant() async {
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'dart:math';!]

void foo() {
  print(p^i);
}
'''),
    );
  }

  Future<void> test_constant_alias() async {
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'dart:math' as math;!]

void foo() {
  print(math.p^i);
}
'''),
    );
  }

  Future<void> test_extension_getter() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
extension E on int {
  int get bar => 42;
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

void f(int i) {
  i.ba^r;
}
'''),
    );
  }

  Future<void> test_extension_nestedInvocations() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
extension E on int {
  void bar() {}
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

var a = 1.abs().ba^r();
'''),
    );
  }

  Future<void> test_extension_setter() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
extension E on int {
  set foo(int _) {}
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

void f(int i) {
  i.fo^o = 0;
}
'''),
    );
  }

  Future<void> test_extensionOverride_getter() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
extension E on int {
  int get bar => 42;
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

void f(int i) {
  E(i).ba^r;
}
'''),
    );
  }

  Future<void> test_extensionOverride_method() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
extension E on int {
  void bar() {}
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

var a = E(1).ba^r();
'''),
    );
  }

  Future<void> test_extensionOverride_setter() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
extension E on int {
  set foo(int _) {}
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

void f(int i) {
  E(i).fo^o = 0;
}
'''),
    );
  }

  Future<void> test_function() async {
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'dart:math';!]

void foo() {
  ma^x(1, 2);
}
'''),
    );
  }

  Future<void> test_function_alias() async {
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'dart:math' as math;!]

void foo() {
  math.ma^x(1, 2);
}
'''),
    );
  }

  Future<void> test_import_double() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
export 'dart:math';
''');
    await _verifyGoToImports(
      TestCode.parse('''
/*[0*/import 'dart:math';/*0]*/
/*[1*/import 'other.dart';/*1]*/

Rando^m? r;
'''),
    );
  }

  Future<void> test_import_double_ambiguous() async {
    newFile(join(projectFolderPath, 'lib', 'a1.dart'), '''
class A {}
''');
    newFile(join(projectFolderPath, 'lib', 'a2.dart'), '''
class A {}
''');
    await _verifyGoToImports(
      TestCode.parse('''
/*[0*/import 'a1.dart';/*0]*/
/*[1*/import 'a2.dart';/*1]*/

// ignore: ambiguous_import
^A? r;
'''),
    );
  }

  Future<void> test_import_double_hide() async {
    newFile(join(projectFolderPath, 'lib', 'a1.dart'), '''
class A {}
''');
    newFile(join(projectFolderPath, 'lib', 'a2.dart'), '''
class A {}
''');
    await _verifyGoToImports(
      TestCode.parse('''
import 'a1.dart' hide A;
[!import 'a2.dart';!]

^A? r;
'''),
    );
  }

  Future<void> test_import_double_same_different_alias() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
export 'dart:math';
''');
    await _verifyGoToImports(
      TestCode.parse('''
import 'dart:math' as math;
[!import 'other.dart' as other;!]

other.Rando^m? r;
'''),
    );
  }

  Future<void> test_import_double_same_different_alias_prefix() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
export 'dart:math';
''');
    await _verifyGoToImports(
      TestCode.parse('''
import 'dart:math' as math;
[!import 'other.dart' as other;!]

other.Ran^dom? r;
'''),
    );
  }

  Future<void> test_import_double_show() async {
    newFile(join(projectFolderPath, 'lib', 'a1.dart'), '''
class A {}

class B {}
''');
    newFile(join(projectFolderPath, 'lib', 'a2.dart'), '''
class A {}
''');
    await _verifyGoToImports(
      TestCode.parse('''
import 'a1.dart' show B;
[!import 'a2.dart' show A;!]

^A? r;
'''),
    );
  }

  Future<void> test_import_double_unambiguous_aliased() async {
    newFile(join(projectFolderPath, 'lib', 'a1.dart'), '''
class A {}
''');
    newFile(join(projectFolderPath, 'lib', 'a2.dart'), '''
class A {}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'a1.dart' as a;!]
import 'a2.dart' as b;

// ignore: ambiguous_import
a.A^? r;
'''),
    );
  }

  Future<void> test_import_multiLevelParts() async {
    // Create a tree of files that all import 'dart:math' and ensure we find
    // only the import from the parent (not a grandparent, sibling, or child).
    //
    //
    // - root                      has import
    //     - level1_other          has import
    //     - level1                has import, is the used reference
    //         - level2_other      has import
    //         - level2            has reference
    //             - level3_other  has import

    createFile('root.dart', '''
import 'dart:math';
part 'level1_other.dart';
part 'level1.dart';
''');

    createFile('level1_other.dart', '''
part of 'root.dart';
import 'dart:math';
''');

    var level1 = createFile('level1.dart', '''
part of 'root.dart';
[!import 'dart:math';!]
part 'level2_other.dart';
part 'level2.dart';
''');

    createFile('level2_other.dart', '''
part of 'level1.dart';
import 'dart:math';
''');

    var level2 = createFile('level2.dart', '''
part of 'level1.dart';
part 'level3_other.dart';

Rando^m? r;
''');

    createFile('level3_other.dart', '''
part of 'level2.dart';
import 'dart:math';
''');

    await _verifyGoToImports(
      // Test the position in level2.
      level2.code,
      fileUri: level2.uri,
      // Expect only the nearest parent import (level1).
      expecting: (level1.uri, level1.code.ranges),
    );
  }

  Future<void> test_import_multiLevelParts_findsInGrandParent() async {
    var root = createFile('root.dart', '''
[!import 'dart:math';!]
part 'level1.dart';
''');

    createFile('level1.dart', '''
part of 'root.dart';
part 'level2.dart';
''');

    var level2 = createFile('level2.dart', '''
part of 'level1.dart';
Rando^m? r;''');

    await _verifyGoToImports(
      // Test the position in level2, expect reference in root.
      level2.code,
      fileUri: level2.uri,
      expecting: (root.uri, root.code.ranges),
    );
  }

  Future<void> test_import_multiLevelParts_findsInParent() async {
    createFile('root.dart', '''
part 'level1.dart';
''');

    var level1 = createFile('level1.dart', '''
part of 'root.dart';
[!import 'dart:math';!]
part 'level2.dart';
''');

    var level2 = createFile('level2.dart', '''
part of 'level1.dart';
Rando^m? r;''');

    await _verifyGoToImports(
      // Test the position in level2, expect reference in level1.
      level2.code,
      fileUri: level2.uri,
      expecting: (level1.uri, level1.code.ranges),
    );
  }

  Future<void> test_import_multiLevelParts_findsInSelf() async {
    createFile('root.dart', '''
part 'level1.dart';
''');

    createFile('level1.dart', '''
part of 'root.dart';
part 'level2.dart';
''');

    var level2 = createFile('level2.dart', '''
part of 'level1.dart';
[!import 'dart:math';!]
Rando^m? r;''');

    await _verifyGoToImports(
      // Test the position in level2, expect reference in same.
      level2.code,
      fileUri: level2.uri,
      expecting: (level2.uri, level2.code.ranges),
    );
  }

  Future<void> test_import_multiple() async {
    await _verifyGoToImports(
      TestCode.parse('''
import 'dart:async' as import1;
import 'dart:async' as import2;
/*[0*/import 'dart:async';/*0]*/
/*[1*/import 'dart:core';/*1]*/
import 'dart:math';

Futur^e<void>? f;
'''),
    );
  }

  Future<void> test_import_part() async {
    var otherFileUri = toUri(join(projectFolderPath, 'lib', 'other.dart'));
    var main = TestCode.parse('''
[!import 'dart:math';!]

part '$otherFileUri';
''');
    newFile(mainFilePath, main.code);
    await _verifyGoToImports(
      TestCode.parse('''
part of '$mainFileUri';

Rando^m? r;
'''),
      fileUri: otherFileUri,
      expecting: (mainFileUri, main.ranges),
    );
  }

  Future<void> test_import_single() async {
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'dart:math';!]

Rando^m? r;
'''),
    );
  }

  Future<void> test_import_single_aliased() async {
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'dart:math' as math;!]

math.Rando^m? r;
'''),
    );
  }

  Future<void> test_import_single_exported() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
export 'dart:math';
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

Rando^m? r;
'''),
    );
  }

  Future<void> test_local_no_result() async {
    await _verifyGoToImports(
      TestCode.parse('''
import 'dart:math';

Random? r;
L^ocalClass? l;

class LocalClass {}
'''),
    );
  }

  Future<void> test_nestedInvocations() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
class A {
  const A();
  A foo() => A();
  void bar() {}
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
import 'other.dart';

var a = A().foo().ba^r();
'''),
    );
  }

  Future<void> test_staticDeclarations() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
class A {
  static const a = 1;
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart';!]

var a = A^.a;
'''),
    );
  }

  Future<void> test_staticDeclarations_prefixed() async {
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
class A {
  static const a = 1;
}
''');
    await _verifyGoToImports(
      TestCode.parse('''
[!import 'other.dart' as o;!]

var a = o.A^.a;
'''),
    );
  }

  Future<void> _verifyGoToImports(
    TestCode code, {
    Uri? fileUri,
    (Uri, List<TestCodeRange>)? expecting,
  }) async {
    expecting ??= (fileUri ?? mainFileUri, code.ranges);
    var (expectedUri, expectedRanges) = expecting;
    var expectedLocations = [
      for (var range in expectedRanges)
        Location(uri: expectedUri, range: range.range),
    ];

    newFile(fromUri(fileUri ?? mainFileUri), code.code);
    await initialize();
    await initialAnalysis;
    var res = await getImports(fileUri ?? mainFileUri, code.position.position);

    expect(res ?? [], expectedLocations);
  }
}
