// Copyright 2019 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:path/path.dart' as p;
import 'package:test/test.dart';

import 'package:sass/src/embedded/embedded_sass.pb.dart';
import 'package:sass/src/embedded/utils.dart';
import 'package:sass/src/util/nullable.dart';

import 'embedded_process.dart';

/// An arbitrary compilation ID to use for tests where the specific ID doesn't
/// matter.
const defaultCompilationId = 4321;

/// Returns a (compilation ID, [InboundMessage]) pair that compiles the given
/// plain CSS string.
InboundMessage compileString(
  String css, {
  int? id,
  bool? alertColor,
  bool? alertAscii,
  Syntax? syntax,
  OutputStyle? style,
  String? url,
  bool? sourceMap,
  bool? sourceMapIncludeSources,
  Iterable<InboundMessage_CompileRequest_Importer>? importers,
  InboundMessage_CompileRequest_Importer? importer,
  Iterable<String>? functions,
  Iterable<String>? fatalDeprecations,
  Iterable<String>? futureDeprecations,
  Iterable<String>? silenceDeprecations,
}) {
  var input = InboundMessage_CompileRequest_StringInput()..source = css;
  if (syntax != null) input.syntax = syntax;
  if (url != null) input.url = url;
  if (importer != null) input.importer = importer;

  var request = InboundMessage_CompileRequest()..string = input;
  if (importers != null) request.importers.addAll(importers);
  if (style != null) request.style = style;
  if (sourceMap != null) request.sourceMap = sourceMap;
  if (sourceMapIncludeSources != null) {
    request.sourceMapIncludeSources = sourceMapIncludeSources;
  }
  if (functions != null) request.globalFunctions.addAll(functions);
  if (alertColor != null) request.alertColor = alertColor;
  if (alertAscii != null) request.alertAscii = alertAscii;
  fatalDeprecations.andThen(request.fatalDeprecation.addAll);
  futureDeprecations.andThen(request.futureDeprecation.addAll);
  silenceDeprecations.andThen(request.silenceDeprecation.addAll);
  return InboundMessage()..compileRequest = request;
}

/// Asserts that [process] emits a [ProtocolError] parse error with the given
/// [message] on its protobuf stream and prints a notice on stderr.
Future<void> expectParseError(
  EmbeddedProcess process,
  Object message, {
  int compilationId = defaultCompilationId,
}) async {
  var (actualCompilationId, actualMessage) = await process.outbound.next;
  expect(actualCompilationId, equals(compilationId));
  expect(
    actualMessage,
    isProtocolError(errorId, ProtocolErrorType.PARSE, message),
  );

  var stderrPrefix = "Host caused parse error: ";
  await expectLater(
    process.stderr,
    message is String
        ? emitsInOrder("$stderrPrefix$message".split("\n"))
        : emits(startsWith(stderrPrefix)),
  );
}

/// Asserts that [process] emits a [ProtocolError] params error with the given
/// [message] on its protobuf stream and prints a notice on stderr.
Future<void> expectParamsError(
  EmbeddedProcess process,
  int id,
  Object message, {
  int compilationId = defaultCompilationId,
}) async {
  var (actualCompilationId, actualMessage) = await process.outbound.next;
  expect(actualCompilationId, equals(compilationId));
  expect(actualMessage, isProtocolError(id, ProtocolErrorType.PARAMS, message));

  var stderrPrefix = "Host caused params error"
      "${id == errorId ? '' : " with request $id"}: ";
  await expectLater(
    process.stderr,
    message is String
        ? emitsInOrder("$stderrPrefix$message".split("\n"))
        : emits(startsWith(stderrPrefix)),
  );
}

/// Asserts that an [OutboundMessage] is a [ProtocolError] with the given [id],
/// [type], and optionally [message].
Matcher isProtocolError(int id, ProtocolErrorType type, [Object? message]) =>
    predicate((value) {
      expect(value, isA<OutboundMessage>());
      var outboundMessage = value as OutboundMessage;
      expect(
        outboundMessage.hasError(),
        isTrue,
        reason: "Expected $outboundMessage to be a ProtocolError",
      );
      expect(outboundMessage.error.id, equals(id));
      expect(outboundMessage.error.type, equals(type));
      if (message != null) expect(outboundMessage.error.message, message);
      return true;
    });

/// Asserts [process] emits a `CanonicalizeRequest` with the default compilation
/// ID and returns it.
Future<OutboundMessage_CanonicalizeRequest> getCanonicalizeRequest(
  EmbeddedProcess process,
) async {
  var message = await process.receive();
  expect(
    message.hasCanonicalizeRequest(),
    isTrue,
    reason: "Expected $message to have a CanonicalizeRequest",
  );
  return message.canonicalizeRequest;
}

/// Asserts [process] emits an `ImportRequest` with the default compilation ID
/// and returns it.
Future<OutboundMessage_ImportRequest> getImportRequest(
  EmbeddedProcess process,
) async {
  var message = await process.receive();
  expect(
    message.hasImportRequest(),
    isTrue,
    reason: "Expected $message to have a ImportRequest",
  );
  return message.importRequest;
}

/// Asserts that [process] emits a `FileImportRequest` with the default
/// compilation ID and returns it.
Future<OutboundMessage_FileImportRequest> getFileImportRequest(
  EmbeddedProcess process,
) async {
  var message = await process.receive();
  expect(
    message.hasFileImportRequest(),
    isTrue,
    reason: "Expected $message to have a FileImportRequest",
  );
  return message.fileImportRequest;
}

/// Asserts that [process] emits a `FunctionCallRequest` with the default
/// compilation ID and returns it.
Future<OutboundMessage_FunctionCallRequest> getFunctionCallRequest(
  EmbeddedProcess process,
) async {
  var message = await process.receive();
  expect(
    message.hasFunctionCallRequest(),
    isTrue,
    reason: "Expected $message to have a FunctionCallRequest",
  );
  return message.functionCallRequest;
}

/// Asserts that [process] emits a with the default compilation ID
/// `CompileResponse.Failure` and returns it.
Future<OutboundMessage_CompileResponse_CompileFailure> getCompileFailure(
  EmbeddedProcess process,
) async {
  var response = await getCompileResponse(process);
  expect(
    response.hasFailure(),
    isTrue,
    reason: "Expected $response to be a failure",
  );
  return response.failure;
}

/// Asserts that [process] emits a with the default compilation ID
/// `CompileResponse.Success` and returns it.
Future<OutboundMessage_CompileResponse_CompileSuccess> getCompileSuccess(
  EmbeddedProcess process,
) async {
  var response = await getCompileResponse(process);
  expect(
    response.hasSuccess(),
    isTrue,
    reason: "Expected $response to be a success",
  );
  return response.success;
}

/// Asserts that [process] emits a `CompileResponse` and with the default
/// compilation ID returns it.
Future<OutboundMessage_CompileResponse> getCompileResponse(
  EmbeddedProcess process,
) async {
  var message = await process.receive();
  expect(
    message.hasCompileResponse(),
    isTrue,
    reason: "Expected $message to have a CompileResponse",
  );
  return message.compileResponse;
}

/// Asserts that [process] emits a `LogEvent` and returns with the default
/// compilation ID it.
Future<OutboundMessage_LogEvent> getLogEvent(EmbeddedProcess process) async {
  var message = await process.receive();
  expect(
    message.hasLogEvent(),
    isTrue,
    reason: "Expected $message to have a LogEvent",
  );
  return message.logEvent;
}

/// Asserts that [process] emits a `CompileResponse` with CSS that matches
/// [css], with a source map that matches [sourceMap] (if passed).
///
/// If [css] is a [String], this automatically wraps it in
/// [equalsIgnoringWhitespace].
///
/// If [sourceMap] is a function, `response.success.sourceMap` is passed to it.
/// Otherwise, it's treated as a matcher for `response.success.sourceMap`.
Future<void> expectSuccess(
  EmbeddedProcess process,
  Object css, {
  Object? sourceMap,
}) async {
  var success = await getCompileSuccess(process);
  expect(success.css, css is String ? equalsIgnoringWhitespace(css) : css);
  if (sourceMap is void Function(String)) {
    sourceMap(success.sourceMap);
  } else if (sourceMap != null) {
    expect(success.sourceMap, sourceMap);
  }
}

/// Returns a [SourceSpan_SourceLocation] with the given [offset], [line], and
/// [column].
SourceSpan_SourceLocation location(int offset, int line, int column) =>
    SourceSpan_SourceLocation()
      ..offset = offset
      ..line = line
      ..column = column;

/// Returns a matcher that verifies whether the given value refers to the same
/// path as [expected].
Matcher equalsPath(String expected) => predicate<String>(
      (actual) => p.equals(actual, expected),
      "equals $expected",
    );
