// Copyright (c) 2026, 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/src/lsp/constants.dart';
import 'package:analyzer_testing/package_config_file_builder.dart';
import 'package:language_server_protocol/protocol_custom_generated.dart';
import 'package:language_server_protocol/protocol_generated.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'server_abstract.dart';

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

@reflectiveTest
class ExhaustiveFlutterWidgetPreviewsTest
    extends AbstractLspAnalysisServerTest {
  @override
  bool get addFlutterLocalizationsPackageDep => true;

  Future<FlutterWidgetPreviews?> getFlutterWidgetPreviews(Uri uri) {
    var request = makeRequest(
      CustomMethods.getFlutterWidgetPreviews,
      TextDocumentIdentifier(uri: uri),
    );
    return expectSuccessfulResponseTo(request, FlutterWidgetPreviews.fromJson);
  }

  Future<FlutterWidgetPreviews?> getWorkspaceFlutterWidgetPreviews() {
    var request = makeRequest(
      CustomMethods.getWorkspaceFlutterWidgetPreviews,
      null,
    );
    return expectSuccessfulResponseTo(request, FlutterWidgetPreviews.fromJson);
  }

  @override
  void setUp() {
    super.setUp();
    writeTestPackageConfig(flutter: true);
    addFlutter();
    addSkyEngine(sdkPath: sdkRoot.path);
    failTestOnErrorDiagnostic = false;
  }

  Future<void> test_addDeletePreviews() async {
    var filePath = join(projectFolderPath, 'lib', 'previews.dart');
    var fileUri = toUri(filePath);
    newFile(filePath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';

@Preview(name: 'Initial')
Widget preview1() => Text('1');
''');

    await initialize();
    var result = await getFlutterWidgetPreviews(fileUri);
    expect(result!.previews, hasLength(1));
    expect(result.previews.first.functionName, 'preview1');

    // Add a preview
    newFile(filePath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';

@Preview(name: 'Initial')
Widget preview1() => Text('1');

@Preview(name: 'Added')
Widget preview2() => Text('2');
''');

    result = await getFlutterWidgetPreviews(fileUri);
    expect(result!.previews, hasLength(2));
    expect(result.previews.any((p) => p.functionName == 'preview2'), isTrue);

    // Delete a preview
    newFile(filePath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';

@Preview(name: 'Added')
Widget preview2() => Text('2');
''');

    result = await getFlutterWidgetPreviews(fileUri);
    expect(result!.previews, hasLength(1));
    expect(result.previews.first.functionName, 'preview2');
  }

  Future<void> test_annotationProperties() async {
    newFile(join(projectFolderPath, 'lib', 'previews.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';

class MyMultiPreview extends MultiPreview {
  const MyMultiPreview(List<Preview> previews) : super(previews);
}

@Preview(
  name: 'Custom Name',
  group: 'My Group',
)
Widget myPreview() => Text('Hello');

@MyMultiPreview([
  Preview(name: 'Light', brightness: Brightness.light),
  Preview(name: 'Dark', brightness: Brightness.dark),
])
Widget multiPreview() => Text('Multi');
''');

    await initialize();
    var result = await getFlutterWidgetPreviews(
      toUri(join(projectFolderPath, 'lib', 'previews.dart')),
    );

    expect(result, isNotNull);
    expect(result!.previews, hasLength(2));

    var custom = result.previews.firstWhere(
      (p) => p.functionName == 'myPreview',
    );
    expect(custom.previewAnnotation, contains("name: 'Custom Name'"));
    expect(custom.previewAnnotation, contains("group: 'My Group'"));
    expect(custom.isMultiPreview, isFalse);

    var multi = result.previews.firstWhere(
      (p) => p.functionName == 'multiPreview',
    );
    expect(multi.isMultiPreview, isTrue);
    // Since namespacing is applied, we check for the literal value without assuming prefixing.
    expect(multi.previewAnnotation, contains("'Light'"));
    expect(multi.previewAnnotation, contains('Brightness.light'));
    expect(multi.previewAnnotation, contains("'Dark'"));
    expect(multi.previewAnnotation, contains('Brightness.dark'));
  }

  Future<void> test_annotationSourceGeneration() async {
    newFile(join(projectFolderPath, 'lib', 'previews.dart'), '''
    import 'package:flutter/material.dart';
    import 'package:flutter/widget_previews.dart';

    enum MyEnum { a, b }

    class ComplexPreview extends Preview {
    final List<int> list;
    final Map<String, dynamic> map;
    final MyEnum e;
    final (int, {String s}) record;
    final Size? size;

    const ComplexPreview({
    required super.name,
    required this.list,
    required this.map,
    required this.e,
    required this.record,
    this.size,
    });
    }

    @ComplexPreview(
    name: 'Complex',
    list: [1, 2, 3],
    map: {'key': 'value', 'nested': [true, false]},
    e: MyEnum.a,
    record: (1, s: 'hello'),
    size: Size(100, 200),
    )
    Widget complexPreview() => Text('Complex');

    class CustomSize {
    final double value;
    const CustomSize.square(this.value);
    }

    class NamedConstructorPreview extends Preview {
    final CustomSize size;
    const NamedConstructorPreview({required super.name, required this.size});
    }

    @NamedConstructorPreview(
    name: 'Named Constructor',
    size: CustomSize.square(150),
    )
    Widget namedConstructorPreview() => Text('Named');
    ''');

    await initialize();
    var result = await getFlutterWidgetPreviews(
      toUri(join(projectFolderPath, 'lib', 'previews.dart')),
    );

    expect(result, isNotNull);
    expect(result!.previews, hasLength(2));

    var complex = result.previews.firstWhere(
      (p) => p.functionName == 'complexPreview',
    );
    var source = complex.previewAnnotation;

    // Validate that namespaces/prefixes are applied (e.g., _i1.ComplexPreview)
    // and that nested structures are correctly formatted.
    expect(source, contains("name: 'Complex'"));
    expect(source, contains('list: [1, 2, 3]'));
    expect(source, contains("map: {'key': 'value', 'nested': [true, false]}"));
    expect(source, contains('MyEnum.a'));
    expect(source, contains("record: (1, s: 'hello')"));
    expect(source, contains('Size(100.0, 200.0)'));

    var named = result.previews.firstWhere(
      (p) => p.functionName == 'namedConstructorPreview',
    );
    expect(named.previewAnnotation, contains('CustomSize.square(150.0)'));
  }

  Future<void> test_customPreviewTypes() async {
    newFile(join(projectFolderPath, 'lib', 'previews.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';

class MyPreview extends Preview {
  final String customAttribute;
  const MyPreview({required String name, required this.customAttribute}) : super(name: name);
}

@MyPreview(name: 'Custom', customAttribute: 'Some Value')
Widget customPreview() => Text('Custom');
''');

    await initialize();
    var result = await getFlutterWidgetPreviews(
      toUri(join(projectFolderPath, 'lib', 'previews.dart')),
    );

    expect(result, isNotNull);
    expect(result!.previews, hasLength(1));
    var preview = result.previews.first;
    expect(preview.functionName, 'customPreview');
    expect(preview.previewAnnotation, contains("name: 'Custom'"));
    expect(
      preview.previewAnnotation,
      contains("customAttribute: 'Some Value'"),
    );
  }

  Future<void> test_errorsAndPropagation() async {
    var depPath = join(projectFolderPath, 'lib', 'dep.dart');
    var mainPath = join(projectFolderPath, 'lib', 'main.dart');
    var mainUri = toUri(mainPath);

    newFile(depPath, 'int x = "not an int"; // Error');
    newFile(mainPath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
import 'dep.dart';

@Preview(name: 'Has Dep Error')
Widget preview() => Text(x.toString());
''');

    await initialize();
    var result = await getFlutterWidgetPreviews(mainUri);
    expect(result!.previews, hasLength(1));
    var preview = result.previews.first;
    expect(preview.hasError, isFalse);
    expect(preview.dependencyHasErrors, isTrue);

    // Fix error in dep
    newFile(depPath, 'int x = 1;');
    result = await getFlutterWidgetPreviews(mainUri);
    expect(result!.previews.first.dependencyHasErrors, isFalse);

    // Add error to main
    newFile(mainPath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
import 'dep.dart';

@Preview(name: 'Has Local Error')
Widget preview() => Text(x.toString()) // Missing semicolon
''');
    result = await getFlutterWidgetPreviews(mainUri);
    expect(result!.previews.first.hasError, isTrue);
    expect(result.previews.first.dependencyHasErrors, isFalse);
  }

  Future<void> test_parts() async {
    var mainPath = join(projectFolderPath, 'lib', 'main.dart');
    var partPath = join(projectFolderPath, 'lib', 'part.dart');
    var mainUri = toUri(mainPath);

    newFile(mainPath, '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
part 'part.dart';

@Preview(name: 'Main Preview')
Widget mainPreview() => Text('Main');
''');

    newFile(partPath, '''
part of 'main.dart';

@Preview(name: 'Part Preview')
Widget partPreview() => Text('Part');
''');

    await initialize();
    var result = await getFlutterWidgetPreviews(mainUri);
    expect(result!.previews, hasLength(2));
    expect(result.previews.any((p) => p.functionName == 'mainPreview'), isTrue);
    expect(result.previews.any((p) => p.functionName == 'partPreview'), isTrue);
    expect(result.previews.map((p) => p.libraryUri.toString()).toSet(), {
      'package:test/main.dart',
    });
    expect(result.scriptUris, [toUri(mainPath), toUri(partPath)]);
  }

  Future<void> test_pubWorkspace() async {
    // Setup a workspace with two packages
    newFile(join(projectFolderPath, 'pubspec.yaml'), '''
workspace:
  - pkgs/a
  - pkgs/b
''');
    newFile(join(projectFolderPath, 'pkgs', 'a', 'pubspec.yaml'), '''
name: a
environment:
  sdk: ^3.7.0
dependencies:
  flutter:
    sdk: flutter
''');
    newFile(join(projectFolderPath, 'pkgs', 'b', 'pubspec.yaml'), '''
name: b
environment:
  sdk: ^3.7.0
dependencies:
  flutter:
    sdk: flutter
''');

    newFile(join(projectFolderPath, 'pkgs', 'a', 'lib', 'a.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Pkg A')
Widget a() => Text('A');
''');
    newFile(join(projectFolderPath, 'pkgs', 'b', 'lib', 'b.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'Pkg B')
Widget b() => Text('B');
''');

    var config = PackageConfigFileBuilder();
    // Do NOT add 'test' package here as writeTestPackageConfig will add it.
    config.add(name: 'a', rootFolder: getFolder('$projectFolderPath/pkgs/a'));
    config.add(name: 'b', rootFolder: getFolder('$projectFolderPath/pkgs/b'));

    writeTestPackageConfig(config: config, flutter: true);

    await initialize();
    var result = await getWorkspaceFlutterWidgetPreviews();
    expect(result!.previews, hasLength(2));
    expect(result.previews.any((p) => p.packageName == 'a'), isTrue);
    expect(result.previews.any((p) => p.packageName == 'b'), isTrue);
  }

  Future<void> test_workspacePreviews() async {
    newFile(join(projectFolderPath, 'lib', 'a.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'A')
Widget a() => Text('A');
''');
    newFile(join(projectFolderPath, 'lib', 'b.dart'), '''
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
@Preview(name: 'B')
Widget b() => Text('B');
''');

    await initialize();
    var result = await getWorkspaceFlutterWidgetPreviews();
    expect(result!.previews, hasLength(2));
    expect(result.previews.any((p) => p.functionName == 'a'), isTrue);
    expect(result.previews.any((p) => p.functionName == 'b'), isTrue);
  }
}
