// Copyright 2016 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 'dart:convert';

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

import 'package:sass/sass.dart' as sass;
import 'package:sass/src/io.dart';

import '../../utils.dart';

/// Defines test that are shared between the Dart and Node.js CLI test suites.
void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
  group("for a simple compilation", () {
    late Map<String, dynamic> map;
    setUp(() async {
      await d.file("test.scss", "a {b: 1 + 2}").create();

      await (await runSass(["test.scss", "out.css"])).shouldExit(0);
      map = _readJson("out.css.map");
    });

    test("refers to the source file", () {
      expect(map, containsPair("sources", ["test.scss"]));
    });

    test("refers to the target file", () {
      expect(map, containsPair("file", "out.css"));
    });

    test("contains mappings", () {
      var result = sass.compileStringToResult("a {b: 1 + 2}", sourceMap: true);
      expect(
        map,
        containsPair("mappings", result.sourceMap!.toJson()["mappings"]),
      );
    });
  });

  group("with multiple sources", () {
    setUp(() async {
      await d.file("test.scss", """
        @use 'dir/other';
        x {y: z}
      """).create();
      await d.dir("dir", [d.file("other.scss", "a {b: 1 + 2}")]).create();
    });

    test("refers to them using relative URLs by default", () async {
      await (await runSass(["test.scss", "out.css"])).shouldExit(0);
      expect(
        _readJson("out.css.map"),
        containsPair("sources", ["dir/other.scss", "test.scss"]),
      );
    });

    test(
      "refers to them using relative URLs with --source-map-urls=relative",
      () async {
        await (await runSass([
          "--source-map-urls=relative",
          "test.scss",
          "out.css",
        ]))
            .shouldExit(0);
        expect(
          _readJson("out.css.map"),
          containsPair("sources", ["dir/other.scss", "test.scss"]),
        );
      },
    );

    test(
      "refers to them using absolute URLs with --source-map-urls=absolute",
      () async {
        await (await runSass([
          "--source-map-urls=absolute",
          "test.scss",
          "out.css",
        ]))
            .shouldExit(0);
        expect(
          _readJson("out.css.map"),
          containsPair("sources", [
            p.toUri(canonicalize(d.path("dir/other.scss"))).toString(),
            p.toUri(canonicalize(d.path("test.scss"))).toString(),
          ]),
        );
      },
    );

    test("includes source contents with --embed-sources", () async {
      await (await runSass([
        "--embed-sources",
        "test.scss",
        "out.css",
      ]))
          .shouldExit(0);
      expect(
        _readJson("out.css.map"),
        containsPair("sourcesContent", [
          "a {b: 1 + 2}",
          readFile(d.path("test.scss")),
        ]),
      );
    });
  });

  group("doesn't normalize file case", () {
    setUp(() => d.file("TeSt.scss", "a {b: c}").create());

    test("when loaded with the same case", () async {
      await (await runSass(["TeSt.scss", "out.css"])).shouldExit(0);
      expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"]));
    });

    test("when imported with the same case", () async {
      await d.file("importer.scss", "@use 'TeSt.scss'").create();
      await (await runSass(["importer.scss", "out.css"])).shouldExit(0);
      expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"]));
    });

    // The following tests rely on Windows' case-insensitive filesystem.

    test("when loaded with a different case", () async {
      await (await runSass(["test.scss", "out.css"])).shouldExit(0);
      expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"]));
    }, testOn: "windows");

    test("when imported with a different case", () async {
      await d.file("importer.scss", "@use 'test.scss'").create();
      await (await runSass(["importer.scss", "out.css"])).shouldExit(0);
      expect(_readJson("out.css.map"), containsPair("sources", ["TeSt.scss"]));
    }, testOn: "windows");
  });

  test("includes a source map comment", () async {
    await d.file("test.scss", "a {b: c}").create();
    await (await runSass(["test.scss", "out.css"])).shouldExit(0);
    await d
        .file("out.css", endsWith("\n\n/*# sourceMappingURL=out.css.map */\n"))
        .validate();
  });

  group("in another directory", () {
    setUp(() async {
      await d.dir("in", [d.file("test.scss", "x {y: z}")]).create();
      await (await runSass(["in/test.scss", "out/test.css"])).shouldExit(0);
    });

    test("refers to a source", () {
      expect(
        _readJson("out/test.css.map"),
        containsPair("sources", ["../in/test.scss"]),
      );
    });

    test("includes a source map comment", () async {
      await d
          .file(
            "out/test.css",
            endsWith("\n\n/*# sourceMappingURL=test.css.map */\n"),
          )
          .validate();
    });
  });

  test("with --stdin uses a data: URL", () async {
    var sass = await runSass(["--stdin", "out.css"]);
    sass.stdin.writeln("a {b: c}");
    sass.stdin.close();
    await sass.shouldExit(0);

    expect(
      _readJson("out.css.map"),
      containsPair("sources", [
        Uri.dataFromString("a {b: c}\n", encoding: utf8).toString(),
      ]),
    );
  });

  group("with --no-source-map,", () {
    setUp(() async {
      await d.file("test.scss", "a {b: c}").create();
    });

    test("no source map is generated", () async {
      await (await runSass([
        "--no-source-map",
        "test.scss",
        "out.css",
      ]))
          .shouldExit(0);

      await d.file("out.css", isNot(contains("/*#"))).validate();
      await d.nothing("out.css.map").validate();
    });

    test("--source-map-urls is disallowed", () async {
      var sass = await runSass([
        "--no-source-map",
        "--source-map-urls=absolute",
        "test.scss",
        "out.css",
      ]);
      expect(
        sass.stdout,
        emits("--source-map-urls isn't allowed with --no-source-map."),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });

    test("--embed-sources is disallowed", () async {
      var sass = await runSass([
        "--no-source-map",
        "--embed-sources",
        "test.scss",
        "out.css",
      ]);
      expect(
        sass.stdout,
        emits("--embed-sources isn't allowed with --no-source-map."),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });

    test("--embed-source-map is disallowed", () async {
      var sass = await runSass([
        "--no-source-map",
        "--embed-source-map",
        "test.scss",
        "out.css",
      ]);
      expect(
        sass.stdout,
        emits("--embed-source-map isn't allowed with --no-source-map."),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });
  });

  group("when emitting to stdout", () {
    test("--source-map isn't allowed", () async {
      await d.file("test.scss", "a {b: c}").create();
      var sass = await runSass(["--source-map", "test.scss"]);
      expect(
        sass.stdout,
        emits(
          "When printing to stdout, --source-map requires "
          "--embed-source-map.",
        ),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });

    test("--source-map-urls is disallowed", () async {
      await d.file("test.scss", "a {b: c}").create();
      var sass = await runSass(["--source-map-urls=absolute", "test.scss"]);
      expect(
        sass.stdout,
        emits(
          "When printing to stdout, --source-map-urls requires "
          "--embed-source-map.",
        ),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });

    test("--embed-sources is disallowed", () async {
      await d.file("test.scss", "a {b: c}").create();
      var sass = await runSass(["--embed-sources", "test.scss"]);
      expect(
        sass.stdout,
        emits(
          "When printing to stdout, --embed-sources requires "
          "--embed-source-map.",
        ),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });

    test(
        "--source-map-urls=relative is disallowed even with "
        "--embed-source-map", () async {
      await d.file("test.scss", "a {b: c}").create();
      var sass = await runSass([
        "--source-map-urls=relative",
        "--embed-source-map",
        "test.scss",
      ]);
      expect(
        sass.stdout,
        emits(
          "--source-map-urls=relative isn't allowed when printing to "
          "stdout.",
        ),
      );
      expect(
        sass.stdout,
        emitsThrough(contains("Print this usage information.")),
      );
      await sass.shouldExit(64);
    });

    test("everything is allowed with --embed-source-map", () async {
      await d.file("test.scss", "a {b: c}").create();
      var sass = await runSass([
        "--source-map",
        "--source-map-urls=absolute",
        "--embed-sources",
        "--embed-source-map",
        "test.scss",
      ]);
      var css = (await sass.stdout.rest.toList()).join("\n");
      await sass.shouldExit(0);

      var map = embeddedSourceMap(css);
      expect(map, isNotEmpty);
      expect(map, isNot(contains("file")));
    });
  });

  group("with --embed-source-map", () {
    setUp(() async {
      await d.file("test.scss", "a {b: 1 + 2}").create();
    });

    Map<String, dynamic>? map;
    group("with the target in the same directory", () {
      setUp(() async {
        await (await runSass([
          "--embed-source-map",
          "test.scss",
          "out.css",
        ]))
            .shouldExit(0);
        var css = readFile(d.path("out.css"));
        map = embeddedSourceMap(css);
      });

      test("contains mappings in the generated CSS", () {
        var result = sass.compileStringToResult(
          "a {b: 1 + 2}",
          sourceMap: true,
        );
        expect(
          map,
          containsPair("mappings", result.sourceMap!.toJson()["mappings"]),
        );
      });

      test("refers to the source file", () {
        expect(map, containsPair("sources", ["test.scss"]));
      });

      test("refers to the target file", () {
        expect(map, containsPair("file", "out.css"));
      });

      test("doesn't generate a source map file", () async {
        await d.nothing("out.css.map").validate();
      });

      // Regression test for #457.
      test("--embed-sources works with non-ASCII characters", () async {
        await d.file("test.scss", "a {b: '▼'}").create();
        await (await runSass([
          "--embed-source-map",
          "--embed-sources",
          "test.scss",
          "out.css",
        ]))
            .shouldExit(0);
        var css = readFile(d.path("out.css"));
        map = embeddedSourceMap(css);

        expect(map, containsPair("sources", ["test.scss"]));
        expect(map, containsPair("sourcesContent", ["a {b: '▼'}"]));
      });
    });

    group("with the target in a different directory", () {
      setUp(() async {
        ensureDir(d.path("dir"));
        await (await runSass([
          "--embed-source-map",
          "test.scss",
          "dir/out.css",
        ]))
            .shouldExit(0);
        var css = readFile(d.path("dir/out.css"));
        map = embeddedSourceMap(css);
      });

      test("refers to the source file", () {
        expect(map, containsPair("sources", ["../test.scss"]));
      });

      test("refers to the target file", () {
        expect(map, containsPair("file", "out.css"));
      });
    });

    test("with stdout as the target", () async {
      var sass = await runSass(["--embed-source-map", "test.scss"]);
      expect(
        sass.stdout,
        emitsInOrder([
          "a {",
          "  b: 3;",
          "}",
          "",
          startsWith("/*# sourceMappingURL=data:"),
        ]),
      );
      await sass.shouldExit(0);
    });
  });
}

/// Reads the file at [path] within [d.sandbox] and JSON-decodes it.
Map<String, dynamic> _readJson(String path) =>
    jsonDecode(readFile(d.path(path))) as Map<String, dynamic>;
