// Copyright (c) 2018, 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:convert';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:language_server_protocol/json_parsing.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

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

@reflectiveTest
class JsonTest {
  var start = Position(line: 1, character: 1);
  var end = Position(line: 2, character: 2);
  late var range = Range(start: start, end: end);

  void test_canParse_enumValue_bad() {
    expect(MarkupKind.canParse('NotAMarkupKind', nullLspJsonReporter), isFalse);
  }

  void test_canParse_enumValue_good() {
    expect(MarkupKind.canParse('plaintext', throwingLspJsonReporter), isTrue);
  }

  void test_canParse_enumValue_unknownValueForUnrestrictedEnum_good() {
    expect(SymbolKind.canParse(-1, throwingLspJsonReporter), isTrue);
  }

  void test_canParse_ignoresUnknownFields() {
    expect(
      RenameFileOptions.canParse({
        'overwrite': true,
        'invalidField': true,
      }, throwingLspJsonReporter),
      isTrue,
    );
    expect(
      RenameFileOptions.canParse({
        'overwrite': 1,
        'invalidField': true,
      }, nullLspJsonReporter),
      isFalse,
    );
  }

  void test_canParse_literalStrings_bad() {
    // The CreateFile type is defined with `{ kind: 'create' }` so the only
    // allowed value for `kind` is "create".
    var canParse = CreateFile.canParse({
      'kind': 'not-create',
      'uri': 'file:///temp/foo',
    }, nullLspJsonReporter);
    expect(canParse, isFalse);
  }

  void test_canParse_literalStrings_good() {
    // The CreateFile type is defined with `{ kind: 'create' }` so the only
    // allowed value for `kind` is "create".
    var canParse = CreateFile.canParse({
      'kind': 'create',
      'uri': 'file:///temp/foo',
    }, throwingLspJsonReporter);
    expect(canParse, isTrue);
  }

  void test_canParse_nulls() {
    // The only required field in InitializeParams is capabilities, and all
    // of the fields on that are optional.
    var canParse = InitializeParams.canParse({
      'processId': null,
      'rootUri': null,
      'capabilities': <String, Object>{},
    }, throwingLspJsonReporter);
    expect(canParse, isTrue);
  }

  void test_canParse_optionalFields() {
    expect(
      RenameFileOptions.canParse(<String, Object>{}, throwingLspJsonReporter),
      isTrue,
    );
    expect(
      RenameFileOptions.canParse({'overwrite': true}, throwingLspJsonReporter),
      isTrue,
    );
    expect(
      RenameFileOptions.canParse({'overwrite': 1}, nullLspJsonReporter),
      isFalse,
    );
  }

  void test_canParse_reports_nestedFieldsOfWrongType() {
    var reporter = LspJsonReporter('params');
    expect(
      CompletionParams.canParse({
        'position': {'line': 1, 'character': 1},
        'textDocument': {'uri': 1},
      }, reporter),
      isFalse,
    );
    expect(reporter.errors, hasLength(greaterThanOrEqualTo(1)));
    expect(
      reporter.errors.first,
      equals('params.textDocument.uri must be of type Uri'),
    );
  }

  void test_canParse_reports_nestedNullFields() {
    var reporter = LspJsonReporter('params');
    expect(
      CompletionParams.canParse({
        'position': {'line': 1, 'character': 1},
        'textDocument': {'uri': null},
      }, reporter),
      isFalse,
    );
    expect(reporter.errors, hasLength(greaterThanOrEqualTo(1)));
    expect(
      reporter.errors.first,
      equals('params.textDocument.uri must not be null'),
    );
  }

  void test_canParse_reports_nestedUndefinedFields() {
    var reporter = LspJsonReporter('params');
    expect(
      CompletionParams.canParse({
        'position': {'line': 1, 'character': 1},
        'textDocument': <String, dynamic>{},
      }, reporter),
      isFalse,
    );
    expect(reporter.errors, hasLength(greaterThanOrEqualTo(1)));
    expect(
      reporter.errors.first,
      equals('params.textDocument.uri must not be undefined'),
    );
  }

  void test_canParse_reports_nulls() {
    var reporter = LspJsonReporter('params');
    expect(CreateFile.canParse({'kind': null}, reporter), isFalse);
    expect(reporter.errors, hasLength(1));
    expect(reporter.errors.first, equals('params.kind must not be null'));
  }

  void test_canParse_reports_typeNotInAllowedTypes() {
    var reporter = LspJsonReporter('params');
    expect(
      WorkspaceEdit.canParse({
        'documentChanges': {'uri': 1},
      }, reporter),
      isFalse,
    );
    expect(reporter.errors, hasLength(greaterThanOrEqualTo(1)));
    expect(
      reporter.errors.first,
      equals(
        'params.documentChanges must be of type List<Either4<CreateFile, DeleteFile, RenameFile, TextDocumentEdit>>',
      ),
    );
  }

  void test_canParse_reports_undefineds() {
    var reporter = LspJsonReporter('params');
    expect(CreateFile.canParse(<String, dynamic>{}, reporter), isFalse);
    expect(reporter.errors, hasLength(1));
    expect(reporter.errors.first, equals('params.kind must not be undefined'));
  }

  void test_canParse_reports_wrongTypes_nonSpecTypes() {
    var reporter = LspJsonReporter('params');
    expect(RenameFileOptions.canParse({'overwrite': 1}, reporter), isFalse);
    expect(reporter.errors, hasLength(1));
    expect(
      reporter.errors.first,
      equals('params.overwrite must be of type bool'),
    );
  }

  void test_canParse_reports_wrongTypes_specTypes() {
    var reporter = LspJsonReporter('params');
    expect(ClientCapabilities.canParse({'textDocument': 1}, reporter), isFalse);
    expect(
      reporter.errors.single,
      equals(
        'params.textDocument must be of type TextDocumentClientCapabilities',
      ),
    );
  }

  void test_canParse_unionsOfLiterals() {
    // Key = value to test
    // Value whether expected to parse
    const testTraceValues = {
      'off': true,
      'message': false,
      'messages': true,
      'verbose': true,
      null: true,
      'invalid': false,
    };
    for (var entry in testTraceValues.entries) {
      var testValue = entry.key;
      var expected = entry.value;
      var reporter = expected ? throwingLspJsonReporter : nullLspJsonReporter;
      var canParse = InitializeParams.canParse({
        'processId': null,
        'rootUri': null,
        'capabilities': <String, Object>{},
        'trace': testValue,
      }, reporter);
      expect(
        canParse,
        expected,
        reason:
            'InitializeParams.canParse returned $canParse with a '
            '"trace" value of "$testValue" but expected $expected',
      );
    }
  }

  void test_fromJson_ignoresUnknownFields() {
    var input = '{"id":1,"invalidField":true,"method":"foo","jsonrpc":"test"}';
    var message = RequestMessage.fromJson(
      jsonDecode(input) as Map<String, Object?>,
    );
    expect(message.id.valueEquals(1), isTrue);
    expect(message.method, equals(Method('foo')));
    expect(message.params, isNull);
    expect(message.jsonrpc, equals('test'));
  }

  void test_fromJson_integersInDoubleFields() {
    var input = '{"alpha":1.0,"blue":0,"green":1,"red":1.5}';
    var message = Color.fromJson(jsonDecode(input) as Map<String, Object?>);
    expect(message.alpha, 1.0);
    expect(message.blue, 0.0);
    expect(message.green, 1.0);
    expect(message.red, 1.5);
  }

  void test_fromJson_subtypes() {
    // Create some JSON that includes a VersionedTextDocumentIdentifier but
    // where the class definition only references a TextDocumentIdentifier.
    var input = jsonEncode(
      TextDocumentPositionParams(
        textDocument: VersionedTextDocumentIdentifier(
          version: 111,
          uri: Uri.file('/foo/bar.dart'),
        ),
        position: Position(line: 1, character: 1),
      ).toJson(),
    );
    var params = TextDocumentPositionParams.fromJson(
      jsonDecode(input) as Map<String, Object?>,
    );
    expect(
      params.textDocument,
      const TypeMatcher<VersionedTextDocumentIdentifier>(),
    );
  }

  void test_fromJson_union_left() {
    var input = '{"id":1,"method":"shutdown","jsonrpc":"test"}';
    var message = RequestMessage.fromJson(
      jsonDecode(input) as Map<String, Object?>,
    );
    expect(message.id, equals(Either2<num, String>.t1(1)));
    expect(message.id.valueEquals(1), isTrue);
    expect(message.jsonrpc, 'test');
    expect(message.method, Method.shutdown);
  }

  void test_fromJson_union_null_notificationMessage() {
    var input = '{"method":"test","jsonrpc":"test"}';
    var message = NotificationMessage.fromJson(
      jsonDecode(input) as Map<String, Object?>,
    );
    expect(message.params, isNull);
  }

  void test_fromJson_union_null_responseMessage() {
    var input = '{"id":null,"jsonrpc":"test"}';
    var message = ResponseMessage.fromJson(
      jsonDecode(input) as Map<String, Object?>,
    );
    expect(message.id, isNull);
  }

  void test_fromJson_union_right() {
    var input = '{"id":"one","method":"shutdown","jsonrpc":"test"}';
    var message = RequestMessage.fromJson(
      jsonDecode(input) as Map<String, Object?>,
    );
    expect(message.id, equals(Either2<num, String>.t2('one')));
    expect(message.id.valueEquals('one'), isTrue);
    expect(message.jsonrpc, 'test');
    expect(message.method, Method.shutdown);
  }

  void test_roundTrip_enums() {
    var obj = FoldingRange(
      startLine: 1,
      startCharacter: 2,
      endLine: 3,
      endCharacter: 4,
      kind: FoldingRangeKind.Comment,
    );
    var json = jsonEncode(obj);
    var restoredObj = FoldingRange.fromJson(
      jsonDecode(json) as Map<String, Object?>,
    );

    expect(restoredObj.startLine, equals(obj.startLine));
    expect(restoredObj.startCharacter, equals(obj.startCharacter));
    expect(restoredObj.endLine, equals(obj.endLine));
    expect(restoredObj.endCharacter, equals(obj.endCharacter));
    expect(restoredObj.kind, equals(obj.kind));
  }

  void test_roundTrip_lists() {
    var workspaceFolders = [
      WorkspaceFolder(uri: Uri.parse('http://example.org/1'), name: '!name1'),
      WorkspaceFolder(uri: Uri.parse('http://example.org/2'), name: '!name2'),
    ];
    var obj = InitializeParams(
      processId: 1,
      clientInfo: ClientInfo(name: 'server name', version: '1.2.3'),
      rootPath: '!root',
      capabilities: ClientCapabilities(),
      trace: TraceValue.Off,
      workspaceFolders: workspaceFolders,
    );
    var json = jsonEncode(obj);
    var restoredObj = InitializeParams.fromJson(
      jsonDecode(json) as Map<String, Object?>,
    );
    var restoredWorkspaceFolders = restoredObj.workspaceFolders!;

    expect(restoredWorkspaceFolders, hasLength(workspaceFolders.length));
    for (var i = 0; i < workspaceFolders.length; i++) {
      expect(
        restoredWorkspaceFolders[i].name,
        equals(workspaceFolders[i].name),
      );
      expect(restoredWorkspaceFolders[i].uri, equals(workspaceFolders[i].uri));
    }
  }

  void test_roundTrip_maps() {
    var obj = WorkspaceEdit(
      changes: <Uri, List<TextEdit>>{
        Uri.file('/fileA'): [TextEdit(range: range, newText: 'text A')],
        Uri.file('/fileB'): [TextEdit(range: range, newText: 'text B')],
      },
    );
    var json = jsonEncode(obj);
    var restoredObj = WorkspaceEdit.fromJson(
      jsonDecode(json) as Map<String, Object?>,
    );

    expect(restoredObj.documentChanges, equals(obj.documentChanges));
    expect(restoredObj.changes, equals(obj.changes));
    expect(restoredObj.changes!.keys, equals(obj.changes!.keys));
    expect(restoredObj.changes!.values, equals(obj.changes!.values));
  }

  void test_toJson_enum() {
    var foldingRange = FoldingRange(
      startLine: 1,
      startCharacter: 2,
      endLine: 3,
      endCharacter: 4,
      kind: FoldingRangeKind.Comment,
    );
    var output = json.encode(foldingRange.toJson());
    var expected =
        '''{
      "endCharacter":4,
      "endLine":3,
      "kind":"comment",
      "startCharacter":2,
      "startLine":1
    }'''
            .replaceAll(RegExp('[ \n]'), '');
    expect(output, equals(expected));
  }

  void test_toJson_enum_list() {
    var kind = ClientCompletionItemOptionsKind(
      valueSet: [CompletionItemKind.Color],
    );
    var json = kind.toJson();
    expect(
      json['valueSet'],
      // The list should contain the toJson (string/int) representation of
      // the color, and not the CompletionItemKind itself.
      equals([CompletionItemKind.Color.toJson()]),
    );
  }

  void test_toJson_lists() {
    var location = Location(
      uri: Uri.parse('http://example.org/'),
      range: range,
    );
    var codeAction = Diagnostic(
      range: range,
      severity: DiagnosticSeverity.Error,
      code: 'test_err',
      source: '/tmp/source.dart',
      message: 'err!!',
      relatedInformation: [
        DiagnosticRelatedInformation(location: location, message: 'message'),
      ],
    );
    var output = json.encode(codeAction.toJson());
    var expected =
        '''{
      "code":"test_err",
      "message":"err!!",
      "range":{
          "end":{"character":2,"line":2},
          "start":{"character":1,"line":1}
      },
      "relatedInformation":[
          {
            "location":{
                "range":{
                  "end":{"character":2,"line":2},
                  "start":{"character":1,"line":1}
                },
                "uri":"http://example.org/"
            },
            "message":"message"
          }
      ],
      "severity":1,
      "source":"/tmp/source.dart"
    }'''
            .replaceAll(RegExp('[ \n]'), '');
    expect(output, equals(expected));
  }

  void test_toJson_nestedUnion() {
    var message = ResponseMessage(
      id: Either2<int, String>.t1(1),
      result: Either2<Either2<List<Location>, Location>, List<LocationLink>>.t1(
        Either2<List<Location>, Location>.t1([
          Location(range: range, uri: Uri.parse('http://example.org/')),
        ]),
      ),
      jsonrpc: jsonRpcVersion,
    );
    var output = json.encode(message.toJson());
    expect(
      output,
      equals(
        '{"id":1,"jsonrpc":"2.0",'
        '"result":[{"range":{"end":{"character":2,"line":2},"start":{"character":1,"line":1}},'
        '"uri":"http://example.org/"}]}',
      ),
    );
  }

  void test_toJson_nestedUnion_map() {
    var message = ResponseMessage(
      id: Either2<int, String>.t1(1),
      result: Either2<Either2<List<Location>, Location>, List<LocationLink>>.t1(
        Either2<List<Location>, Location>.t1([
          Location(range: range, uri: Uri.parse('http://example.org/')),
        ]),
      ),
      jsonrpc: jsonRpcVersion,
    );
    var map = message.toJson();
    expect(
      map,
      equals({
        'id': 1,
        'jsonrpc': '2.0',
        'result': [
          {
            'range': {
              'end': {'character': 2, 'line': 2},
              'start': {'character': 1, 'line': 1},
            },
            'uri': 'http://example.org/',
          },
        ],
      }),
    );
  }

  void test_toJson_responseMessage_noError() {
    var id = Either2<int, String>.t1(1);
    var result = 'my result';
    var resp = ResponseMessage(id: id, result: result, jsonrpc: jsonRpcVersion);
    var jsonMap = resp.toJson();
    expect(jsonMap, contains('result'));
    expect(jsonMap, isNot(contains('error')));
  }

  void test_toJson_responseMessage_noResult_error() {
    var id = Either2<int, String>.t1(1);
    var error = ResponseError(code: ErrorCodes.ParseError, message: 'Error');
    var resp = ResponseMessage(id: id, error: error, jsonrpc: jsonRpcVersion);
    var jsonMap = resp.toJson();
    expect(jsonMap, contains('error'));
    expect(jsonMap, isNot(contains('result')));
  }

  void test_toJson_responseMessage_nullResult_noError() {
    var id = Either2<int, String>.t1(1);
    var resp = ResponseMessage(id: id, jsonrpc: jsonRpcVersion);
    var jsonMap = resp.toJson();
    expect(jsonMap, contains('result'));
    expect(jsonMap, isNot(contains('error')));
  }

  void test_toJson_responseMessage_throws_resultAndError() {
    var id = Either2<int, String>.t1(1);
    var result = 'my result';
    var error = ResponseError(code: ErrorCodes.ParseError, message: 'Error');
    var resp = ResponseMessage(
      id: id,
      result: result,
      error: error,
      jsonrpc: jsonRpcVersion,
    );
    expect(resp.toJson, throwsA(TypeMatcher<String>()));
  }

  void test_toJson_union() {
    var num = Either2.t1(1);
    var string = Either2.t2('Test');
    expect(json.encode(num.toJson()), equals('1'));
    expect(json.encode(string.toJson()), equals('"Test"'));
  }

  void test_toJson_unionContainingInterfaceType() {
    var params = Either2<String, TextDocumentItem>.t2(
      TextDocumentItem(
        uri: Uri.parse('http://example.org/'),
        languageId: '!language',
        version: 1,
        text: '!text',
      ),
    );
    var output = json.encode(params);
    expect(
      output,
      equals(
        '{"languageId":"!language","text":"!text","uri":"http://example.org/","version":1}',
      ),
    );
  }

  void test_toJson_unionField() {
    var message = RequestMessage(
      id: Either2<int, String>.t1(1),
      method: Method.shutdown,
      jsonrpc: 'test',
    );
    var output = json.encode(message.toJson());
    expect(output, equals('{"id":1,"jsonrpc":"test","method":"shutdown"}'));
  }

  void test_toJson_untypedLists() {
    var list = <Object>[
      Location(uri: Uri.parse('http://example.org/'), range: range),
      true,
    ];

    var output = specToJson(list);
    expect(
      output,
      equals([
        {
          'range': {
            'end': {'character': 2, 'line': 2},
            'start': {'character': 1, 'line': 1},
          },
          'uri': 'http://example.org/',
        },
        true,
      ]),
    );
  }
}
