import { parse, parseExpression } from "@babel/parser";
import * as t from "@babel/types";
import fs from "node:fs";
import path from "node:path";
import fixtures from "@babel/helper-fixtures";
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
import { commonJS } from "$repo-utils";
import { encode } from "@jridgewell/sourcemap-codec";

import generate from "../lib/index.js";

const { __dirname } = commonJS(import.meta.url);

describe("generation", function () {
  it("multiple sources", function () {
    const sources = {
      "a.js": "function hi (msg) { console.log(msg); }\n",
      "b.js": "hi('hello');\n",
    };
    const parsed = Object.keys(sources).reduce(function (_parsed, filename) {
      _parsed[filename] = parse(sources[filename], {
        sourceFilename: filename,
      });
      return _parsed;
    }, {});

    const combinedAst = {
      type: "File",
      program: {
        type: "Program",
        sourceType: "module",
        body: [].concat(
          parsed["a.js"].program.body,
          parsed["b.js"].program.body,
        ),
      },
    };

    const generated = generate(combinedAst, { sourceMaps: true }, sources);

    expect(generated.map).toMatchInlineSnapshot(`
      {
        "file": undefined,
        "ignoreList": [],
        "mappings": "AAAA,SAAS,EAAE,CAAE,GAAG,EAAE;EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;AAAE;ACAtC,EAAE,CAAC,OAAO,CAAC",
        "names": [],
        "sourceRoot": undefined,
        "sources": [
          "a.js",
          "b.js",
        ],
        "sourcesContent": [
          "function hi (msg) { console.log(msg); }
      ",
          "hi('hello');
      ",
        ],
        "version": 3,
      }
    `);

    expect(generated.rawMappings).toMatchInlineSnapshot(`
      [
        {
          "generated": {
            "column": 0,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 0,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 9,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 9,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 11,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 11,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 12,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 13,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 15,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 16,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 17,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 18,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 2,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 20,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 9,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 27,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 10,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 28,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 13,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 31,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 14,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 32,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 17,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 35,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 18,
            "line": 2,
          },
          "name": undefined,
          "original": {
            "column": 36,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 0,
            "line": 3,
          },
          "name": undefined,
          "original": {
            "column": 38,
            "line": 1,
          },
          "source": "a.js",
        },
        {
          "generated": {
            "column": 0,
            "line": 4,
          },
          "name": undefined,
          "original": {
            "column": 0,
            "line": 1,
          },
          "source": "b.js",
        },
        {
          "generated": {
            "column": 2,
            "line": 4,
          },
          "name": undefined,
          "original": {
            "column": 2,
            "line": 1,
          },
          "source": "b.js",
        },
        {
          "generated": {
            "column": 3,
            "line": 4,
          },
          "name": undefined,
          "original": {
            "column": 3,
            "line": 1,
          },
          "source": "b.js",
        },
        {
          "generated": {
            "column": 10,
            "line": 4,
          },
          "name": undefined,
          "original": {
            "column": 10,
            "line": 1,
          },
          "source": "b.js",
        },
        {
          "generated": {
            "column": 11,
            "line": 4,
          },
          "name": undefined,
          "original": {
            "column": 11,
            "line": 1,
          },
          "source": "b.js",
        },
      ]
    `);

    expect(generated.code).toBe(
      "function hi(msg) {\n  console.log(msg);\n}\nhi('hello');",
    );
  });

  it("identifierName", function () {
    const code = "function foo() { bar; }\n";

    const ast = parse(code, { filename: "inline" }).program;
    const fn = ast.body[0];

    const id = fn.id;
    id.name += "2";
    id.loc.identifierName = "foo";

    const id2 = fn.body.body[0].expression;
    id2.name += "2";
    id2.loc.identifierName = "bar";

    const generated = generate(
      ast,
      {
        filename: "inline",
        sourceFileName: "inline",
        sourceMaps: true,
      },
      code,
    );

    expect(generated).toMatchInlineSnapshot(`
      {
        "code": "function foo2() {
        bar2;
      }",
        "decodedMap": {
          "file": undefined,
          "ignoreList": [],
          "mappings": [
            [
              [
                0,
                0,
                0,
                0,
              ],
              [
                9,
                0,
                0,
                9,
                0,
              ],
              [
                13,
                0,
                0,
                12,
                0,
              ],
              [
                14,
                0,
                0,
                12,
              ],
              [
                16,
                0,
                0,
                15,
              ],
            ],
            [
              [
                2,
                0,
                0,
                17,
                1,
              ],
              [
                6,
                0,
                0,
                20,
              ],
            ],
            [
              [
                0,
                0,
                0,
                22,
              ],
            ],
          ],
          "names": [
            "foo",
            "bar",
          ],
          "sourceRoot": undefined,
          "sources": [
            "inline",
          ],
          "sourcesContent": [
            "function foo() { bar; }
      ",
          ],
          "version": 3,
        },
        "map": {
          "file": undefined,
          "ignoreList": [],
          "mappings": "AAAA,SAASA,IAAGA,CAAA,EAAG;EAAEC,IAAG;AAAE",
          "names": [
            "foo",
            "bar",
          ],
          "sourceRoot": undefined,
          "sources": [
            "inline",
          ],
          "sourcesContent": [
            "function foo() { bar; }
      ",
          ],
          "version": 3,
        },
        "rawMappings": [
          {
            "generated": {
              "column": 0,
              "line": 1,
            },
            "name": undefined,
            "original": {
              "column": 0,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 9,
              "line": 1,
            },
            "name": "foo",
            "original": {
              "column": 9,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 13,
              "line": 1,
            },
            "name": "foo",
            "original": {
              "column": 12,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 14,
              "line": 1,
            },
            "name": undefined,
            "original": {
              "column": 12,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 16,
              "line": 1,
            },
            "name": undefined,
            "original": {
              "column": 15,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 2,
              "line": 2,
            },
            "name": "bar",
            "original": {
              "column": 17,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 6,
              "line": 2,
            },
            "name": undefined,
            "original": {
              "column": 20,
              "line": 1,
            },
            "source": "inline",
          },
          {
            "generated": {
              "column": 0,
              "line": 3,
            },
            "name": undefined,
            "original": {
              "column": 22,
              "line": 1,
            },
            "source": "inline",
          },
        ],
      }
    `);
  });

  it("newline in template literal", () => {
    const code = "`before\n\nafter`;";
    const ast = parse(code, { filename: "inline" }).program;
    const generated = generate(
      ast,
      {
        filename: "inline",
        sourceFileName: "inline",
        sourceMaps: true,
      },
      code,
    );

    const consumer = new TraceMap(generated.map);
    const loc = originalPositionFor(consumer, { line: 2, column: 1 });
    expect(loc).toMatchObject({
      column: 0,
      line: 2,
    });
  });

  it("newline in string literal", () => {
    const code = "'before\\\n\\\nafter';";
    const ast = parse(code, { filename: "inline" }).program;
    const generated = generate(
      ast,
      {
        filename: "inline",
        sourceFileName: "inline",
        sourceMaps: true,
      },
      code,
    );

    const consumer = new TraceMap(generated.map);
    const loc = originalPositionFor(consumer, { line: 2, column: 1 });
    expect(loc).toMatchObject({
      column: 0,
      line: 2,
    });
  });

  it("lazy source map generation", function () {
    const code = "function hi (msg) { console.log(msg); }\n";

    const ast = parse(code, { filename: "a.js" }).program;
    const generated = generate(ast, {
      sourceFileName: "a.js",
      sourceMaps: true,
    });

    expect(Array.isArray(generated.rawMappings)).toBe(true);

    expect(
      Object.getOwnPropertyDescriptor(generated, "map"),
    ).not.toHaveProperty("value");

    expect(generated).toHaveProperty("map");
    expect(typeof generated.map).toBe("object");
  });

  it("wraps around infer inside an array type", () => {
    const type = t.tsArrayType(
      t.tsInferType(t.tsTypeParameter(null, null, t.identifier("T"))),
    );

    const output = generate(type).code;
    expect(output).toBe("(infer T)[]");
  });

  it("should not deduplicate comments with same start index", () => {
    const code1 = "/*#__PURE__*/ a();";
    const code2 = "/*#__PURE__*/ b();";

    const ast1 = parse(code1).program;
    const ast2 = parse(code2).program;

    const ast = t.program([...ast1.body, ...ast2.body]);

    expect(generate(ast).code).toBe("/*#__PURE__*/a();\n/*#__PURE__*/b();");
  });

  it("comments with null or undefined loc", () => {
    const code = "/*#__PURE__*/ /*#__PURE__*/";

    const ast = parse(code);

    ast.comments[0].loc = null;
    ast.comments[1].loc = undefined;

    expect(generate(ast).code).toBe("/*#__PURE__*/\n/*#__PURE__*/");
  });

  it("comments without loc", () => {
    const ast = parse(
      `
        import {
            Attribute,
            AttributeSDKType
        }
        from "../../base/v1beta1/attribute";
        import {
            Rpc
        }
        from "../../../helpers";
        import * as _m0 from "protobufjs/minimal";
        import {
            MsgSignProviderAttributes,
            MsgSignProviderAttributesSDKType,
            MsgSignProviderAttributesResponse,
            MsgSignProviderAttributesResponseSDKType,
            MsgDeleteProviderAttributes,
            MsgDeleteProviderAttributesSDKType,
            MsgDeleteProviderAttributesResponse,
            MsgDeleteProviderAttributesResponseSDKType
        }
        from "./audit";
        /** Msg defines the provider Msg service */
        export interface Msg {
            /** SignProviderAttributes defines a method that signs provider attributes */
            signProviderAttributes(request: MsgSignProviderAttributes): Promise < MsgSignProviderAttributesResponse > ;
            /** DeleteProviderAttributes defines a method that deletes provider attributes */
            deleteProviderAttributes(request: MsgDeleteProviderAttributes): Promise < MsgDeleteProviderAttributesResponse > ;
        }
        export class MsgClientImpl implements Msg {
            private readonly rpc: Rpc;
            constructor(rpc: Rpc) {
                this.rpc = rpc;
            }
            /* SignProviderAttributes defines a method that signs provider attributes */
            signProviderAttributes = async(request: MsgSignProviderAttributes): Promise < MsgSignProviderAttributesResponse >  => {
                const data = MsgSignProviderAttributes.encode(request).finish();
                const promise = this.rpc.request("akash.audit.v1beta1.Msg", "SignProviderAttributes", data);
                return promise.then(data => MsgSignProviderAttributesResponse.decode(new _m0.Reader(data)));
            };
            /* DeleteProviderAttributes defines a method that deletes provider attributes */
            deleteProviderAttributes = async(request: MsgDeleteProviderAttributes): Promise < MsgDeleteProviderAttributesResponse >  => {
                const data = MsgDeleteProviderAttributes.encode(request).finish();
                const promise = this.rpc.request("akash.audit.v1beta1.Msg", "DeleteProviderAttributes", data);
                return promise.then(data => MsgDeleteProviderAttributesResponse.decode(new _m0.Reader(data)));
            };
        }
    `,
      { sourceType: "module", plugins: ["typescript"] },
    );

    for (const comment of ast.comments) {
      comment.loc = undefined;
    }

    expect(generate(ast).code).toMatchInlineSnapshot(`
      "import { Attribute, AttributeSDKType } from "../../base/v1beta1/attribute";
      import { Rpc } from "../../../helpers";
      import * as _m0 from "protobufjs/minimal";
      import { MsgSignProviderAttributes, MsgSignProviderAttributesSDKType, MsgSignProviderAttributesResponse, MsgSignProviderAttributesResponseSDKType, MsgDeleteProviderAttributes, MsgDeleteProviderAttributesSDKType, MsgDeleteProviderAttributesResponse, MsgDeleteProviderAttributesResponseSDKType } from "./audit";
      /** Msg defines the provider Msg service */
      export interface Msg {
        /** SignProviderAttributes defines a method that signs provider attributes */
        signProviderAttributes(request: MsgSignProviderAttributes): Promise<MsgSignProviderAttributesResponse>;
        /** DeleteProviderAttributes defines a method that deletes provider attributes */
        deleteProviderAttributes(request: MsgDeleteProviderAttributes): Promise<MsgDeleteProviderAttributesResponse>;
      }
      export class MsgClientImpl implements Msg {
        private readonly rpc: Rpc;
        constructor(rpc: Rpc) {
          this.rpc = rpc;
        }
        /* SignProviderAttributes defines a method that signs provider attributes */
        signProviderAttributes = async (request: MsgSignProviderAttributes): Promise<MsgSignProviderAttributesResponse> => {
          const data = MsgSignProviderAttributes.encode(request).finish();
          const promise = this.rpc.request("akash.audit.v1beta1.Msg", "SignProviderAttributes", data);
          return promise.then(data => MsgSignProviderAttributesResponse.decode(new _m0.Reader(data)));
        };
        /* DeleteProviderAttributes defines a method that deletes provider attributes */
        deleteProviderAttributes = async (request: MsgDeleteProviderAttributes): Promise<MsgDeleteProviderAttributesResponse> => {
          const data = MsgDeleteProviderAttributes.encode(request).finish();
          const promise = this.rpc.request("akash.audit.v1beta1.Msg", "DeleteProviderAttributes", data);
          return promise.then(data => MsgDeleteProviderAttributesResponse.decode(new _m0.Reader(data)));
        };
      }"
    `);
  });

  it("comments without loc2", () => {
    const ast = parse(
      `
        (function (_templateFactory) {
            "use strict";

            const template = (0, _templateFactory.createTemplateFactory)(
            /*{{somevalue}}*/
            {
                "id": null,
                "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
                "moduleName": "(unknown template module)",
                "isStrictMode": false
            });
        });

        const template = (0, _templateFactory.createTemplateFactory)(
        /*
          {{somevalue}}
        */
        {
            "id": null,
            "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
            "moduleName": "(unknown template module)",
            "isStrictMode": false
        });
      `,
      { sourceType: "module" },
    );

    for (const comment of ast.comments) {
      comment.loc = undefined;
    }

    expect(generate(ast).code).toMatchInlineSnapshot(`
      "(function (_templateFactory) {
        "use strict";

        const template = (0, _templateFactory.createTemplateFactory)(
        /*{{somevalue}}*/
        {
          "id": null,
          "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
          "moduleName": "(unknown template module)",
          "isStrictMode": false
        });
      });
      const template = (0, _templateFactory.createTemplateFactory)(
      /*
                {{somevalue}}
              */
      {
        "id": null,
        "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
        "moduleName": "(unknown template module)",
        "isStrictMode": false
      });"
    `);
  });

  it("comments without loc3", () => {
    const ast = parse(
      `
        /** This describes how the endpoint is implemented when the lease is deployed */
        export enum Endpoint_Kind {
          /** SHARED_HTTP - Describes an endpoint that becomes a Kubernetes Ingress */
          SHARED_HTTP = 0,
          /** RANDOM_PORT - Describes an endpoint that becomes a Kubernetes NodePort */
          RANDOM_PORT = 1,
          UNRECOGNIZED = -1,
        }
      `,
      { sourceType: "module", plugins: ["typescript"] },
    );

    for (const comment of ast.comments) {
      comment.loc = undefined;
    }

    expect(generate(ast).code).toMatchInlineSnapshot(`
      "/** This describes how the endpoint is implemented when the lease is deployed */
      export enum Endpoint_Kind {
        /** SHARED_HTTP - Describes an endpoint that becomes a Kubernetes Ingress */
        SHARED_HTTP = 0,
        /** RANDOM_PORT - Describes an endpoint that becomes a Kubernetes NodePort */
        RANDOM_PORT = 1,
        UNRECOGNIZED = -1
      }"
    `);
  });

  it("comments without node.loc", () => {
    const ast = parse(
      `
        (function (_templateFactory) {
            "use strict";

            const template = (0, _templateFactory.createTemplateFactory)(
            /*{{somevalue}}*/
            {
                "id": null,
                "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
                "moduleName": "(unknown template module)",
                "isStrictMode": false
            });
        });

        const template = (0, _templateFactory.createTemplateFactory)(
        /*
          {{somevalue}}
        */
        {
            "id": null,
            "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
            "moduleName": "(unknown template module)",
            "isStrictMode": false
        });
      `,
      { sourceType: "module" },
    );

    const ast2 = t.cloneNode(ast, true, true);

    for (let i = 0; i < ast.comments.length; i++) {
      ast2.comments[i].loc = ast.comments[i].loc;
    }

    expect(generate(ast2).code).toMatchInlineSnapshot(`
      "(function (_templateFactory) {
        "use strict";

        const template = (0, _templateFactory.createTemplateFactory)(
        /*{{somevalue}}*/
        {
          "id": null,
          "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
          "moduleName": "(unknown template module)",
          "isStrictMode": false
        });
      });
      const template = (0, _templateFactory.createTemplateFactory)(
      /*
        {{somevalue}}
      */
      {
        "id": null,
        "block": "[[[1,[34,0]]],[],false,[\\"somevalue\\"]]",
        "moduleName": "(unknown template module)",
        "isStrictMode": false
      });"
    `);
  });

  it("inputSourceMap without sourcesContent", () => {
    const ast = parse("var t = x => x * x;");

    expect(
      generate(ast, {
        sourceMaps: true,
        inputSourceMap: {
          version: 3,
          names: ["t", "x"],
          sources: ["source-maps/arrow-function/input.js"],
          mappings:
            "AAAA,IAAIA,CAAC,GAAG,SAAJA,CAACA,CAAGC,CAAC;EAAA,OAAIA,CAAC,GAAGA,CAAC;AAAA",
        },
      }).map,
    ).toMatchInlineSnapshot(`
      {
        "file": undefined,
        "ignoreList": [],
        "mappings": "AAAA,IAAI,CAAC,GAAG,SAAJ,CAAC",
        "names": [],
        "sourceRoot": undefined,
        "sources": [
          "source-maps/arrow-function/input.js",
        ],
        "sourcesContent": [
          undefined,
        ],
        "version": 3,
      }
    `);
  });

  it("should not throw when loc.column === 0 with inputSourceMap", () => {
    const ast = parseExpression("a(\n)");

    ast.loc.end.column = 0;

    expect(
      generate(ast, {
        sourceMaps: true,
        inputSourceMap: {
          version: 3,
          names: [],
          sources: ["input.js"],
          // [ generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex ]
          mappings: encode([[0, 0, 1, 0]]),
        },
      }).rawMappings,
    ).toMatchInlineSnapshot(`
      [
        {
          "generated": {
            "column": 0,
            "line": 1,
          },
          "name": undefined,
          "original": {
            "column": 0,
            "line": 1,
          },
          "source": "input.js",
        },
        {
          "generated": {
            "column": 2,
            "line": 1,
          },
          "name": undefined,
          "original": undefined,
          "source": undefined,
        },
      ]
    `);
  });

  it("source maps should emit names for renamed identifiers", () => {
    const input = `
      let a = 1;
      let b = 2;
      function c() {}
      function d() {}
      class C {
        #e() {};
        #f() {};
      }
    `;
    const ast = parse(input);
    ast.program.body[0].declarations[0].id.name = "x";
    ast.program.body[2].id.name = "y";
    ast.program.body[4].body.body[0].key.id.name = "z";

    const out = generate(
      ast,
      { sourceMaps: true, sourceFileName: "input.js" },
      input,
    );

    expect(out.code).toMatchInlineSnapshot(`
      "let x = 1;
      let b = 2;
      function y() {}
      function d() {}
      class C {
        #z() {}
        #f() {}
      }"
    `);
    expect(out.map).toMatchInlineSnapshot(`
      {
        "file": undefined,
        "ignoreList": [],
        "mappings": "AACM,IAAIA,CAAC,GAAG,CAAC;AACT,IAAI,CAAC,GAAG,CAAC;AACT,SAASC,CAACA,CAAA,EAAG,CAAC;AACd,SAAS,CAAC,GAAG,CAAC;AACd,MAAM,CAAC,CAAC;EACN,CAACC,CAACC,CAAA,EAAG,CAAC;EACN,CAAC,CAAC,GAAG,CAAC;AACR",
        "names": [
          "a",
          "c",
          "e",
          "#e",
        ],
        "sourceRoot": undefined,
        "sources": [
          "input.js",
        ],
        "sourcesContent": [
          "
            let a = 1;
            let b = 2;
            function c() {}
            function d() {}
            class C {
              #e() {};
              #f() {};
            }
          ",
        ],
        "version": 3,
      }
    `);
  });
});

describe("programmatic generation", function () {
  it("should add parenthesis when NullishCoalescing is used along with ||", function () {
    // https://github.com/babel/babel/issues/10260
    const nullishCoalesce = t.logicalExpression(
      "??",
      t.logicalExpression("||", t.identifier("a"), t.identifier("b")),
      t.identifier("c"),
    );
    const output = generate(nullishCoalesce).code;
    expect(output).toBe(`(a || b) ?? c`);
  });

  it("should add parenthesis when NullishCoalesing is used with &&", function () {
    const nullishCoalesce = t.logicalExpression(
      "??",
      t.identifier("a"),
      t.logicalExpression(
        "&&",
        t.identifier("b"),
        t.logicalExpression("&&", t.identifier("c"), t.identifier("d")),
      ),
    );
    const output = generate(nullishCoalesce).code;
    expect(output).toBe(`a ?? (b && c && d)`);
  });

  it("numeric member expression", function () {
    // Should not generate `0.foo`
    const mem = t.memberExpression(
      t.numericLiteral(60702),
      t.identifier("foo"),
    );
    new Function(generate(mem).code);
  });

  it("nested if statements needs block", function () {
    const ifStatement = t.ifStatement(
      t.stringLiteral("top cond"),
      t.whileStatement(
        t.stringLiteral("while cond"),
        t.ifStatement(
          t.stringLiteral("nested"),
          t.expressionStatement(t.numericLiteral(1)),
        ),
      ),
      t.expressionStatement(t.stringLiteral("alt")),
    );

    const ast = parse(generate(ifStatement).code);
    expect(ast.program.body[0].consequent.type).toBe("BlockStatement");
  });

  it("prints directives in block with empty body", function () {
    const blockStatement = t.blockStatement(
      [],
      [t.directive(t.directiveLiteral("use strict"))],
    );

    const output = generate(blockStatement).code;
    expect(output).toBe(`{
  "use strict";
}`);
  });

  it("flow object indentation", function () {
    const objectStatement = t.objectTypeAnnotation([
      t.objectTypeProperty(t.identifier("bar"), t.stringTypeAnnotation()),
    ]);

    const output = generate(objectStatement).code;
    expect(output).toBe(`{
  bar: string
}`);
  });

  it("flow object exact", function () {
    const objectStatement = t.objectTypeAnnotation(
      [t.objectTypeProperty(t.identifier("bar"), t.stringTypeAnnotation())],
      undefined,
      undefined,
      undefined,
      true,
    );

    const output = generate(objectStatement).code;
    expect(output).toBe(`{|
  bar: string
|}`);
  });

  it("flow object indentation with empty leading ObjectTypeProperty", function () {
    const objectStatement = t.objectTypeAnnotation(
      [],
      [
        t.objectTypeIndexer(
          t.identifier("key"),
          t.anyTypeAnnotation(),
          t.numberTypeAnnotation(),
        ),
      ],
    );

    const output = generate(objectStatement).code;

    expect(output).toBe(`{
  [key: any]: number
}`);
  });

  it("flow interface with nullish extends", () => {
    const interfaceDeclaration = t.interfaceDeclaration(
      t.identifier("A"),
      undefined,
      undefined,
      t.objectTypeAnnotation([]),
    );
    const output = generate(interfaceDeclaration).code;
    expect(output).toBe("interface A {}");
  });

  it("flow function type annotation with no parent", () => {
    const functionTypeAnnotation = t.functionTypeAnnotation(
      null,
      [],
      null,
      t.voidTypeAnnotation(),
    );
    const output = generate(functionTypeAnnotation).code;
    expect(output).toBe("() => void");
  });

  it("generate a child node with retainLines", () => {
    const node = parse("a;\n\nexpect(a).toMatchInlineSnapshot(`[1, 2]`\n);")
      .program.body[1].expression;

    expect(node.type).toBe("CallExpression");

    expect(generate(node, { retainLines: true }).code).toMatchInlineSnapshot(`
      "

      expect(a).toMatchInlineSnapshot(\`[1, 2]\`
      )"
    `);

    node.loc.end.line = node.loc.start.line;

    expect(generate(node, { retainLines: true }).code).toMatchInlineSnapshot(`
      "

      expect(a).toMatchInlineSnapshot(\`[1, 2]\`)"
    `);
  });

  describe("directives", function () {
    it("preserves escapes", function () {
      const directive = t.directive(
        t.directiveLiteral(String.raw`us\x65 strict`),
      );
      const output = generate(directive).code;

      expect(output).toBe(String.raw`"us\x65 strict";`);
    });

    it("preserves escapes in minified output", function () {
      // https://github.com/babel/babel/issues/4767

      const directive = t.directive(t.directiveLiteral(String.raw`foo\n\t\r`));
      const output = generate(directive, { minified: true }).code;

      expect(output).toBe(String.raw`"foo\n\t\r";`);
    });

    it("unescaped single quote", function () {
      const directive = t.directive(t.directiveLiteral(String.raw`'\'\"`));
      const output = generate(directive).code;

      expect(output).toBe(String.raw`"'\'\"";`);
    });

    it("unescaped double quote", function () {
      const directive = t.directive(t.directiveLiteral(String.raw`"\'\"`));
      const output = generate(directive).code;

      expect(output).toBe(String.raw`'"\'\"';`);
    });

    it("unescaped single and double quotes together throw", function () {
      const directive = t.directive(t.directiveLiteral(String.raw`'"`));

      expect(() => {
        generate(directive);
      }).toThrow();
    });

    it("preserves single quotes if not minified", function () {
      const directive = parse("'use strict';").program.directives[0];
      const output = generate(directive).code;

      expect(output).toBe("'use strict';");
    });

    it("converts single quotes to double quotes if minified", function () {
      const directive = parse("'use strict';").program.directives[0];
      const output = generate(directive, { minified: true }).code;

      expect(output).toBe('"use strict";');
    });
  });

  describe("typescript generate parentheses if necessary", function () {
    it("wraps around union for array", () => {
      const typeStatement = t.tsArrayType(
        t.tsUnionType([
          t.tsIntersectionType([t.tsNumberKeyword(), t.tsBooleanKeyword()]),
          t.tsNullKeyword(),
        ]),
      );
      const output = generate(typeStatement).code;
      expect(output).toBe("(number & boolean | null)[]");
    });
    it("wraps around intersection for array", () => {
      const typeStatement = t.tsArrayType(
        t.tsIntersectionType([t.tsNumberKeyword(), t.tsBooleanKeyword()]),
      );
      const output = generate(typeStatement).code;
      expect(output).toBe("(number & boolean)[]");
    });
    it("wraps around optional type", () => {
      const typeStatement = t.tsOptionalType(
        t.tsIntersectionType([t.tsNumberKeyword(), t.tsBooleanKeyword()]),
      );
      const output = generate(typeStatement).code;
      expect(output).toBe("(number & boolean)?");
    });
  });

  describe("object expressions", () => {
    it("not wrapped in parentheses when standalone", () => {
      const objectExpression = t.objectExpression([]);
      const output = generate(objectExpression).code;
      expect(output).toBe("{}");
    });

    it("wrapped in parentheses in expression statement", () => {
      const expressionStatement = t.expressionStatement(t.objectExpression([]));
      const output = generate(expressionStatement).code;
      expect(output).toBe("({});");
    });

    it("wrapped in parentheses in arrow function", () => {
      const arrowFunctionExpression = t.arrowFunctionExpression(
        [],
        t.objectExpression([]),
      );
      const output = generate(arrowFunctionExpression).code;
      expect(output).toBe("() => ({})");
    });

    it("not wrapped in parentheses in conditional", () => {
      const conditionalExpression = t.conditionalExpression(
        t.objectExpression([]),
        t.booleanLiteral(true),
        t.booleanLiteral(false),
      );
      const output = generate(conditionalExpression).code;
      expect(output).toBe("{} ? true : false");
    });

    it("wrapped in parentheses in conditional in expression statement", () => {
      const expressionStatement = t.expressionStatement(
        t.conditionalExpression(
          t.objectExpression([]),
          t.booleanLiteral(true),
          t.booleanLiteral(false),
        ),
      );
      const output = generate(expressionStatement).code;
      expect(output).toBe("({}) ? true : false;");
    });

    it("wrapped in parentheses in conditional in arrow function", () => {
      const arrowFunctionExpression = t.arrowFunctionExpression(
        [],
        t.conditionalExpression(
          t.objectExpression([]),
          t.booleanLiteral(true),
          t.booleanLiteral(false),
        ),
      );
      const output = generate(arrowFunctionExpression).code;
      expect(output).toBe("() => ({}) ? true : false");
    });

    it("not wrapped in parentheses in binary expression", () => {
      const binaryExpression = t.binaryExpression(
        "+",
        t.objectExpression([]),
        t.numericLiteral(1),
      );
      const output = generate(binaryExpression).code;
      expect(output).toBe("{} + 1");
    });

    it("wrapped in parentheses in binary expression in expression statement", () => {
      const expressionStatement = t.expressionStatement(
        t.binaryExpression("+", t.objectExpression([]), t.numericLiteral(1)),
      );
      const output = generate(expressionStatement).code;
      expect(output).toBe("({}) + 1;");
    });

    it("wrapped in parentheses in binary expression in arrow function", () => {
      const arrowFunctionExpression = t.arrowFunctionExpression(
        [],
        t.binaryExpression("+", t.objectExpression([]), t.numericLiteral(1)),
      );
      const output = generate(arrowFunctionExpression).code;
      expect(output).toBe("() => ({}) + 1");
    });

    it("not wrapped in parentheses in sequence expression", () => {
      const sequenceExpression = t.sequenceExpression([
        t.objectExpression([]),
        t.numericLiteral(1),
      ]);
      const output = generate(sequenceExpression).code;
      expect(output).toBe("{}, 1");
    });

    it("wrapped in parentheses in sequence expression in expression statement", () => {
      const expressionStatement = t.expressionStatement(
        t.sequenceExpression([t.objectExpression([]), t.numericLiteral(1)]),
      );
      const output = generate(expressionStatement).code;
      expect(output).toBe("({}), 1;");
    });

    it("wrapped in parentheses in sequence expression in arrow function", () => {
      const arrowFunctionExpression = t.arrowFunctionExpression(
        [],
        t.sequenceExpression([t.objectExpression([]), t.numericLiteral(1)]),
      );
      const output = generate(arrowFunctionExpression).code;
      expect(output).toBe("() => ({}, 1)");
    });
  });

  describe("function expressions", () => {
    it("not wrapped in parentheses when standalone", () => {
      const functionExpression = t.functionExpression(
        null,
        [],
        t.blockStatement([]),
      );
      const output = generate(functionExpression).code;
      expect(output).toBe("function () {}");
    });

    it("wrapped in parentheses in expression statement", () => {
      const expressionStatement = t.expressionStatement(
        t.functionExpression(null, [], t.blockStatement([])),
      );
      const output = generate(expressionStatement).code;
      expect(output).toBe("(function () {});");
    });

    it("wrapped in parentheses in export default declaration", () => {
      const exportDefaultDeclaration = t.exportDefaultDeclaration(
        t.functionExpression(null, [], t.blockStatement([])),
      );
      const output = generate(exportDefaultDeclaration).code;
      expect(output).toBe("export default (function () {});");
    });
  });

  describe("class expressions", () => {
    it("not wrapped in parentheses when standalone", () => {
      const classExpression = t.classExpression(null, null, t.classBody([]));
      const output = generate(classExpression).code;
      expect(output).toBe("class {}");
    });

    it("wrapped in parentheses in expression statement", () => {
      const expressionStatement = t.expressionStatement(
        t.classExpression(null, null, t.classBody([])),
      );
      const output = generate(expressionStatement).code;
      expect(output).toBe("(class {});");
    });

    it("wrapped in parentheses in export default declaration", () => {
      const exportDefaultDeclaration = t.exportDefaultDeclaration(
        t.classExpression(null, null, t.classBody([])),
      );
      const output = generate(exportDefaultDeclaration).code;
      expect(output).toBe("export default (class {});");
    });
  });

  describe("jsescOption.minimal", () => {
    const string = t.stringLiteral("\u8868\u683C_\u526F\u672C");

    it("true", () => {
      const output = generate(string, { jsescOption: { minimal: true } }).code;
      expect(output).toBe(`"表格_副本"`);
    });

    it("false", () => {
      const output = generate(string, { jsescOption: { minimal: false } }).code;
      expect(output).toBe(`"\\u8868\\u683C_\\u526F\\u672C"`);
    });

    it("default", () => {
      const output = generate(string).code;

      expect(output).toBe(`"表格_副本"`);
    });
  });

  describe("typescript interface declaration", () => {
    it("empty extends array", () => {
      const tsInterfaceDeclaration = t.tsInterfaceDeclaration(
        t.identifier("A"),
        undefined,
        [],
        t.tsInterfaceBody([]),
      );
      const output = generate(tsInterfaceDeclaration).code;
      expect(output).toBe("interface A {}");
    });
  });

  describe("identifier let", () => {
    it("detects open bracket from non-optional OptionalMemberExpression", () => {
      const ast = parse(`for (let?.[x];;);`, {
        sourceType: "script",
        strictMode: "false",
      });
      ast.program.body[0].init.optional = false;
      const output = generate(ast).code;
      expect(output).toBe("for ((let)[x];;);");
    });
  });

  describe("should print inner comments even if there are no suitable inner locations", () => {
    it("atomic node", () => {
      const id = t.identifier("foo");
      id.innerComments = [{ type: "CommentBlock", value: "foo" }];
      expect(generate(id).code).toMatchInlineSnapshot(`"foo /*foo*/"`);
    });

    it("node without inner locations", () => {
      const expr = t.binaryExpression(
        "+",
        t.numericLiteral(1),
        t.numericLiteral(2),
      );
      expr.innerComments = [{ type: "CommentBlock", value: "foo" }];
      expect(generate(expr).code).toMatchInlineSnapshot(`"1 + 2 /*foo*/"`);
    });

    it("comment skipped in arrow function because of newlines", () => {
      const arrow = t.arrowFunctionExpression(
        [t.identifier("x"), t.identifier("y")],
        t.identifier("z"),
      );
      arrow.innerComments = [
        { type: "CommentBlock", value: "foo" },
        { type: "CommentBlock", value: "new\nline" },
      ];
      expect(generate(arrow).code).toMatchInlineSnapshot(`
        "(x, y) /*foo*/ => z
        /*new
        line*/"
      `);
    });
  });

  it("comment in arrow function with return type", () => {
    const arrow = t.arrowFunctionExpression(
      [t.identifier("x"), t.identifier("y")],
      t.identifier("z"),
    );
    arrow.returnType = t.tsTypeAnnotation(t.tsAnyKeyword());
    arrow.returnType.trailingComments = [
      { type: "CommentBlock", value: "foo" },
      // This comment is dropped. There is no way to safely print it
      // as a trailingComment of the return type.
      { type: "CommentBlock", value: "new\nline" },
    ];
    expect(generate(arrow).code).toMatchInlineSnapshot(
      `"(x, y): any /*foo*/ => z"`,
    );
  });

  it("multi-line leading comment after return", () => {
    const val = t.identifier("val");
    val.leadingComments = [{ type: "CommentBlock", value: "new\nline" }];
    expect(generate(t.returnStatement(val)).code).toMatch(`return (
  /*new
  line*/
  val
);`);
  });

  it("multi-line leading comment after return 2", () => {
    const ast = parse(
      `return (
        /*new
        line*/ val);`,
      {
        allowReturnOutsideFunction: true,
      },
    );
    // Remove `parenthesized`
    ast.program.body[0].argument.extra = null;
    expect(generate(ast).code).toMatchInlineSnapshot(`
        "return (
          /*new
          line*/
          val
        );"
      `);
  });

  it("multi-line leading comment after return compact", () => {
    const val = t.identifier("val");
    val.leadingComments = [{ type: "CommentBlock", value: "new\nline" }];
    expect(
      generate(t.returnStatement(val), {
        compact: true,
      }).code,
    ).toMatchInlineSnapshot(`
        "return(/*new
        line*/val);"
      `);
  });

  it("multi-line leading comment after return concise", () => {
    const val = t.identifier("val");
    val.leadingComments = [{ type: "CommentBlock", value: "new\nline" }];
    expect(
      generate(t.returnStatement(val), {
        concise: true,
      }).code,
    ).toMatchInlineSnapshot(`
      "return ( /*new
      line*/ val );"
    `);
  });

  it("correctly indenting when `retainLines`", () => {
    const ast = parse(
      `
export const App = () => {
  return (
      /**
        * First
        */
      2
  );
};

/**
  * Second
  */`,
      {
        sourceType: "module",
      },
    );

    expect(generate(ast, { retainLines: true }).code).toMatchInlineSnapshot(`
      "
      export const App = () => {
        return (
          /**
            * First
            */
          2);

      };

      /**
        * Second
        */"
    `);
  });
});

const suites = fixtures(new URL("./fixtures", import.meta.url));

afterEach(() => {
  jest.restoreAllMocks();
});

suites.forEach(function (testSuite) {
  describe("generation/" + testSuite.title, function () {
    testSuite.tests.forEach(function (task) {
      const testFn = task.disabled ? it.skip : it;

      testFn(
        task.title,

        function () {
          const expected = task.expect;
          const actual = task.actual;
          const actualCode = actual.code;

          if (actualCode) {
            const parserOpts = {
              filename: actual.loc,
              plugins: task.options.plugins || [],
              strictMode: task.options.strictMode === false ? false : true,
              sourceType: "module",
              sourceMaps: !!task.sourceMap,
              ...task.options.parserOpts,
            };
            const actualAst = parse(actualCode, parserOpts);
            const options = {
              sourceFileName: path.relative(__dirname, actual.loc),
              ...task.options,
              sourceMaps: task.sourceMap ? true : task.options.sourceMaps,
            };

            const run = () => {
              return generate(actualAst, options, actualCode);
            };

            const throwMsg = options.throws;
            if (throwMsg) {
              expect(() => run()).toThrow(
                throwMsg === true ? undefined : throwMsg,
              );
            } else {
              jest.spyOn(console, "warn").mockImplementation(() => {});

              const result = run();

              if (
                options.warns &&
                (!process.env.IS_PUBLISH || !options.noWarnInPublishBuild)
              ) {
                expect(console.warn).toHaveBeenCalledWith(
                  expect.stringContaining(options.warns),
                );
              } else {
                expect(console.warn).not.toHaveBeenCalled();
              }

              if (options.sourceMaps) {
                try {
                  expect(result.map).toEqual(task.sourceMap);
                } catch (e) {
                  if (!process.env.OVERWRITE && task.sourceMap) throw e;
                  console.log(`Updated test file: ${task.sourceMapFile.loc}`);
                  fs.writeFileSync(
                    task.sourceMapFile.loc,
                    JSON.stringify(result.map, null, 2),
                  );
                }
              }

              if (
                !expected.code &&
                result.code &&
                fs.statSync(path.dirname(expected.loc)).isDirectory() &&
                !process.env.CI
              ) {
                console.log(`New test file created: ${expected.loc}`);
                fs.writeFileSync(expected.loc, result.code);
              } else {
                try {
                  expect(result.code).toBe(expected.code);
                  if (!options.expectedReParseError) {
                    expect(() => {
                      parse(result.code, parserOpts);
                    }).not.toThrow();
                  }
                } catch (e) {
                  if (!process.env.OVERWRITE) throw e;
                  console.log(`Updated test file: ${expected.loc}`);
                  fs.writeFileSync(expected.loc, result.code);
                }
              }
            }
          }
        },
      );
    });
  });
});
