// 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:checked_yaml/checked_yaml.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:test/test.dart';

import 'pub_utils.dart';

const defaultPubspec = {
  'name': 'sample',
  'environment': {'sdk': '>=2.12.0 <3.0.0'},
};

String _encodeJson(Object? input) =>
    const JsonEncoder.withIndent(' ').convert(input);

Matcher _throwsParsedYamlException(String prettyValue) => throwsA(
      const TypeMatcher<ParsedYamlException>().having(
        (e) {
          final message = e.formattedMessage;
          printOnFailure("Actual error format:\nr'''\n$message'''");
          _printDebugParsedYamlException(e);
          return message;
        },
        'formattedMessage',
        prettyValue,
      ),
    );

void _printDebugParsedYamlException(ParsedYamlException e) {
  var innerError = e.innerError;
  StackTrace? innerStack;

  if (innerError is CheckedFromJsonException) {
    final cfje = innerError;

    if (cfje.innerError != null) {
      innerError = cfje.innerError;
      innerStack = cfje.innerStack;
    }
  }

  if (innerError != null) {
    final items = [innerError];
    if (innerStack != null) {
      items.add(Trace.format(innerStack));
    }

    final content =
        LineSplitter.split(items.join('\n')).map((e) => '  $e').join('\n');

    printOnFailure('Inner error details:\n$content');
  }
}

Future<Pubspec> parse(
  Object? content, {
  bool quietOnError = false,
  bool skipTryPub = false,
  bool lenient = false,
}) async {
  final encoded = _encodeJson(content);

  ProcResult? pubResult;
  if (!skipTryPub) {
    // ignore: deprecated_member_use
    pubResult = await tryPub(encoded);
    expect(pubResult, isNotNull);
  }

  try {
    final value = Pubspec.parse(encoded, lenient: lenient);

    if (pubResult != null) {
      addTearDown(() {
        expect(
          pubResult!.cleanParse,
          isTrue,
          reason:
              'On success, parsing from the pub client should also succeed.',
        );
      });
    }
    return value;
  } catch (e) {
    if (pubResult != null) {
      addTearDown(() {
        expect(
          pubResult!.cleanParse,
          isFalse,
          reason: 'On failure, parsing from the pub client should also fail.',
        );
      });
    }
    if (e is ParsedYamlException) {
      if (!quietOnError) {
        _printDebugParsedYamlException(e);
      }
    }
    rethrow;
  }
}

void expectParseThrows(
  Object? content,
  String expectedError, {
  bool skipTryPub = false,
  bool lenient = false,
}) =>
    expect(
      () => parse(
        content,
        lenient: lenient,
        quietOnError: true,
        skipTryPub: skipTryPub,
      ),
      _throwsParsedYamlException(expectedError),
    );

void expectParseThrowsContaining(
  Object? content,
  String errorFragment, {
  bool skipTryPub = false,
  bool lenient = false,
}) {
  expect(
    () => parse(
      content,
      lenient: lenient,
      quietOnError: true,
      skipTryPub: skipTryPub,
    ),
    _throwsParsedYamlExceptionContaining(errorFragment),
  );
}

// ignore: prefer_expression_function_bodies
Matcher _throwsParsedYamlExceptionContaining(String errorFragment) {
  return throwsA(
    const TypeMatcher<ParsedYamlException>().having(
      (e) {
        final message = e.formattedMessage;
        printOnFailure("Actual error format:\nr'''\n$message'''");
        _printDebugParsedYamlException(e);
        return message;
      },
      'formattedMessage',
      contains(errorFragment),
    ),
  );
}
