// Copyright 2021 Google LLC. 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.

@TestOn('vm')
library;

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

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

import 'embedded_process.dart';
import 'utils.dart';

void main() {
  late EmbeddedProcess process;
  setUp(() async {
    process = await EmbeddedProcess.start();
  });

  group("emits a protocol error", () {
    late OutboundMessage_FileImportRequest request;

    setUp(() async {
      process.send(
        compileString(
          "@use 'other'",
          importers: [
            InboundMessage_CompileRequest_Importer()..fileImporterId = 1,
          ],
        ),
      );

      request = await getFileImportRequest(process);
    });

    test("for a response without a corresponding request ID", () async {
      process.send(
        InboundMessage()
          ..fileImportResponse =
              (InboundMessage_FileImportResponse()..id = request.id + 1),
      );

      await expectParamsError(
        process,
        errorId,
        "Response ID ${request.id + 1} doesn't match any outstanding "
        "requests in compilation $defaultCompilationId.",
      );
      await process.shouldExit(76);
    });

    test("for a response that doesn't match the request type", () async {
      process.send(
        InboundMessage()
          ..canonicalizeResponse =
              (InboundMessage_CanonicalizeResponse()..id = request.id),
      );

      await expectParamsError(
        process,
        errorId,
        "Request ID ${request.id} doesn't match response type "
        "InboundMessage_CanonicalizeResponse in compilation "
        "$defaultCompilationId.",
      );
      await process.shouldExit(76);
    });
  });

  group("emits a compile failure", () {
    late OutboundMessage_FileImportRequest request;

    setUp(() async {
      process.send(
        compileString(
          "@use 'other'",
          importers: [
            InboundMessage_CompileRequest_Importer()..fileImporterId = 1,
          ],
        ),
      );

      request = await getFileImportRequest(process);
    });

    group("for a FileImportResponse with a URL", () {
      test("that's empty", () async {
        process.send(
          InboundMessage()
            ..fileImportResponse = (InboundMessage_FileImportResponse()
              ..id = request.id
              ..fileUrl = ""),
        );

        await _expectUseError(
          process,
          'The file importer must return an absolute URL, was ""',
        );
        await process.close();
      });

      test("that's relative", () async {
        process.send(
          InboundMessage()
            ..fileImportResponse = (InboundMessage_FileImportResponse()
              ..id = request.id
              ..fileUrl = "foo"),
        );

        await _expectUseError(
          process,
          'The file importer must return an absolute URL, was "foo"',
        );
        await process.close();
      });

      test("that's not file:", () async {
        process.send(
          InboundMessage()
            ..fileImportResponse = (InboundMessage_FileImportResponse()
              ..id = request.id
              ..fileUrl = "other:foo"),
        );

        await _expectUseError(
          process,
          'The file importer must return a file: URL, was "other:foo"',
        );
        await process.close();
      });
    });
  });

  group("includes in FileImportRequest", () {
    var compilationId = 1234;
    var importerId = 5679;
    late OutboundMessage_FileImportRequest request;
    setUp(() async {
      process.send(
        compileString(
          "@use 'other'",
          id: compilationId,
          importers: [
            InboundMessage_CompileRequest_Importer()
              ..fileImporterId = importerId,
          ],
        ),
      );
      request = await getFileImportRequest(process);
    });

    test("a known importerId", () async {
      expect(request.importerId, equals(importerId));
      await process.kill();
    });

    test("the imported URL", () async {
      expect(request.url, equals("other"));
      await process.kill();
    });

    test("whether the import came from an @import", () async {
      expect(request.fromImport, isFalse);
      await process.kill();
    });
  });

  test("errors cause compilation to fail", () async {
    process.send(
      compileString(
        "@use 'other'",
        importers: [
          InboundMessage_CompileRequest_Importer()..fileImporterId = 1,
        ],
      ),
    );

    var request = await getFileImportRequest(process);
    process.send(
      InboundMessage()
        ..fileImportResponse = (InboundMessage_FileImportResponse()
          ..id = request.id
          ..error = "oh no"),
    );

    var failure = await getCompileFailure(process);
    expect(failure.message, equals('oh no'));
    expect(failure.span.text, equals("@use 'other'"));
    expect(failure.stackTrace, equals('- 1:1  root stylesheet\n'));
    await process.close();
  });

  test("null results count as not found", () async {
    process.send(
      compileString(
        "@use 'other'",
        importers: [
          InboundMessage_CompileRequest_Importer()..fileImporterId = 1,
        ],
      ),
    );

    var request = await getFileImportRequest(process);
    process.send(
      InboundMessage()
        ..fileImportResponse =
            (InboundMessage_FileImportResponse()..id = request.id),
    );

    var failure = await getCompileFailure(process);
    expect(failure.message, equals("Can't find stylesheet to import."));
    expect(failure.span.text, equals("@use 'other'"));
    await process.close();
  });

  group("attempts importers in order", () {
    test("with multiple file importers", () async {
      process.send(
        compileString(
          "@use 'other'",
          importers: [
            for (var i = 0; i < 10; i++)
              InboundMessage_CompileRequest_Importer()..fileImporterId = i,
          ],
        ),
      );

      for (var i = 0; i < 10; i++) {
        var request = await getFileImportRequest(process);
        expect(request.importerId, equals(i));
        process.send(
          InboundMessage()
            ..fileImportResponse =
                (InboundMessage_FileImportResponse()..id = request.id),
        );
      }

      await process.kill();
    });

    test("with a mixture of file and normal importers", () async {
      process.send(
        compileString(
          "@use 'other'",
          importers: [
            for (var i = 0; i < 10; i++)
              if (i % 2 == 0)
                InboundMessage_CompileRequest_Importer()..fileImporterId = i
              else
                InboundMessage_CompileRequest_Importer()..importerId = i,
          ],
        ),
      );

      for (var i = 0; i < 10; i++) {
        if (i % 2 == 0) {
          var request = await getFileImportRequest(process);
          expect(request.importerId, equals(i));
          process.send(
            InboundMessage()
              ..fileImportResponse =
                  (InboundMessage_FileImportResponse()..id = request.id),
          );
        } else {
          var request = await getCanonicalizeRequest(process);
          expect(request.importerId, equals(i));
          process.send(
            InboundMessage()
              ..canonicalizeResponse =
                  (InboundMessage_CanonicalizeResponse()..id = request.id),
          );
        }
      }

      await process.kill();
    });
  });

  test("tries resolved URL as a relative path first", () async {
    await d.file("upstream.scss", "a {b: c}").create();
    await d.file("midstream.scss", "@use 'upstream';").create();

    process.send(
      compileString(
        "@use 'midstream'",
        importers: [
          for (var i = 0; i < 10; i++)
            InboundMessage_CompileRequest_Importer()..fileImporterId = i,
        ],
      ),
    );

    for (var i = 0; i < 5; i++) {
      var request = await getFileImportRequest(process);
      expect(request.url, equals("midstream"));
      expect(request.importerId, equals(i));
      process.send(
        InboundMessage()
          ..fileImportResponse =
              (InboundMessage_FileImportResponse()..id = request.id),
      );
    }

    var request = await getFileImportRequest(process);
    expect(request.importerId, equals(5));
    process.send(
      InboundMessage()
        ..fileImportResponse = (InboundMessage_FileImportResponse()
          ..id = request.id
          ..fileUrl = p.toUri(d.path("midstream")).toString()),
    );

    await expectSuccess(process, "a { b: c; }");
    await process.close();
  });

  group("handles an importer for a string compile request", () {
    setUp(() async {
      await d.file("other.scss", "a {b: c}").create();
    });

    test("without a base URL", () async {
      process.send(
        compileString(
          "@use 'other'",
          importer: InboundMessage_CompileRequest_Importer()
            ..fileImporterId = 1,
        ),
      );

      var request = await getFileImportRequest(process);
      expect(request.url, equals("other"));

      process.send(
        InboundMessage()
          ..fileImportResponse = (InboundMessage_FileImportResponse()
            ..id = request.id
            ..fileUrl = p.toUri(d.path("other")).toString()),
      );

      await expectSuccess(process, "a { b: c; }");
      await process.close();
    });

    test("with a base URL", () async {
      process.send(
        compileString(
          "@use 'other'",
          url: p.toUri(d.path("input")).toString(),
          importer: InboundMessage_CompileRequest_Importer()
            ..fileImporterId = 1,
        ),
      );

      await expectSuccess(process, "a { b: c; }");
      await process.close();
    });
  });
}

/// Asserts that [process] emits a [CompileFailure] result with the given
/// [message] on its protobuf stream and causes the compilation to fail.
Future<void> _expectUseError(EmbeddedProcess process, Object message) async {
  var failure = await getCompileFailure(process);
  expect(failure.message, equals(message));
  expect(failure.span.text, equals("@use 'other'"));
}
