// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as assert from "assert";
import execa from "execa";
import * as fs from "fs/promises";
import { readdirSync } from "fs";
import * as path from "path";
import * as semver from "semver";
import * as typescript from "typescript";
// @ts-ignore: The test installs @pulumi/pulumi
import { runtime } from "@pulumi/pulumi";
// @ts-ignore: The test installs @pulumi/pulumi
import * as pkg from "@pulumi/pulumi/runtime/closure/package";


const platformIndependentEOL = /\r\n|\r|\n/g;

// Load the snapshot with a version range that satisfies the typescript version.
async function getSnapshot(testCase: string, typescriptVersion: string): Promise<string> {
    const files = await fs.readdir(`./cases/${testCase}`);
    for (const file of files) {
        if (file.startsWith("snapshot.") && file.endsWith(".txt")) {
            const range = file.slice("snapshot.".length, -".txt".length);
            if (range) {
                if (semver.satisfies(typescriptVersion, range)) {
                    return fs.readFile(path.join("cases", testCase, `snapshot.${range}.txt`), "utf-8");
                }
            }
        }
    }
    return fs.readFile(path.join("cases", testCase, "snapshot.txt"), "utf-8");
}

// This test validates that the typescript version used by the closure tests
// is the same as the one used by the pulumi package and that we are testing
// what we think we are testing ...
it(`resolve to the correct typescript version within the pulumi package`,
    async function () {
        const { stdout } = await execa("npm", ["ls", "typescript", "--json"], { cwd: __dirname, reject: false });
        const deps = JSON.parse(stdout);
        const version = deps.dependencies["@pulumi/pulumi"].dependencies.typescript.version;
        assert.strictEqual(version, typescript.version);
    });

describe(`closure tests (TypeScript ${typescript.version})`, function () {
    const cases = readdirSync("cases"); // describe does not support async functions
    for (const testCase of cases) {
        const { func, isFactoryFunction, error: expectedError, description, allowSecrets, after } = require(`./cases/${testCase}`);

        const nodeMajor = parseInt(process.version.split(".")[0].slice(1));
        if (description === "Use webcrypto via global.crypto" && nodeMajor < 19) {
            // This test uses global.crypto, which is only available in Node 19 and later.
            continue;
        }

        it(`${description} (TypeScript ${typescript.version})`, async () => {
            if (expectedError) {
                await assert.rejects(async () => {
                    await runtime.serializeFunction(func, {
                        allowSecrets: allowSecrets ?? false,
                        isFactoryFunction: isFactoryFunction ?? false,
                    });
                }, err => {
                    const actual = anonymizeFunctionNames((<Error>err).message);
                    assert.strictEqual(actual, expectedError);
                    return true;
                });
            } else {
                const sf = await runtime.serializeFunction(func, {
                    allowSecrets: allowSecrets ?? false,
                    isFactoryFunction: isFactoryFunction ?? false,
                });
                if (after) {
                    after();
                }
                let snapshot = await getSnapshot(testCase, typescript.version);
                // Replace all new lines with \n to make the comparison platform independent.
                snapshot = snapshot.replace(platformIndependentEOL, "\n")
                const actual = sf.text.replace(platformIndependentEOL, "\n")
                assert.strictEqual(actual, snapshot);
            }
        });
    }
});

function anonymizeFunctionNames(text: string): string {
    return text.replace(/function '.+'/g, "function '<anonymous>'");
}

describe("mock package", () => {
    describe("module", () => {
        it("remaps exports correctly for mockpackage", () => {
            assert.strictEqual(pkg.getModuleFromPath("mockpackage/lib/index.js"), "mockpackage");
        });
        it("should return undefined on unexported members", () => {
            assert.throws(() => pkg.getModuleFromPath("mockpackage/lib/external.js"));
        });
    });
    describe("disregard null targets", () => {
        // ./node_modules/es-module-package/package.json
        const packagedef = {
            name: "es-module-package",
            exports: {
                "./features/private-internal-b/*": null,
                "./features/*": "./src/features/*.js",
                "./features/private-internal/*": null,
            },
        };
        it(`handles wildcard paths`, () => {
            assert.strictEqual(
                pkg.getModuleFromPath("es-module-package/src/features/private-inter.js", packagedef),
                "es-module-package/features/private-inter",
            );
            assert.strictEqual(
                pkg.getModuleFromPath("es-module-package/src/features/x.js", packagedef),
                "es-module-package/features/x",
            );
            assert.strictEqual(
                pkg.getModuleFromPath("es-module-package/src/features/y/z/foo/bar/baz.js", packagedef),
                "es-module-package/features/y/z/foo/bar/baz",
            );
        });
        it(`handles whitelisting blacklisted directories`, () => {
            assert.strictEqual(
                pkg.getModuleFromPath("es-module-package/features/internal/public/index.js", {
                    name: "es-module-package",
                    exports: {
                        "./features/internal/*": null,
                        ".": "./features/internal/public/index.js",
                    },
                }),
                "es-module-package",
            );
        });
    });
    describe("basic package exports", () => {
        // https://nodejs.org/api/packages.html#package-entry-points
        const packagedef = {
            name: "my-mod",
            exports: {
                ".": "./lib/index.js",
                "./lib": "./lib/index.js",
                "./lib/index": "./lib/index.js",
                "./lib/index.js": "./lib/index.js",
                "./feature": "./feature/index.js",
                "./feature/index.js": "./feature/index.js",
                "./package.json": "./package.json",
            },
        };
        it("handles multiple aliases 1", () => {
            assert.strictEqual(pkg.getModuleFromPath("my-mod/lib/index.js", packagedef), "my-mod/lib/index.js");
        });
        it("handles multiple aliases 2", () => {
            assert.strictEqual(pkg.getModuleFromPath("my-mod/feature/index.js", packagedef), "my-mod/feature/index.js");
        });
        it("returns with no modification", () => {
            assert.strictEqual(pkg.getModuleFromPath("my-mod/package.json", packagedef), "my-mod/package.json");
        });
    });
    describe("wildcard package exports", () => {
        const packagedef = {
            name: "my-mod",
            exports: {
                ".": "./lib/index.js",
                "./lib": "./lib/index.js",
                "./lib/*": "./lib/*.js",
                "./feature": "./feature/index.js",
                "./feature/*": "./feature/*.js",
                "./package.json": "./package.json",
            },
        };
        it("wildcard module", () => {
            assert.strictEqual(pkg.getModuleFromPath("my-mod/lib/foobar.js", packagedef), "my-mod/lib/foobar");
            assert.strictEqual(pkg.getModuleFromPath("my-mod/lib/foo.js.js", packagedef), "my-mod/lib/foo.js"); // check
            assert.strictEqual(pkg.getModuleFromPath("my-mod/feature/foobar.js", packagedef), "my-mod/feature/foobar");
        });
        it("manual regression tests", () => {
            assert.strictEqual(
                pkg.getModuleFromPath("my-mod/internal/public/index.js.js", {
                    name: "my-mod",
                    exports: {
                        ".": "./internal/public/index.js",
                        "./public/*": "./internal/public/*.js",
                    },
                }),
                "my-mod/public/index.js",
            );
            assert.strictEqual(
                pkg.getModuleFromPath("my-mod/internal/public/index.js", {
                    name: "my-mod",
                    exports: {
                        ".": "./internal/public/index.js",
                        "./public/*": "./internal/public/*",
                    },
                }),
                "my-mod",
            );
        });
    });
    describe("conditional import/require package exports", () => {
        const packagedef = {
            // package.json
            name: "that-mod",
            exports: {
                ".": "./main.js",
                "./feature": {
                    node: "./feature-node.js",
                    default: "./feature.js",
                },
            },
            type: "module",
        };
        it("remaps conditional node/default nested packages", () => {
            assert.strictEqual(pkg.getModuleFromPath("that-mod/main.js", packagedef), "that-mod");
            assert.strictEqual(pkg.getModuleFromPath("that-mod/feature-node.js", packagedef), "that-mod/feature");
            assert.strictEqual(pkg.getModuleFromPath("that-mod/feature.js", packagedef), "that-mod/feature");
        });
    });
    describe("conditional import/require package exports", () => {
        const packagedef = {
            // package.json
            name: "this-mod",
            main: "./main-require.cjs",
            exports: {
                import: "./main-module.js",
                require: "./main-require.cjs",
            },
            type: "module",
        };
        it("remaps to main pkg", () => {
            assert.throws(() => pkg.getModuleFromPath("this-mod/main-module.js", packagedef));
            assert.strictEqual(pkg.getModuleFromPath("this-mod/main-require.cjs", packagedef), "this-mod");
        });
    });

    describe("error cases", () => {
        it("returns the original module if package.json not found", () => {
            assert.strictEqual(pkg.getModuleFromPath("this-mod/main-require.cjs"), "this-mod/main-require.cjs");
        });
    });
});