// 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:analyzer/dart/element/scope.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
import 'package:analyzer_utilities/testing/tree_string_sink.dart';
import 'package:collection/collection.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../../../util/element_printer.dart';
import '../../dart/resolution/node_text_expectations.dart';
import '../elements_base.dart';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(LibraryFragmentElementTest_keepLinking);
    defineReflectiveTests(LibraryFragmentElementTest_fromBytes);
    defineReflectiveTests(UpdateNodeTextExpectations);
  });
}

abstract class LibraryFragmentElementTest extends ElementsBaseTest {
  test_libraryExports() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
export 'dart:math';
''');

    var library = await buildLibrary(r'''
export 'dart:io';
part 'a.dart';
''');

    checkElementText(library, r'''
library
  reference: <testLibrary>
  fragments
    <testLibraryFragment>
      element: <testLibrary>
      nextFragment: <testLibrary>::@fragment::package:test/a.dart
    <testLibrary>::@fragment::package:test/a.dart
      element: <testLibrary>
      enclosingFragment: <testLibraryFragment>
      previousFragment: <testLibraryFragment>
''');
  }

  test_libraryExports_metadata() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
@deprecated
export 'dart:math';
''');

    var library = await buildLibrary(r'''
part 'a.dart';
''');

    checkElementText(library, r'''
library
  reference: <testLibrary>
  fragments
    <testLibraryFragment>
      element: <testLibrary>
      nextFragment: <testLibrary>::@fragment::package:test/a.dart
    <testLibrary>::@fragment::package:test/a.dart
      element: <testLibrary>
      enclosingFragment: <testLibraryFragment>
      previousFragment: <testLibraryFragment>
''');
  }

  test_libraryImports() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'dart:math';
''');

    var library = await buildLibrary(r'''
import 'dart:io';
part 'a.dart';
''');

    checkElementText(library, r'''
library
  reference: <testLibrary>
  fragments
    <testLibraryFragment>
      element: <testLibrary>
      nextFragment: <testLibrary>::@fragment::package:test/a.dart
      libraryImports
        dart:io
    <testLibrary>::@fragment::package:test/a.dart
      element: <testLibrary>
      enclosingFragment: <testLibraryFragment>
      previousFragment: <testLibraryFragment>
      libraryImports
        dart:math
''');
  }

  test_libraryImports_metadata() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
@deprecated
import 'dart:math';
''');

    var library = await buildLibrary(r'''
part 'a.dart';
''');

    checkElementText(library, r'''
library
  reference: <testLibrary>
  fragments
    <testLibraryFragment>
      element: <testLibrary>
      nextFragment: <testLibrary>::@fragment::package:test/a.dart
    <testLibrary>::@fragment::package:test/a.dart
      element: <testLibrary>
      enclosingFragment: <testLibraryFragment>
      previousFragment: <testLibraryFragment>
      libraryImports
        dart:math
          metadata
            Annotation
              atSign: @ @21
              name: SimpleIdentifier
                token: deprecated @22
                element: dart:core::<fragment>::@getter::deprecated#element
                staticType: null
              element2: dart:core::<fragment>::@getter::deprecated#element
''');
  }

  test_scope_accessibleExtensions_imported() async {
    newFile('$testPackageLibPath/x.dart', r'''
extension X on int {}
''');

    newFile('$testPackageLibPath/y.dart', r'''
extension Y on int {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'y.dart';
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart';
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(withAccessibleExtensions: true, library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
      Uri.parse('package:test/b.dart'),
    ], [], r'''
package:test/test.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
package:test/a.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
    package:test/y.dart::@extension::Y
package:test/aa.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
    package:test/y.dart::@extension::Y
package:test/b.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
''');
  }

  test_scope_accessibleExtensions_imported_withPrefix() async {
    newFile('$testPackageLibPath/x.dart', r'''
extension X on int {}
''');

    newFile('$testPackageLibPath/y.dart', r'''
extension Y on int {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'y.dart' as y;
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart' as x;
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(withAccessibleExtensions: true, library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
      Uri.parse('package:test/b.dart'),
    ], [], r'''
package:test/test.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
package:test/a.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
    package:test/y.dart::@extension::Y
package:test/aa.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
    package:test/y.dart::@extension::Y
package:test/b.dart
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
''');
  }

  test_scope_accessibleExtensions_local() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
part 'aa.dart';
extension A on int {}
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
extension B on int {}
''');

    var library = await buildLibrary(r'''
part 'a.dart';
extension Z on int {}
''');

    _assertScopeLookups(withAccessibleExtensions: true, library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
    ], [], r'''
package:test/test.dart
  accessibleExtensions
    <testLibrary>::@extension::A
    <testLibrary>::@extension::B
    dart:core::@extension::EnumName
    <testLibrary>::@extension::Z
package:test/a.dart
  accessibleExtensions
    <testLibrary>::@extension::A
    <testLibrary>::@extension::B
    dart:core::@extension::EnumName
    <testLibrary>::@extension::Z
package:test/aa.dart
  accessibleExtensions
    <testLibrary>::@extension::A
    <testLibrary>::@extension::B
    dart:core::@extension::EnumName
    <testLibrary>::@extension::Z
''');
  }

  test_scope_accessibleExtensions_unnamed() async {
    var library = await buildLibrary(r'''
part 'a.dart';
extension on int {}
''');

    _assertScopeLookups(withAccessibleExtensions: true, library, [
      Uri.parse('package:test/test.dart'),
    ], [
      ''
    ], r'''
package:test/test.dart
  <empty>
    getter: <null>
  accessibleExtensions
    <testLibrary>::@extension::0
    dart:core::@extension::EnumName
''');
  }

  test_scope_hasPrefix() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io' as prefix;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'prefix.exitCode',
    ], r'''
package:test/test.dart
  prefix.exitCode
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
package:test/a.dart
  prefix.exitCode
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
''');
  }

  test_scope_hasPrefix_append() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'dart:math' as prefix;
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
part 'aaa.dart';
''');

    newFile('$testPackageLibPath/aaa.dart', r'''
part of 'aa.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io' as prefix;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
      Uri.parse('package:test/aaa.dart'),
    ], [
      'prefix.File',
      'prefix.Random',
    ], r'''
package:test/test.dart
  prefix.File
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: <null>
package:test/a.dart
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:math::@class::Random
package:test/aa.dart
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:math::@class::Random
package:test/aaa.dart
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:math::@class::Random
''');
  }

  test_scope_hasPrefix_append_skipFile() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
import 'dart:math' as prefix;
''');

    var library = await buildLibrary(r'''
import 'dart:io' as prefix;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
    ], [
      'prefix.File',
      'prefix.Random',
    ], r'''
package:test/test.dart
  prefix.File
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: <null>
package:test/a.dart
  prefix.File
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: <null>
package:test/aa.dart
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/aa.dart::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibrary>::@fragment::package:test/aa.dart::@prefix2::prefix
    getter: dart:math::@class::Random
''');
  }

  test_scope_hasPrefix_deferred() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'dart:math' deferred as prefix;
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
import 'dart:math' deferred as prefix;
''');

    var library = await buildLibrary(r'''
import 'dart:io' deferred as prefix;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
    ], [
      'loadLibrary',
      'prefix.File',
      'prefix.Random',
    ], r'''
package:test/test.dart
  loadLibrary
    getter: <null>
  prefix.File
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Random
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: <null>
package:test/a.dart
  loadLibrary
    getter: <null>
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: <null>
  prefix.Random
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:math::@class::Random
package:test/aa.dart
  loadLibrary
    getter: <null>
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/aa.dart::@prefix2::prefix
    getter: <null>
  prefix.Random
    prefix: <testLibrary>::@fragment::package:test/aa.dart::@prefix2::prefix
    getter: dart:math::@class::Random
''');
  }

  test_scope_hasPrefix_lookup_ambiguous_notSdk_both() async {
    newFile('$testPackageLibPath/a.dart', r'''
var foo = 0;
''');

    newFile('$testPackageLibPath/b.dart', r'''
var foo = 1.2;
''');

    var library = await buildLibrary(r'''
import 'a.dart' as prefix;
import 'b.dart' as prefix;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'prefix.foo',
    ], r'''
package:test/test.dart
  prefix.foo
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: multiplyDefinedElement
      package:test/a.dart::<fragment>::@getter::foo#element
      package:test/b.dart::<fragment>::@getter::foo#element
    setter: multiplyDefinedElement
      package:test/a.dart::<fragment>::@setter::foo#element
      package:test/b.dart::<fragment>::@setter::foo#element
''');
  }

  test_scope_hasPrefix_lookup_ambiguous_notSdk_first() async {
    newFile('$testPackageLibPath/a.dart', r'''
var pi = 4;
''');

    var library = await buildLibrary(r'''
import 'a.dart' as prefix;
import 'dart:math' as prefix;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'prefix.pi',
    ], r'''
package:test/test.dart
  prefix.pi
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: package:test/a.dart::<fragment>::@getter::pi#element
    setter: package:test/a.dart::<fragment>::@setter::pi#element
''');
  }

  test_scope_hasPrefix_lookup_ambiguous_notSdk_second() async {
    newFile('$testPackageLibPath/a.dart', r'''
var pi = 4;
''');

    var library = await buildLibrary(r'''
import 'dart:math' as prefix;
import 'a.dart' as prefix;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'prefix.pi',
    ], r'''
package:test/test.dart
  prefix.pi
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: package:test/a.dart::<fragment>::@getter::pi#element
    setter: package:test/a.dart::<fragment>::@setter::pi#element
''');
  }

  test_scope_hasPrefix_lookup_ambiguous_same() async {
    newFile('$testPackageLibPath/a.dart', r'''
var foo = 0;
''');

    newFile('$testPackageLibPath/b.dart', r'''
export 'a.dart';
''');

    var library = await buildLibrary(r'''
import 'a.dart' as prefix;
import 'b.dart' as prefix;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'prefix.foo',
    ], r'''
package:test/test.dart
  prefix.foo
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: package:test/a.dart::<fragment>::@getter::foo#element
    setter: package:test/a.dart::<fragment>::@setter::foo#element
''');
  }

  test_scope_hasPrefix_lookup_differentPrefix() async {
    newFile('$testPackageLibPath/a.dart', r'''
var foo = 0;
''');

    newFile('$testPackageLibPath/b.dart', r'''
var bar = 0;
''');

    var library = await buildLibrary(r'''
import 'a.dart' as prefix;
import 'b.dart' as prefix2;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'prefix.foo',
      'prefix.bar',
      'prefix2.foo',
      'prefix2.bar',
    ], r'''
package:test/test.dart
  prefix.foo
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: package:test/a.dart::<fragment>::@getter::foo#element
    setter: package:test/a.dart::<fragment>::@setter::foo#element
  prefix.bar
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: <null>
  prefix2.foo
    prefix2: <testLibraryFragment>::@prefix2::prefix2
    getter: <null>
  prefix2.bar
    prefix2: <testLibraryFragment>::@prefix2::prefix2
    getter: package:test/b.dart::<fragment>::@getter::bar#element
    setter: package:test/b.dart::<fragment>::@setter::bar#element
''');
  }

  test_scope_hasPrefix_lookup_notFound() async {
    var library = await buildLibrary(r'''
import 'dart:math' as math;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'math.noSuchElement',
    ], r'''
package:test/test.dart
  math.noSuchElement
    math: <testLibraryFragment>::@prefix2::math
    getter: <null>
''');
  }

  test_scope_hasPrefix_lookup_respectsCombinator_hide() async {
    var library = await buildLibrary(r'''
import 'dart:math' as math hide sin;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'math.sin',
      'math.cos',
    ], r'''
package:test/test.dart
  math.sin
    math: <testLibraryFragment>::@prefix2::math
    getter: <null>
  math.cos
    math: <testLibraryFragment>::@prefix2::math
    getter: dart:math::@function::cos
''');
  }

  test_scope_hasPrefix_lookup_respectsCombinator_show() async {
    var library = await buildLibrary(r'''
import 'dart:math' as math show sin;
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
    ], [
      'math.sin',
      'math.cos',
    ], r'''
package:test/test.dart
  math.sin
    math: <testLibraryFragment>::@prefix2::math
    getter: dart:math::@function::sin
  math.cos
    math: <testLibraryFragment>::@prefix2::math
    getter: <null>
''');
  }

  test_scope_hasPrefix_shadow() async {
    newFile('$testPackageLibPath/x.dart', r'''
class Directory {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'x.dart' as prefix;
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
part 'aaa.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io' as prefix;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
    ], [
      'prefix.File',
      'prefix.Directory',
    ], r'''
package:test/test.dart
  prefix.File
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Directory
    prefix: <testLibraryFragment>::@prefix2::prefix
    getter: dart:io::@class::Directory
package:test/a.dart
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Directory
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: package:test/x.dart::@class::Directory
package:test/aa.dart
  prefix.File
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: dart:io::@class::File
  prefix.Directory
    prefix: <testLibrary>::@fragment::package:test/a.dart::@prefix2::prefix
    getter: package:test/x.dart::@class::Directory
''');
  }

  test_scope_localShadowsPrefix() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
void foo() {}
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io' as foo;
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/b.dart'),
    ], [
      'foo'
    ], r'''
package:test/test.dart
  foo
    getter: <testLibrary>::@function::foo
package:test/a.dart
  foo
    getter: <testLibrary>::@function::foo
package:test/b.dart
  foo
    getter: <testLibrary>::@function::foo
''');
  }

  test_scope_noPrefix_combinators_hide() async {
    newFile('$testPackageLibPath/x.dart', r'''
class A {}
class B {}
class C {}
class D {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart' hide A, C;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'A',
      'B',
      'C',
      'D',
    ], r'''
package:test/test.dart
  A
    getter: <null>
  B
    getter: package:test/x.dart::@class::B
  C
    getter: <null>
  D
    getter: package:test/x.dart::@class::D
package:test/a.dart
  A
    getter: <null>
  B
    getter: package:test/x.dart::@class::B
  C
    getter: <null>
  D
    getter: package:test/x.dart::@class::D
''');
  }

  test_scope_noPrefix_combinators_hide_show() async {
    newFile('$testPackageLibPath/x.dart', r'''
class A {}
class B {}
class C {}
class D {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart' hide A, C show B;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'A',
      'B',
      'C',
      'D',
    ], r'''
package:test/test.dart
  A
    getter: <null>
  B
    getter: package:test/x.dart::@class::B
  C
    getter: <null>
  D
    getter: <null>
package:test/a.dart
  A
    getter: <null>
  B
    getter: package:test/x.dart::@class::B
  C
    getter: <null>
  D
    getter: <null>
''');
  }

  test_scope_noPrefix_combinators_show() async {
    newFile('$testPackageLibPath/x.dart', r'''
class A {}
class B {}
class C {}
class D {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart' show A, C;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'A',
      'B',
      'C',
      'D',
    ], r'''
package:test/test.dart
  A
    getter: package:test/x.dart::@class::A
  B
    getter: <null>
  C
    getter: package:test/x.dart::@class::C
  D
    getter: <null>
package:test/a.dart
  A
    getter: package:test/x.dart::@class::A
  B
    getter: <null>
  C
    getter: package:test/x.dart::@class::C
  D
    getter: <null>
''');
  }

  test_scope_noPrefix_combinators_show_gide() async {
    newFile('$testPackageLibPath/x.dart', r'''
class A {}
class B {}
class C {}
class D {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart' show A, C hide B, C;
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'A',
      'B',
      'C',
      'D',
    ], r'''
package:test/test.dart
  A
    getter: package:test/x.dart::@class::A
  B
    getter: <null>
  C
    getter: <null>
  D
    getter: <null>
package:test/a.dart
  A
    getter: package:test/x.dart::@class::A
  B
    getter: <null>
  C
    getter: <null>
  D
    getter: <null>
''');
  }

  test_scope_noPrefix_fragmentImportShadowsParent() async {
    newFile('$testPackageLibPath/x.dart', r'''
int get exitCode => 0;
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'x.dart';
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io';
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
      Uri.parse('package:test/b.dart'),
    ], [
      'exitCode',
    ], r'''
package:test/test.dart
  exitCode
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
package:test/a.dart
  exitCode
    getter: package:test/x.dart::<fragment>::@getter::exitCode#element
package:test/aa.dart
  exitCode
    getter: package:test/x.dart::<fragment>::@getter::exitCode#element
package:test/b.dart
  exitCode
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
''');
  }

  test_scope_noPrefix_implicitDartCore() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'Object',
    ], r'''
package:test/test.dart
  Object
    getter: dart:core::@class::Object
package:test/a.dart
  Object
    getter: dart:core::@class::Object
''');
  }

  test_scope_noPrefix_inheritsFromParentFragment_fromDefining() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io';
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      'exit',
      'exitCode',
    ], r'''
package:test/test.dart
  exit
    getter: dart:io::@function::exit
  exitCode
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
package:test/a.dart
  exit
    getter: dart:io::@function::exit
  exitCode
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
''');
  }

  test_scope_noPrefix_inheritsFromParentFragment_fromPart() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
import 'dart:io';
part 'aa.dart';
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
      Uri.parse('package:test/b.dart'),
    ], [
      'exitCode',
    ], r'''
package:test/test.dart
  exitCode
    getter: <null>
package:test/a.dart
  exitCode
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
package:test/aa.dart
  exitCode
    getter: dart:io::<fragment>::@getter::exitCode#element
    setter: dart:io::<fragment>::@setter::exitCode#element
package:test/b.dart
  exitCode
    getter: <null>
''');
  }

  test_scope_noPrefix_localShadowsImported_getter() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
int get exitCode => 0;
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io';
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/b.dart'),
    ], [
      'exitCode',
    ], r'''
package:test/test.dart
  exitCode
    getter: <testLibrary>::@fragment::package:test/a.dart::@getter::exitCode#element
package:test/a.dart
  exitCode
    getter: <testLibrary>::@fragment::package:test/a.dart::@getter::exitCode#element
package:test/b.dart
  exitCode
    getter: <testLibrary>::@fragment::package:test/a.dart::@getter::exitCode#element
''');
  }

  test_scope_noPrefix_localShadowsImported_setter() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
set exitCode(int _) {}
''');

    newFile('$testPackageLibPath/b.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'dart:io';
part 'a.dart';
part 'b.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/b.dart'),
    ], [
      'exitCode',
    ], r'''
package:test/test.dart
  exitCode
    getter: <null>
    setter: <testLibrary>::@fragment::package:test/a.dart::@setter::exitCode#element
package:test/a.dart
  exitCode
    getter: <null>
    setter: <testLibrary>::@fragment::package:test/a.dart::@setter::exitCode#element
package:test/b.dart
  exitCode
    getter: <null>
    setter: <testLibrary>::@fragment::package:test/a.dart::@setter::exitCode#element
''');
  }

  test_scope_noPrefix_localsOfFragments() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
part 'aa.dart';
class A {}
''');

    newFile('$testPackageLibPath/aa.dart', r'''
part of 'a.dart';
class B {}
''');

    var library = await buildLibrary(r'''
part 'a.dart';
class Z {}
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
      Uri.parse('package:test/aa.dart'),
    ], [
      'A',
      'B',
      'Z',
    ], r'''
package:test/test.dart
  A
    getter: <testLibrary>::@class::A
  B
    getter: <testLibrary>::@class::B
  Z
    getter: <testLibrary>::@class::Z
package:test/a.dart
  A
    getter: <testLibrary>::@class::A
  B
    getter: <testLibrary>::@class::B
  Z
    getter: <testLibrary>::@class::Z
package:test/aa.dart
  A
    getter: <testLibrary>::@class::A
  B
    getter: <testLibrary>::@class::B
  Z
    getter: <testLibrary>::@class::Z
''');
  }

  test_scope_noPrefix_localsOfFragments_private() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
class _A {}
set _foo(int _) {}
''');

    var library = await buildLibrary(r'''
part 'a.dart';
class _Z {}
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      '_A',
      '_Z',
      '_foo',
    ], r'''
package:test/test.dart
  _A
    getter: <testLibrary>::@class::_A
  _Z
    getter: <testLibrary>::@class::_Z
  _foo
    getter: <null>
    setter: <testLibrary>::@fragment::package:test/a.dart::@setter::_foo#element
package:test/a.dart
  _A
    getter: <testLibrary>::@class::_A
  _Z
    getter: <testLibrary>::@class::_Z
  _foo
    getter: <null>
    setter: <testLibrary>::@fragment::package:test/a.dart::@setter::_foo#element
''');
  }

  test_scope_wildcardName_class() async {
    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
class _ {}
''');

    var library = await buildLibrary(r'''
part 'a.dart';
''');

    _assertScopeLookups(library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      '_'
    ], r'''
package:test/test.dart
  _
    getter: <testLibrary>::@class::_
package:test/a.dart
  _
    getter: <testLibrary>::@class::_
''');
  }

  test_scope_wildcardName_importPrefix() async {
    newFile('$testPackageLibPath/x.dart', r'''
extension X on int {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
import 'x.dart' as _;
part 'a.dart';
''');

    _assertScopeLookups(withAccessibleExtensions: true, library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      '_.X'
    ], r'''
package:test/test.dart
  _.X
    _: <null>
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
package:test/a.dart
  _.X
    _: <null>
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
''');
  }

  test_scope_wildcardName_importPrefix_preWildcardVariables() async {
    newFile('$testPackageLibPath/x.dart', r'''
extension X on int {}
''');

    newFile('$testPackageLibPath/a.dart', r'''
part of 'test.dart';
''');

    var library = await buildLibrary(r'''
// @dart=3.5
import 'x.dart' as _;
part 'a.dart';
''');

    _assertScopeLookups(withAccessibleExtensions: true, library, [
      Uri.parse('package:test/test.dart'),
      Uri.parse('package:test/a.dart'),
    ], [
      '_.X'
    ], r'''
package:test/test.dart
  _.X
    _: <testLibraryFragment>::@prefix2::_
    getter: package:test/x.dart::@extension::X
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
package:test/a.dart
  _.X
    _: <testLibraryFragment>::@prefix2::_
    getter: package:test/x.dart::@extension::X
  accessibleExtensions
    dart:core::@extension::EnumName
    package:test/x.dart::@extension::X
''');
  }

  void _assertScopeLookups(
    LibraryElementImpl library,
    List<Uri> fragmentUris,
    List<String> requests,
    String expected, {
    bool withAccessibleExtensions = false,
  }) {
    var buffer = StringBuffer();

    var sink = TreeStringSink(
      sink: buffer,
      indent: '',
    );

    var elementPrinter = ElementPrinter(
      sink: sink,
      configuration: ElementPrinterConfiguration(),
    );

    for (var fragmentUri in fragmentUris) {
      sink.writelnWithIndent(fragmentUri);
      sink.withIndent(() {
        var fragment = library.units.singleWhere((fragment) {
          return fragment.source.uri == fragmentUri;
        });

        for (var request in requests) {
          var periodIndex = request.indexOf('.');
          var (prefixName, rawName) = switch (periodIndex) {
            > 0 => (
                request.substring(0, periodIndex),
                request.substring(periodIndex + 1),
              ),
            _ => (null, request),
          };

          void writeResult(ScopeLookupResult result) {
            sink.withIndent(() {
              elementPrinter.writeNamedElement2('getter', result.getter2);
              if (result.setter2 case var setter?) {
                elementPrinter.writeNamedElement2('setter', setter);
              }
            });
          }

          sink.writelnWithIndent(
            request.ifNotEmptyOrElse('<empty>'),
          );

          if (prefixName != null) {
            var prefixLookup = fragment.scope.lookup(prefixName);
            expect(prefixLookup.setter2, isNull);
            var importPrefix = prefixLookup.getter2;
            if (importPrefix == null) {
              sink.withIndent(() {
                elementPrinter.writeNamedElement2(prefixName, importPrefix);
              });
            } else {
              importPrefix as PrefixElementImpl2;
              sink.withIndent(() {
                elementPrinter.writeNamedElement2(prefixName, importPrefix);
              });
              var result = importPrefix.scope.lookup(rawName);
              writeResult(result);
            }
          } else {
            var result = fragment.scope.lookup(rawName);
            writeResult(result);
          }
        }

        if (withAccessibleExtensions) {
          elementPrinter.writeElementList2(
            'accessibleExtensions',
            fragment.accessibleExtensions2.sortedBy((e) => e.name3 ?? ''),
          );
        }
      });
    }

    var actual = buffer.toString();
    if (actual != expected) {
      print('-------- Actual --------');
      print('$actual------------------------');
      NodeTextExpectationsCollector.add(actual);
    }
    expect(actual, expected);
  }
}

@reflectiveTest
class LibraryFragmentElementTest_fromBytes extends LibraryFragmentElementTest {
  @override
  bool get keepLinkingLibraries => false;
}

@reflectiveTest
class LibraryFragmentElementTest_keepLinking
    extends LibraryFragmentElementTest {
  @override
  bool get keepLinkingLibraries => true;
}
