// 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/protocol/protocol_generated.dart' as server;
import 'package:analysis_server/src/protocol/protocol_internal.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin_common;
import 'package:analyzer_plugin/protocol/protocol_generated.dart'
    as analyzer_plugin;
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
import 'package:analyzer_testing/resource_provider_mixin.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(JsonTest);
    defineReflectiveTests(JsonWithConvertedFilePathsTest);
  });
}

@reflectiveTest
class JsonTest {
  final decoder = ResponseDecoder(null);

  void test_fromJson() {
    var json = {'offset': 0, 'length': 1, 'label': 'x'};
    var label = ClosingLabel.fromJson(
      decoder,
      '',
      json,
      clientUriConverter: null,
    );
    expect(label.label, 'x');
    expect(label.offset, 0);
    expect(label.length, 1);
  }

  void test_toJson() {
    var closingLabel = ClosingLabel(0, 1, 'x');
    var json = closingLabel.toJson(clientUriConverter: null);

    expect(json, {'offset': 0, 'length': 1, 'label': 'x'});
  }
}

@reflectiveTest
class JsonWithConvertedFilePathsTest with ResourceProviderMixin {
  final decoder = ResponseDecoder(null);
  late ClientUriConverter clientUriConverter;

  void setUp() {
    // These tests use a dummy encoder that just prefixes the FilePaths with
    // "Encoded" and "Decoded" to simplify testing. The real implementation will
    // convert between file paths and URI strings.
    clientUriConverter = _PrefixingUriConverter();
  }

  void tearDown() {
    // Because this is currently global, restore after the tests.
    clientUriConverter = ClientUriConverter.noop(pathContext);
  }

  void test_fromJson_filePath_list() {
    var json = {
      'files': ['/my/file/1', '/my/file/2'],
    };
    var params = AnalysisFlushResultsParams.fromJson(
      decoder,
      '',
      json,
      clientUriConverter: clientUriConverter,
    );
    expect(params.files, ['Decoded /my/file/1', 'Decoded /my/file/2']);
  }

  void test_fromJson_filePath_map_keyValue() {
    var json = {
      'included': [],
      'excluded': [],
      'packageRoots': {'/my/file/key': '/my/file/value'},
    };
    var params = AnalysisSetAnalysisRootsParams.fromJson(
      decoder,
      '',
      json,
      clientUriConverter: clientUriConverter,
    );
    expect(params.packageRoots, {
      'Decoded /my/file/key': 'Decoded /my/file/value',
    });
  }

  void test_fromJson_filePath_map_value_list() {
    var json = {
      'subscriptions': {
        'CLOSING_LABELS': ['/my/file/value'],
      },
    };
    var params = AnalysisSetSubscriptionsParams.fromJson(
      decoder,
      '',
      json,
      clientUriConverter: clientUriConverter,
    );
    expect(params.subscriptions, {
      AnalysisService.CLOSING_LABELS: ['Decoded /my/file/value'],
    });
  }

  void test_fromJson_filePath_nested() {
    var json = {
      'fixes': [
        {
          'error': {
            'severity': 'INFO',
            'type': 'LINT',
            'location': {
              'file': '/my/file',
              'offset': 0,
              'length': 1,
              'startLine': 2,
              'startColumn': 3,
            },
            'message': 'x',
            'code': 'y',
          },
          'fixes': [],
        },
      ],
    };
    var result = EditGetFixesResult.fromJson(
      decoder,
      '',
      json,
      clientUriConverter: clientUriConverter,
    );
    expect(result.fixes.single.error.location.file, 'Decoded /my/file');
  }

  void test_fromJson_filePath_topLevel() {
    var json = {'file': '/my/file', 'labels': []};
    var params = AnalysisClosingLabelsParams.fromJson(
      decoder,
      '',
      json,
      clientUriConverter: clientUriConverter,
    );
    expect(params.file, 'Decoded /my/file');
  }

  /// Verify that when a Path/URI encoder is set (done in [setUp]), it does not
  /// apply to common classes used within plugin protocol classes.
  void test_nestedCommonClasses_analyzerPlugin_noEncoder_fromJson_noDecoding() {
    var fixes = analyzer_plugin.AnalysisErrorFixes.fromJson(decoder, '', {
      'error': {
        'severity': 'ERROR',
        'type': 'COMPILE_TIME_ERROR',
        'location': {
          'file': '/my/file',
          'offset': 1,
          'length': 2,
          'startLine': 3,
          'startColumn': 4,
        },
        'message': 'message',
        'code': 'code',
      },
      'fixes': [],
    });
    // Expect file was not prefixed with "Decoded".
    expect(fixes.error.location.file, '/my/file');
  }

  /// Verify that when a Path/URI encoder is set (done in [setUp]), it does not
  /// apply to common classes used within plugin protocol classes.
  void test_nestedCommonClasses_analyzerPlugin_noEncoder_toJson_noEncoding() {
    var fixes = analyzer_plugin.AnalysisErrorFixes(
      plugin_common.AnalysisError(
        plugin_common.AnalysisErrorSeverity.ERROR,
        plugin_common.AnalysisErrorType.COMPILE_TIME_ERROR,
        plugin_common.Location('/my/file', 1, 2, 3, 4),
        'message',
        'code',
      ),
    );
    // Expect file was not prefixed with "Encoded".
    expect(
      (fixes.toJson() as dynamic)['error']['location']['file'],
      '/my/file',
    );
  }

  /// Verify that when a Path/URI encoder is set (done in [setUp]), it does
  /// apply to common classes used within server protocol classes.
  void test_nestedCommonClasses_server_withEncoder_fromJson_hasDecoding() {
    var fixes = server.AnalysisErrorFixes.fromJson(decoder, '', {
      'error': {
        'severity': 'ERROR',
        'type': 'COMPILE_TIME_ERROR',
        'location': {
          'file': '/my/file',
          'offset': 1,
          'length': 2,
          'startLine': 3,
          'startColumn': 4,
        },
        'message': 'message',
        'code': 'code',
      },
      'fixes': [],
    }, clientUriConverter: clientUriConverter);
    // Expect file was prefixed with "Decoded".
    expect(fixes.error.location.file, 'Decoded /my/file');
  }

  /// Verify that when a Path/URI encoder is set (done in [setUp]), it does
  /// apply to common classes used within server protocol classes.
  void test_nestedCommonClasses_server_withEncoder_toJson_hasEncoding() {
    var fixes = server.AnalysisErrorFixes(
      plugin_common.AnalysisError(
        plugin_common.AnalysisErrorSeverity.ERROR,
        plugin_common.AnalysisErrorType.COMPILE_TIME_ERROR,
        plugin_common.Location('/my/file', 1, 2, 3, 4),
        'message',
        'code',
      ),
    );
    // Expect file was prefixed with "Encoded".
    expect(
      (fixes.toJson(clientUriConverter: clientUriConverter)
          as dynamic)['error']['location']['file'],
      'Encoded /my/file',
    );
  }

  void test_toJson_list() {
    var params = AnalysisFlushResultsParams(['/my/file/1', '/my/file/2']);
    var json = params.toJson(clientUriConverter: clientUriConverter);

    expect(json, {
      'files': ['Encoded /my/file/1', 'Encoded /my/file/2'],
    });
  }

  void test_toJson_map_keyValue() {
    var params = AnalysisSetAnalysisRootsParams(
      [],
      [],
      packageRoots: {'/my/file/key': '/my/file/value'},
    );
    var json = params.toJson(clientUriConverter: clientUriConverter);
    expect(json, {
      'included': [],
      'excluded': [],
      'packageRoots': {'Encoded /my/file/key': 'Encoded /my/file/value'},
    });
  }

  void test_toJson_map_value_list() {
    var params = AnalysisSetSubscriptionsParams({
      AnalysisService.CLOSING_LABELS: ['/my/file/value'],
    });
    var json = params.toJson(clientUriConverter: clientUriConverter);
    expect(json, {
      'subscriptions': {
        'CLOSING_LABELS': ['Encoded /my/file/value'],
      },
    });
  }

  void test_toJson_nested() {
    var result = EditGetFixesResult([
      AnalysisErrorFixes(
        AnalysisError(
          AnalysisErrorSeverity.INFO,
          AnalysisErrorType.LINT,
          Location('/my/file', 0, 1, 2, 3),
          'x',
          'y',
        ),
      ),
    ]);
    var json = result.toJson(clientUriConverter: clientUriConverter);
    expect(json, {
      'fixes': [
        {
          'error': {
            'severity': 'INFO',
            'type': 'LINT',
            'location': {
              'file': 'Encoded /my/file',
              'offset': 0,
              'length': 1,
              'startLine': 2,
              'startColumn': 3,
            },
            'message': 'x',
            'code': 'y',
          },
          'fixes': [],
        },
      ],
    });
  }

  void test_toJson_topLevel() {
    var closingLabelParams = AnalysisClosingLabelsParams('/my/file', []);
    var json = closingLabelParams.toJson(
      clientUriConverter: clientUriConverter,
    );

    expect(json, {'file': 'Encoded /my/file', 'labels': []});
  }
}

/// A [ClientUriConverter] that just prefixes the input string for testing.
class _PrefixingUriConverter implements ClientUriConverter {
  @override
  String fromClientFilePath(String filePathOrUri) => 'Decoded $filePathOrUri';

  @override
  dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError();

  @override
  String toClientFilePath(String filePath) => 'Encoded $filePath';
}
