// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import {
  assert,
  assertEquals,
  assertStringIncludes,
  assertThrowsAsync,
} from "../../../test_util/std/testing/asserts.ts";

Deno.test({
  name: "Deno.emit() - sources provided",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "/foo.ts",
      {
        sources: {
          "/foo.ts": `import * as bar from "./bar.ts";\n\nconsole.log(bar);\n`,
          "/bar.ts": `export const bar = "bar";\n`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files).sort();
    assert(keys[0].endsWith("/bar.ts.js"));
    assert(keys[1].endsWith("/bar.ts.js.map"));
    assert(keys[2].endsWith("/foo.ts.js"));
    assert(keys[3].endsWith("/foo.ts.js.map"));
  },
});

Deno.test({
  name: "Deno.emit() - no sources provided",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "./subdir/mod1.ts",
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files).sort();
    assertEquals(keys.length, 6);
    assert(keys[0].endsWith("subdir/mod1.ts.js"));
    assert(keys[1].endsWith("subdir/mod1.ts.js.map"));
  },
});

Deno.test({
  name: "Deno.emit() - compiler options effects emit",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "/foo.ts",
      {
        compilerOptions: {
          module: "amd",
          sourceMap: false,
        },
        sources: { "/foo.ts": `export const foo = "foo";` },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files);
    assertEquals(keys.length, 1);
    const key = keys[0];
    assert(key.endsWith("/foo.ts.js"));
    assert(files[key].startsWith("define("));
  },
});

Deno.test({
  name: "Deno.emit() - pass lib in compiler options",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "file:///foo.ts",
      {
        compilerOptions: {
          lib: ["dom", "es2018", "deno.ns"],
        },
        sources: {
          "file:///foo.ts": `console.log(document.getElementById("foo"));
          console.log(Deno.args);`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files).sort();
    assertEquals(keys, ["file:///foo.ts.js", "file:///foo.ts.js.map"]);
  },
});

Deno.test({
  name: "Deno.emit() - type references can be loaded",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "file:///a.ts",
      {
        sources: {
          "file:///a.ts": `/// <reference types="./b.d.ts" />
          const b = new B();
          console.log(b.b);`,
          "file:///b.d.ts": `declare class B {
            b: string;
          }`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files).sort();
    assertEquals(keys, ["file:///a.ts.js", "file:///a.ts.js.map"]);
  },
});

Deno.test({
  name: "Deno.emit() - compilerOptions.types",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "file:///a.ts",
      {
        compilerOptions: {
          types: ["file:///b.d.ts"],
        },
        sources: {
          "file:///a.ts": `const b = new B();
          console.log(b.b);`,
          "file:///b.d.ts": `declare class B {
            b: string;
          }`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files).sort();
    assertEquals(keys, ["file:///a.ts.js", "file:///a.ts.js.map"]);
  },
});

Deno.test({
  name: "Deno.emit() - import maps",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "file:///a.ts",
      {
        importMap: {
          imports: {
            "b": "./b.ts",
          },
        },
        importMapPath: "file:///import-map.json",
        sources: {
          "file:///a.ts": `import * as b from "b"
            console.log(b);`,
          "file:///b.ts": `export const b = "b";`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    const keys = Object.keys(files).sort();
    assertEquals(
      keys,
      [
        "file:///a.ts.js",
        "file:///a.ts.js.map",
        "file:///b.ts.js",
        "file:///b.ts.js.map",
      ],
    );
  },
});

Deno.test({
  name: "Deno.emit() - no check",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "/foo.ts",
      {
        check: false,
        sources: {
          "/foo.ts": `export enum Foo { Foo, Bar, Baz };\n`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 3);
    const keys = Object.keys(files).sort();
    assert(keys[0].endsWith("/foo.ts.js"));
    assert(keys[1].endsWith("/foo.ts.js.map"));
    assert(files[keys[0]].startsWith("export var Foo;"));
  },
});

Deno.test({
  name: "Deno.emit() - no check - config effects emit",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "/foo.ts",
      {
        check: false,
        compilerOptions: { removeComments: true },
        sources: {
          "/foo.ts":
            `/** This is JSDoc */\nexport enum Foo { Foo, Bar, Baz };\n`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 3);
    const keys = Object.keys(files).sort();
    assert(keys[0].endsWith("/foo.ts.js"));
    assert(keys[1].endsWith("/foo.ts.js.map"));
    assert(!files[keys[0]].includes("This is JSDoc"));
  },
});

Deno.test({
  name: "Deno.emit() - bundle as module script - with sources",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "/foo.ts",
      {
        bundle: "module",
        sources: {
          "/foo.ts": `export * from "./bar.ts";\n`,
          "/bar.ts": `export const bar = "bar";\n`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    assertEquals(
      Object.keys(files).sort(),
      ["deno:///bundle.js", "deno:///bundle.js.map"].sort(),
    );
    assert(files["deno:///bundle.js"].includes(`const bar1 = "bar"`));
  },
});

Deno.test({
  name: "Deno.emit() - bundle as module script - no sources",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "./subdir/mod1.ts",
      {
        bundle: "module",
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    assertEquals(
      Object.keys(files).sort(),
      ["deno:///bundle.js", "deno:///bundle.js.map"].sort(),
    );
    assert(files["deno:///bundle.js"].length);
  },
});

Deno.test({
  name: "Deno.emit() - bundle as module script - include js modules",
  async fn() {
    const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
      "/foo.js",
      {
        bundle: "module",
        sources: {
          "/foo.js": `export * from "./bar.js";\n`,
          "/bar.js": `export const bar = "bar";\n`,
        },
      },
    );
    assertEquals(diagnostics.length, 0);
    assert(!ignoredOptions);
    assertEquals(stats.length, 12);
    assertEquals(
      Object.keys(files).sort(),
      ["deno:///bundle.js.map", "deno:///bundle.js"].sort(),
    );
    assert(files["deno:///bundle.js"].includes(`const bar1 = "bar"`));
  },
});

Deno.test({
  name: "Deno.emit() - generates diagnostics",
  async fn() {
    const { diagnostics, files } = await Deno.emit(
      "/foo.ts",
      {
        sources: {
          "/foo.ts": `document.getElementById("foo");`,
        },
      },
    );
    assertEquals(diagnostics.length, 1);
    const keys = Object.keys(files).sort();
    assert(keys[0].endsWith("/foo.ts.js"));
    assert(keys[1].endsWith("/foo.ts.js.map"));
  },
});

// See https://github.com/denoland/deno/issues/6908
Deno.test({
  name: "Deno.emit() - invalid syntax does not panic",
  async fn() {
    await assertThrowsAsync(async () => {
      await Deno.emit("/main.js", {
        sources: {
          "/main.js": `
            export class Foo {
              constructor() {
                console.log("foo");
              }
              export get() {
                console.log("bar");
              }
            }`,
        },
      });
    });
  },
});

Deno.test({
  name: 'Deno.emit() - allows setting of "importsNotUsedAsValues"',
  async fn() {
    const { diagnostics } = await Deno.emit("/a.ts", {
      sources: {
        "/a.ts": `import { B } from "./b.ts";
          const b: B = { b: "b" };`,
        "/b.ts": `export interface B {
          b:string;
        };`,
      },
      compilerOptions: {
        importsNotUsedAsValues: "error",
      },
    });
    assert(diagnostics);
    assertEquals(diagnostics.length, 1);
    assert(diagnostics[0].messageText);
    assert(diagnostics[0].messageText.includes("This import is never used"));
  },
});

Deno.test({
  name: "Deno.emit() - Unknown media type does not panic",
  async fn() {
    await assertThrowsAsync(async () => {
      await Deno.emit("https://example.com/foo", {
        sources: {
          "https://example.com/foo": `let foo: string = "foo";`,
        },
      });
    });
  },
});

Deno.test({
  name: "Deno.emit() - non-normalized specifier and source can compile",
  async fn() {
    const specifier = "https://example.com/foo//bar.ts";
    const { files } = await Deno.emit(specifier, {
      sources: {
        [specifier]: `export let foo: string = "foo";`,
      },
    });
    assertEquals(files[`${specifier}.js`], 'export let foo = "foo";\n');
    assert(typeof files[`${specifier}.js.map`] === "string");
  },
});

Deno.test({
  name: `Deno.emit() - bundle as classic script iife`,
  async fn() {
    const { diagnostics, files } = await Deno.emit("/a.ts", {
      bundle: "classic",
      sources: {
        "/a.ts": `import { b } from "./b.ts";
          console.log(b);`,
        "/b.ts": `export const b = "b";`,
      },
    });
    assert(diagnostics);
    assertEquals(diagnostics.length, 0);
    assertEquals(Object.keys(files).length, 2);
    assert(files["deno:///bundle.js"].startsWith("(function() {\n"));
    assert(files["deno:///bundle.js"].endsWith("})();\n"));
    assert(files["deno:///bundle.js.map"]);
  },
});

Deno.test({
  name: `Deno.emit() - throws descriptive error when unable to load import map`,
  async fn() {
    await assertThrowsAsync(
      async () => {
        await Deno.emit("/a.ts", {
          bundle: "classic",
          sources: {
            "/a.ts": `console.log("hello");`,
          },
          importMapPath: "file:///import_map_does_not_exist.json",
        });
      },
      Error,
      "Unable to load 'file:///import_map_does_not_exist.json' import map",
    );
  },
});

Deno.test({
  name: `Deno.emit() - support source maps with bundle option`,
  async fn() {
    {
      const { diagnostics, files } = await Deno.emit("/a.ts", {
        bundle: "classic",
        sources: {
          "/a.ts": `import { b } from "./b.ts";
          console.log(b);`,
          "/b.ts": `export const b = "b";`,
        },
        compilerOptions: {
          inlineSourceMap: true,
          sourceMap: false,
        },
      });
      assert(diagnostics);
      assertEquals(diagnostics.length, 0);
      assertEquals(Object.keys(files).length, 1);
      assertStringIncludes(files["deno:///bundle.js"], "sourceMappingURL");
    }

    const { diagnostics, files } = await Deno.emit("/a.ts", {
      bundle: "classic",
      sources: {
        "/a.ts": `import { b } from "./b.ts";
        console.log(b);`,
        "/b.ts": `export const b = "b";`,
      },
    });
    assert(diagnostics);
    assertEquals(diagnostics.length, 0);
    assertEquals(Object.keys(files).length, 2);
    assert(files["deno:///bundle.js"]);
    assert(files["deno:///bundle.js.map"]);
  },
});

Deno.test({
  name: `Deno.emit() - graph errors as diagnostics`,
  ignore: Deno.build.os === "windows",
  async fn() {
    const { diagnostics } = await Deno.emit("/a.ts", {
      sources: {
        "/a.ts": `import { b } from "./b.ts";
        console.log(b);`,
      },
    });
    assert(diagnostics);
    assertEquals(diagnostics, [
      {
        category: 1,
        code: 2305,
        start: { line: 0, character: 9 },
        end: { line: 0, character: 10 },
        messageText:
          `Module '"deno:///missing_dependency.d.ts"' has no exported member 'b'.`,
        messageChain: null,
        source: null,
        sourceLine: 'import { b } from "./b.ts";',
        fileName: "file:///a.ts",
        relatedInformation: null,
      },
      {
        category: 1,
        code: 900001,
        start: null,
        end: null,
        messageText: "Unable to find specifier in sources: file:///b.ts",
        messageChain: null,
        source: null,
        sourceLine: null,
        fileName: "file:///b.ts",
        relatedInformation: null,
      },
    ]);
    assert(
      Deno.formatDiagnostics(diagnostics).includes(
        "Unable to find specifier in sources: file:///b.ts",
      ),
    );
  },
});