// Copyright 2016-2021, 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 assert from "assert";
import * as semver from "semver";
import * as tmp from "tmp";
import * as upath from "upath";
import * as fs from "fs";

import {
    CommandResult,
    ConfigMap,
    EngineEvent,
    fullyQualifiedStackName,
    LocalWorkspace,
    LocalWorkspaceOptions,
    OutputMap,
    ProjectSettings,
    ProjectRuntime,
    PulumiCommand,
    Stack,
} from "../../automation";
import { ComponentResource, ComponentResourceOptions, Config, output } from "../../index";
import { getTestOrg, getTestSuffix } from "./util";

const versionRegex = /(\d+\.)(\d+\.)(\d+)(-.*)?/;
const userAgent = "pulumi/pulumi/test";

describe("LocalWorkspace", () => {
    it(`projectSettings from yaml/yml/json`, async () => {
        for (const ext of ["yaml", "yml", "json"]) {
            const ws = await LocalWorkspace.create(
                withTestBackend(
                    { workDir: upath.joinSafe(__dirname, "data", ext) },
                    "testproj",
                    "A minimal Go Pulumi program",
                    "go",
                ),
            );
            const settings = await ws.projectSettings();
            assert.strictEqual(settings.name, "testproj");
            assert.strictEqual(settings.runtime, "go");
            assert.strictEqual(settings.description, "A minimal Go Pulumi program");
        }
    });

    it(`stackSettings from yaml/yml/json`, async () => {
        for (const ext of ["yaml", "yml", "json"]) {
            const ws = await LocalWorkspace.create(
                withTestBackend({ workDir: upath.joinSafe(__dirname, "data", ext) }),
            );
            const settings = await ws.stackSettings("dev");
            assert.strictEqual(settings.secretsProvider, "abc");
            assert.strictEqual(settings.config!["plain"], "plain");
            assert.strictEqual(settings.config!["secure"].secure, "secret");
            await ws.saveStackSettings("dev", settings);
            assert.strictEqual(settings.secretsProvider, "abc");
        }
    });

    it(`fails gracefully for missing local workspace workDir`, async () => {
        try {
            const ws = await LocalWorkspace.create(withTestBackend({ workDir: "invalid-missing-workdir" }));
            assert.fail("expected create with invalid workDir to throw");
        } catch (err) {
            assert.strictEqual(
                err.toString(),
                "Error: Invalid workDir passed to local workspace: 'invalid-missing-workdir' does not exist",
            );
        }
    });

    it(`adds/removes/lists plugins successfully`, async () => {
        const ws = await LocalWorkspace.create(withTestBackend({}));
        await ws.installPlugin("aws", "v3.0.0");
        // See https://github.com/pulumi/pulumi/issues/11013 for why this is disabled
        //await ws.installPluginFromServer("scaleway", "v1.2.0", "github://api.github.com/lbrlabs");
        await ws.removePlugin("aws", "3.0.0");
        await ws.listPlugins();
    });

    it(`create/select/remove LocalWorkspace stack`, async () => {
        const projectName = "node_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        await ws.createStack(stackName);
        await ws.selectStack(stackName);
        await ws.removeStack(stackName);
    });

    it(`create/select/createOrSelect Stack`, async () => {
        const projectName = "node_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        await Stack.create(stackName, ws);
        await Stack.select(stackName, ws);
        await Stack.createOrSelect(stackName, ws);
        await ws.removeStack(stackName);
    });
    describe("Tag methods: get/set/remove/list", () => {
        const projectName = "testProjectName";
        const runtime = "nodejs";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime,
        };
        let workspace: LocalWorkspace;
        beforeEach(async () => {
            workspace = await LocalWorkspace.create(
                withTestBackend({
                    projectSettings: projectSettings,
                }),
            );
            await workspace.createStack(stackName);
        });
        it("lists tag values", async () => {
            if (!process.env.PULUMI_ACCESS_TOKEN) {
                console.log('Skipping "list tag values values" test');
                // Skip the test because the local backend doesn't support tags
                return;
            }
            const result = await workspace.listTags(stackName);
            assert.strictEqual(result["pulumi:project"], projectName);
            assert.strictEqual(result["pulumi:runtime"], runtime);
        });
        it("sets and removes tag values", async () => {
            if (!process.env.PULUMI_ACCESS_TOKEN) {
                console.log('Skipping "sets and removes tag values" test');
                // Skip the test because the local backend doesn't support tags
                return;
            }
            // sets
            await workspace.setTag(stackName, "foo", "bar");
            const actualValue = await workspace.getTag(stackName, "foo");
            assert.strictEqual(actualValue, "bar");
            // removes
            await workspace.removeTag(stackName, "foo");
            const actualTags = await workspace.listTags(stackName);
            assert.strictEqual(actualTags["foo"], undefined);
        });
        it("gets a single tag value", async () => {
            if (!process.env.PULUMI_ACCESS_TOKEN) {
                console.log('Skipping "gets a single tag value" test');
                // Skip the test because the local backend doesn't support tags
                return;
            }
            const actualValue = await workspace.getTag(stackName, "pulumi:project");
            assert.strictEqual(actualValue, actualValue.trim());
            assert.strictEqual(actualValue, projectName);
        });
        afterEach(async () => {
            await workspace.removeStack(stackName);
        });
    });
    describe("ListStack Methods", async () => {
        describe("ListStacks", async () => {
            const stackJson = `[
                    {
                        "name": "testorg1/testproj1/teststack1",
                        "current": false,
                        "url": "https://app.pulumi.com/testorg1/testproj1/teststack1"
                    },
                    {
                        "name": "testorg1/testproj1/teststack2",
                        "current": false,
                        "url": "https://app.pulumi.com/testorg1/testproj1/teststack2"
                    }
                ]`;
            it(`should handle stacks correctly for listStacks`, async () => {
                const mockWithReturnedStacks = {
                    command: "pulumi",
                    version: null,
                    run: async (args: string[], cwd: string, additionalEnv: { [key: string]: string }) => {
                        return new CommandResult(stackJson, "", 0);
                    },
                };

                const workspace = await LocalWorkspace.create(
                    withTestBackend({ pulumiCommand: mockWithReturnedStacks }),
                );
                const stacks = await workspace.listStacks();

                assert.strictEqual(stacks.length, 2);
                assert.strictEqual(stacks[0].name, "testorg1/testproj1/teststack1");
                assert.strictEqual(stacks[0].current, false);
                assert.strictEqual(stacks[0].url, "https://app.pulumi.com/testorg1/testproj1/teststack1");
                assert.strictEqual(stacks[1].name, "testorg1/testproj1/teststack2");
                assert.strictEqual(stacks[1].current, false);
                assert.strictEqual(stacks[1].url, "https://app.pulumi.com/testorg1/testproj1/teststack2");
            });

            it(`should use correct args for listStacks`, async () => {
                let capturedArgs: string[] = [];
                const mockPulumiCommand = {
                    command: "pulumi",
                    version: null,
                    run: async (args: string[], cwd: string, additionalEnv: { [key: string]: string }) => {
                        capturedArgs = args;
                        return new CommandResult(stackJson, "", 0);
                    },
                };
                const workspace = await LocalWorkspace.create(
                    withTestBackend({
                        pulumiCommand: mockPulumiCommand,
                    }),
                );
                await workspace.listStacks();
                assert.deepStrictEqual(capturedArgs, ["stack", "ls", "--json"]);
            });
        });

        describe("ListStacks with all", async () => {
            const stackJson = `[
                    {
                        "name": "testorg1/testproj1/teststack1",
                        "current": false,
                        "url": "https://app.pulumi.com/testorg1/testproj1/teststack1"
                    },
                    {
                        "name": "testorg1/testproj2/teststack2",
                        "current": false,
                        "url": "https://app.pulumi.com/testorg1/testproj2/teststack2"
                    }
                ]`;
            it(`should handle stacks correctly for listStacks when all is set`, async () => {
                const mockWithReturnedStacks = {
                    command: "pulumi",
                    version: null,
                    run: async () => new CommandResult(stackJson, "", 0),
                };
                const workspace = await LocalWorkspace.create(
                    withTestBackend({
                        pulumiCommand: mockWithReturnedStacks,
                    }),
                );
                const stacks = await workspace.listStacks({ all: true });
                assert.strictEqual(stacks.length, 2);
                assert.strictEqual(stacks[0].name, "testorg1/testproj1/teststack1");
                assert.strictEqual(stacks[0].current, false);
                assert.strictEqual(stacks[0].url, "https://app.pulumi.com/testorg1/testproj1/teststack1");
                assert.strictEqual(stacks[1].name, "testorg1/testproj2/teststack2");
                assert.strictEqual(stacks[1].current, false);
                assert.strictEqual(stacks[1].url, "https://app.pulumi.com/testorg1/testproj2/teststack2");
            });

            it(`should use correct args for listStacks when all is set`, async () => {
                let capturedArgs: string[] = [];
                const mockPuluiCommand = {
                    command: "pulumi",
                    version: null,
                    run: async (args: string[], cwd: string, additionalEnv: { [key: string]: string }) => {
                        capturedArgs = args;
                        return new CommandResult(stackJson, "", 0);
                    },
                };
                const workspace = await LocalWorkspace.create(
                    withTestBackend({
                        pulumiCommand: mockPuluiCommand,
                    }),
                );
                await workspace.listStacks({ all: true });
                assert.deepStrictEqual(capturedArgs, ["stack", "ls", "--json", "--all"]);
            });
        });
    });
    it(`Environment functions`, async function () {
        // Skipping test because the required environments are in the moolumi org.
        if (getTestOrg() !== "moolumi") {
            this.skip();
            return;
        }
        const projectName = "node_env_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await Stack.create(stackName, ws);

        // Adding non-existent env should fail.
        await assert.rejects(
            stack.addEnvironments("non-existent-env"),
            "stack.addEnvironments('non-existent-env') did not reject",
        );

        // Adding existing envs should succeed.
        await stack.addEnvironments("automation-api-test-env", "automation-api-test-env-2");

        let envs = await stack.listEnvironments();
        assert.deepStrictEqual(envs, ["automation-api-test-env", "automation-api-test-env-2"]);

        const config = await stack.getAllConfig();
        assert.strictEqual(config["node_env_test:new_key"].value, "test_value");
        assert.strictEqual(config["node_env_test:also"].value, "business");

        // Removing existing env should succeed.
        await stack.removeEnvironment("automation-api-test-env");
        envs = await stack.listEnvironments();
        assert.deepStrictEqual(envs, ["automation-api-test-env-2"]);

        const alsoConfig = await stack.getConfig("also");
        assert.strictEqual(alsoConfig.value, "business");
        await assert.rejects(stack.getConfig("new_key"), "stack.getConfig('new_key') did not reject");

        await stack.removeEnvironment("automation-api-test-env-2");
        envs = await stack.listEnvironments();
        assert.strictEqual(envs.length, 0);
        await assert.rejects(stack.getConfig("also"), "stack.getConfig('also') did not reject");

        await ws.removeStack(stackName);
    });
    it(`Config`, async () => {
        const projectName = "node_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await Stack.create(stackName, ws);

        const config = {
            plain: { value: "abc" },
            secret: { value: "def", secret: true },
        };
        let caught = 0;

        const plainKey = normalizeConfigKey("plain", projectName);
        const secretKey = normalizeConfigKey("secret", projectName);

        try {
            await stack.getConfig(plainKey);
        } catch (error) {
            caught++;
        }
        assert.strictEqual(caught, 1, "expected config get on empty value to throw");

        let values = await stack.getAllConfig();
        assert.strictEqual(Object.keys(values).length, 0, "expected stack config to be empty");
        await stack.setAllConfig(config);
        values = await stack.getAllConfig();
        assert.strictEqual(values[plainKey].value, "abc");
        assert.strictEqual(values[plainKey].secret, false);
        assert.strictEqual(values[secretKey].value, "def");
        assert.strictEqual(values[secretKey].secret, true);

        await stack.removeConfig("plain");
        values = await stack.getAllConfig();
        assert.strictEqual(Object.keys(values).length, 1, "expected stack config to have 1 value");
        await stack.setConfig("foo", { value: "bar" });
        values = await stack.getAllConfig();
        assert.strictEqual(Object.keys(values).length, 2, "expected stack config to have 2 values");

        await ws.removeStack(stackName);
    });
    it(`config_flag_like`, async () => {
        const projectName = "config_flag_like";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await Stack.create(stackName, ws);
        await stack.setConfig("key", { value: "-value" });
        await stack.setConfig("secret-key", { value: "-value", secret: true });
        const values = await stack.getAllConfig();
        assert.strictEqual(values["config_flag_like:key"].value, "-value");
        assert.strictEqual(values["config_flag_like:key"].secret, false);
        assert.strictEqual(values["config_flag_like:secret-key"].value, "-value");
        assert.strictEqual(values["config_flag_like:secret-key"].secret, true);
        await stack.setAllConfig({
            key: { value: "-value2" },
            "secret-key": { value: "-value2", secret: true },
        });
        const values2 = await stack.getAllConfig();
        assert.strictEqual(values2["config_flag_like:key"].value, "-value2");
        assert.strictEqual(values2["config_flag_like:key"].secret, false);
        assert.strictEqual(values2["config_flag_like:secret-key"].value, "-value2");
        assert.strictEqual(values2["config_flag_like:secret-key"].secret, true);
    });
    it(`Config path`, async () => {
        const projectName = "node_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await Stack.create(stackName, ws);

        // test backward compatibility
        await stack.setConfig("key1", { value: "value1" });
        // test new flag without subPath
        await stack.setConfig("key2", { value: "value2" }, false);
        // test new flag with subPath
        await stack.setConfig("key3.subKey1", { value: "value3" }, true);
        // test secret
        await stack.setConfig("key4", { value: "value4", secret: true });
        // test subPath and key as secret
        await stack.setConfig("key5.subKey1", { value: "value5", secret: true }, true);
        // test string with dots
        await stack.setConfig("key6.subKey1", { value: "value6", secret: true });
        // test string with dots
        await stack.setConfig("key7.subKey1", { value: "value7", secret: true }, false);
        // test subPath
        await stack.setConfig("key7.subKey2", { value: "value8" }, true);
        // test subPath
        await stack.setConfig("key7.subKey3", { value: "value9" }, true);

        // test backward compatibility
        const cv1 = await stack.getConfig("key1");
        assert.strictEqual(cv1.value, "value1");
        assert.strictEqual(cv1.secret, false);

        // test new flag without subPath
        const cv2 = await stack.getConfig("key2", false);
        assert.strictEqual(cv2.value, "value2");
        assert.strictEqual(cv2.secret, false);

        // test new flag with subPath
        const cv3 = await stack.getConfig("key3.subKey1", true);
        assert.strictEqual(cv3.value, "value3");
        assert.strictEqual(cv3.secret, false);

        // test secret
        const cv4 = await stack.getConfig("key4");
        assert.strictEqual(cv4.value, "value4");
        assert.strictEqual(cv4.secret, true);

        // test subPath and key as secret
        const cv5 = await stack.getConfig("key5.subKey1", true);
        assert.strictEqual(cv5.value, "value5");
        assert.strictEqual(cv5.secret, true);

        // test string with dots
        const cv6 = await stack.getConfig("key6.subKey1");
        assert.strictEqual(cv6.value, "value6");
        assert.strictEqual(cv6.secret, true);

        // test string with dots
        const cv7 = await stack.getConfig("key7.subKey1", false);
        assert.strictEqual(cv7.value, "value7");
        assert.strictEqual(cv7.secret, true);

        // test string with dots
        const cv8 = await stack.getConfig("key7.subKey2", true);
        assert.strictEqual(cv8.value, "value8");
        assert.strictEqual(cv8.secret, false);

        // test string with dots
        const cv9 = await stack.getConfig("key7.subKey3", true);
        assert.strictEqual(cv9.value, "value9");
        assert.strictEqual(cv9.secret, false);

        await stack.removeConfig("key1");
        await stack.removeConfig("key2", false);
        await stack.removeConfig("key3", false);
        await stack.removeConfig("key4", false);
        await stack.removeConfig("key5", false);
        await stack.removeConfig("key6.subKey1", false);
        await stack.removeConfig("key7.subKey1", false);

        const cfg = await stack.getAllConfig();
        assert.strictEqual(cfg["node_test:key7"].value, '{"subKey2":"value8","subKey3":"value9"}');

        await ws.removeStack(stackName);
    });
    // This test requires the existence of a Pulumi.dev.yaml file because we are reading the nested
    // config from the file. This means we can't remove the stack at the end of the test.
    // We should also not include secrets in this config, because the secret encryption is only valid within
    // the context of a stack and org, and running this test in different orgs will fail if there are secrets.
    it(`nested_config`, async () => {
        const stackName = fullyQualifiedStackName(getTestOrg(), "nested_config", "dev");
        const workDir = upath.joinSafe(__dirname, "data", "nested_config");
        const stack = await LocalWorkspace.createOrSelectStack(
            { stackName, workDir },
            withTestBackend({}, "nested_config"),
        );

        const allConfig = await stack.getAllConfig();
        const outerVal = allConfig["nested_config:outer"];
        assert.strictEqual(outerVal.secret, false);
        assert.strictEqual(outerVal.value, '{"inner":"my_value","other":"something_else"}');

        const listVal = allConfig["nested_config:myList"];
        assert.strictEqual(listVal.secret, false);
        assert.strictEqual(listVal.value, '["one","two","three"]');

        const outer = await stack.getConfig("outer");
        assert.strictEqual(outer.secret, false);
        assert.strictEqual(outer.value, '{"inner":"my_value","other":"something_else"}');

        const list = await stack.getConfig("myList");
        assert.strictEqual(list.secret, false);
        assert.strictEqual(list.value, '["one","two","three"]');
    });
    it(`can list stacks and currently selected stack`, async () => {
        const projectName = `node_list_test${getTestSuffix()}`;
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackNamer = () => `int_test${getTestSuffix()}`;
        const stackNames: string[] = [];
        for (let i = 0; i < 2; i++) {
            const stackName = fullyQualifiedStackName(getTestOrg(), projectName, stackNamer());
            stackNames[i] = stackName;
            await Stack.create(stackName, ws);
            const stackSummary = await ws.stack();
            assert.strictEqual(stackSummary?.current, true);
            const stacks = await ws.listStacks();
            assert.strictEqual(stacks.length, i + 1);
        }

        for (const name of stackNames) {
            await ws.removeStack(name);
        }
    });
    it(`returns valid whoami result`, async () => {
        const projectName = "node_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const whoAmIResult = await ws.whoAmI();
        assert(whoAmIResult.user !== null);
        assert(whoAmIResult.url !== null);
    });
    it(`stack status methods`, async () => {
        const projectName = "node_test";
        const projectSettings: ProjectSettings = {
            name: projectName,
            runtime: "nodejs",
        };
        const ws = await LocalWorkspace.create(withTestBackend({ projectSettings }));
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await Stack.create(stackName, ws);
        const history = await stack.history();
        assert.strictEqual(history.length, 0);
        const info = await stack.info();
        assert.strictEqual(typeof info, "undefined");
        await ws.removeStack(stackName);
    });
    // TODO[pulumi/pulumi#8220] understand why this test was flaky
    xit(`runs through the stack lifecycle with a local program`, async () => {
        const stackName = fullyQualifiedStackName(getTestOrg(), "testproj", `int_test${getTestSuffix()}`);
        const workDir = upath.joinSafe(__dirname, "data", "testproj");
        const stack = await LocalWorkspace.createStack({ stackName, workDir }, withTestBackend({}));

        const config: ConfigMap = {
            bar: { value: "abc" },
            buzz: { value: "secret", secret: true },
        };
        await stack.setAllConfig(config);

        // pulumi up
        const upRes = await stack.up({ userAgent });
        assert.strictEqual(Object.keys(upRes.outputs).length, 3);
        assert.strictEqual(upRes.outputs["exp_static"].value, "foo");
        assert.strictEqual(upRes.outputs["exp_static"].secret, false);
        assert.strictEqual(upRes.outputs["exp_cfg"].value, "abc");
        assert.strictEqual(upRes.outputs["exp_cfg"].secret, false);
        assert.strictEqual(upRes.outputs["exp_secret"].value, "secret");
        assert.strictEqual(upRes.outputs["exp_secret"].secret, true);
        assert.strictEqual(upRes.summary.kind, "update");
        assert.strictEqual(upRes.summary.result, "succeeded");

        // pulumi preview
        const preRes = await stack.preview({ userAgent });
        assert.strictEqual(preRes.changeSummary.same, 1);

        // pulumi refresh
        const refRes = await stack.refresh({ userAgent });
        assert.strictEqual(refRes.summary.kind, "refresh");
        assert.strictEqual(refRes.summary.result, "succeeded");

        // pulumi destroy
        const destroyRes = await stack.destroy({ userAgent });
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });
    it(`runs through the stack lifecycle with a local dotnet program`, async () => {
        const stackName = fullyQualifiedStackName(getTestOrg(), "testproj_dotnet", `int_test${getTestSuffix()}`);
        const workDir = upath.joinSafe(__dirname, "data", "testproj_dotnet");
        const stack = await LocalWorkspace.createStack(
            { stackName, workDir },
            withTestBackend({}, "testproj_dotnet", "", "dotnet"),
        );

        // pulumi up
        const upRes = await stack.up({ userAgent });
        assert.strictEqual(Object.keys(upRes.outputs).length, 1);
        assert.strictEqual(upRes.outputs["exp_static"].value, "foo");
        assert.strictEqual(upRes.outputs["exp_static"].secret, false);
        assert.strictEqual(upRes.summary.kind, "update");
        assert.strictEqual(upRes.summary.result, "succeeded");

        // pulumi preview
        const preRes = await stack.preview({ userAgent });
        assert.strictEqual(preRes.changeSummary.same, 1);

        // pulumi refresh
        const refRes = await stack.refresh({ userAgent });
        assert.strictEqual(refRes.summary.kind, "refresh");
        assert.strictEqual(refRes.summary.result, "succeeded");

        // pulumi destroy
        const destroyRes = await stack.destroy({ userAgent });
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });
    it(`runs through the stack lifecycle with an inline program`, async () => {
        const program = async () => {
            const config = new Config();
            return {
                exp_static: "foo",
                exp_cfg: config.get("bar"),
                exp_secret: config.getSecret("buzz"),
            };
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        const stackConfig: ConfigMap = {
            bar: { value: "abc" },
            buzz: { value: "secret", secret: true },
        };
        await stack.setAllConfig(stackConfig);

        // pulumi up
        const upRes = await stack.up({ userAgent });
        assert.strictEqual(Object.keys(upRes.outputs).length, 3);
        assert.strictEqual(upRes.outputs["exp_static"].value, "foo");
        assert.strictEqual(upRes.outputs["exp_static"].secret, false);
        assert.strictEqual(upRes.outputs["exp_cfg"].value, "abc");
        assert.strictEqual(upRes.outputs["exp_cfg"].secret, false);
        assert.strictEqual(upRes.outputs["exp_secret"].value, "secret");
        assert.strictEqual(upRes.outputs["exp_secret"].secret, true);
        assert.strictEqual(upRes.summary.kind, "update");
        assert.strictEqual(upRes.summary.result, "succeeded");

        // pulumi preview
        const preRes = await stack.preview({ userAgent });
        assert.strictEqual(preRes.changeSummary.same, 1);

        // pulumi refresh
        const refRes = await stack.refresh({ userAgent });
        assert.strictEqual(refRes.summary.kind, "refresh");
        assert.strictEqual(refRes.summary.result, "succeeded");

        // pulumi destroy
        const destroyRes = await stack.destroy({ userAgent });
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });
    it(`runs through the stack lifecycle with an inline program, testing removing without destroying`, async () => {
        const program = async () => {
            class MyResource extends ComponentResource {
                constructor(name: string, opts?: ComponentResourceOptions) {
                    super("my:module:MyResource", name, {}, opts);
                }
            }
            new MyResource("res");
            return {};
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        await stack.up({ userAgent });

        // we shouldn't be able to remove the stack without force
        // since the stack has an active resource
        assert.rejects(stack.workspace.removeStack(stackName));

        await stack.workspace.removeStack(stackName, { force: true });

        // we shouldn't be able to select the stack after it's been removed
        // we expect this error
        assert.rejects(stack.workspace.selectStack(stackName));
    });
    it("runs through the stack lifecycle with an inline program, testing destroy with --remove", async () => {
        // Arrange.
        const program = async () => {
            class MyResource extends ComponentResource {
                constructor(name: string, opts?: ComponentResourceOptions) {
                    super("my:module:MyResource", name, {}, opts);
                }
            }
            new MyResource("res");
            return {};
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        await stack.up({ userAgent });

        // Act.
        await stack.destroy({ userAgent, remove: true });

        // Assert.
        await assert.rejects(stack.workspace.selectStack(stackName));
    });
    it(`refreshes with refresh option`, async () => {
        // We create a simple program, and scan the output for an indication
        // that adding refresh: true will perfrom a refresh operation.
        const program = async () => {
            return {
                toggle: true,
            };
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );
        // • First, run Up so we can set the initial state.
        await stack.up({ userAgent });
        // • Next, run preview with refresh and check that the refresh was performed.
        const refresh = true;
        const previewRes = await stack.preview({ userAgent, refresh });
        assert.match(previewRes.stdout, /refreshing/);
        assert.strictEqual(previewRes.changeSummary.same, 1, "preview expected 1 same (the stack)");

        const upRes = await stack.up({ userAgent, refresh });
        assert.match(upRes.stdout, /refreshing/);

        const destroyRes = await stack.destroy({ userAgent, refresh });
        assert.match(destroyRes.stdout, /refreshing/);
    });
    it(`destroys an inline program with excludeProtected`, async () => {
        const program = async () => {
            class MyResource extends ComponentResource {
                constructor(name: string, opts?: ComponentResourceOptions) {
                    super("my:module:MyResource", name, {}, opts);
                }
            }
            const config = new Config();
            const protect = config.getBoolean("protect") ?? false;
            new MyResource("first", { protect });
            new MyResource("second");
            return {};
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        // initial up
        await stack.setConfig("protect", { value: "true" });
        await stack.up({ userAgent });

        // pulumi destroy
        const destroyRes = await stack.destroy({ userAgent, excludeProtected: true });
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");
        assert.match(destroyRes.stdout, /All unprotected resources were destroyed/);

        // unprotected resources
        await stack.removeConfig("protect");
        await stack.up({ userAgent });

        // pulumi destroy to cleanup all resources
        await stack.destroy({ userAgent });

        await stack.workspace.removeStack(stackName);
    });
    it(`successfully initializes multiple stacks`, async () => {
        const program = async () => {
            const config = new Config();
            return {
                exp_static: "foo",
                exp_cfg: config.get("bar"),
                exp_secret: config.getSecret("buzz"),
            };
        };
        const projectName = "inline_node";
        const stackNames = Array.from(Array(10).keys()).map((_) =>
            fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`),
        );
        const stacks = await Promise.all(
            stackNames.map(
                async (stackName) => LocalWorkspace.createStack({ stackName, projectName, program }),
                withTestBackend({}, "inline_node"),
            ),
        );
        await stacks.map((stack) => stack.workspace.removeStack(stack.name));
    });
    it(`runs through the stack lifecycle with multiple inline programs in parallel`, async () => {
        const program = async () => {
            const config = new Config();
            return {
                exp_static: "foo",
                exp_cfg: config.get("bar"),
                exp_secret: config.getSecret("buzz"),
            };
        };
        const projectName = "inline_node";
        const stackNames = Array.from(Array(30).keys()).map((_) =>
            fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`),
        );

        const testStackLifetime = async (stackName: string) => {
            const stack = await LocalWorkspace.createStack(
                { stackName, projectName, program },
                withTestBackend({}, "inline_node"),
            );

            const stackConfig: ConfigMap = {
                bar: { value: "abc" },
                buzz: { value: "secret", secret: true },
            };
            await stack.setAllConfig(stackConfig);

            // pulumi up
            const upRes = await stack.up({ userAgent }); // pulumi up
            assert.strictEqual(Object.keys(upRes.outputs).length, 3);
            assert.strictEqual(upRes.outputs["exp_static"].value, "foo");
            assert.strictEqual(upRes.outputs["exp_static"].secret, false);
            assert.strictEqual(upRes.outputs["exp_cfg"].value, "abc");
            assert.strictEqual(upRes.outputs["exp_cfg"].secret, false);
            assert.strictEqual(upRes.outputs["exp_secret"].value, "secret");
            assert.strictEqual(upRes.outputs["exp_secret"].secret, true);
            assert.strictEqual(upRes.summary.kind, "update");
            assert.strictEqual(upRes.summary.result, "succeeded");

            // pulumi preview
            const preRes = await stack.preview({ userAgent }); // pulumi preview
            assert.strictEqual(preRes.changeSummary.same, 1);

            // pulumi refresh
            const refRes = await stack.refresh({ userAgent });
            assert.strictEqual(refRes.summary.kind, "refresh");
            assert.strictEqual(refRes.summary.result, "succeeded");

            // pulumi destroy
            const destroyRes = await stack.destroy({ userAgent });
            assert.strictEqual(destroyRes.summary.kind, "destroy");
            assert.strictEqual(destroyRes.summary.result, "succeeded");

            await stack.workspace.removeStack(stack.name);
        };
        for (let i = 0; i < stackNames.length; i += 10) {
            const chunk = stackNames.slice(i, i + 10);
            await Promise.all(chunk.map(async (stackName) => await testStackLifetime(stackName)));
        }
    });
    it(`handles events`, async () => {
        const program = async () => {
            const config = new Config();
            return {
                exp_static: "foo",
                exp_cfg: config.get("bar"),
                exp_secret: config.getSecret("buzz"),
            };
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        const stackConfig: ConfigMap = {
            bar: { value: "abc" },
            buzz: { value: "secret", secret: true },
        };
        await stack.setAllConfig(stackConfig);

        let seenSummaryEvent = false;
        const findSummaryEvent = (event: EngineEvent) => {
            if (event.summaryEvent) {
                seenSummaryEvent = true;
            }
        };

        // pulumi preview
        const preRes = await stack.preview({ onEvent: findSummaryEvent });
        assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `preview`");
        assert.strictEqual(preRes.changeSummary.create, 1);

        // pulumi up
        seenSummaryEvent = false;
        const upRes = await stack.up({ onEvent: findSummaryEvent });
        assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `up`");
        assert.strictEqual(upRes.summary.kind, "update");
        assert.strictEqual(upRes.summary.result, "succeeded");

        // pulumi preview
        seenSummaryEvent = false;
        const preResAgain = await stack.preview({ onEvent: findSummaryEvent });
        assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `preview`");
        assert.strictEqual(preResAgain.changeSummary.same, 1);

        // pulumi refresh
        seenSummaryEvent = false;
        const refRes = await stack.refresh({ onEvent: findSummaryEvent });
        assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `refresh`");
        assert.strictEqual(refRes.summary.kind, "refresh");
        assert.strictEqual(refRes.summary.result, "succeeded");

        // pulumi destroy
        seenSummaryEvent = false;
        const destroyRes = await stack.destroy({ onEvent: findSummaryEvent });
        assert.strictEqual(seenSummaryEvent, true, "No SummaryEvent for `destroy`");
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });
    // TODO[pulumi/pulumi#7127]: Re-enabled the warning.
    // Temporarily skipping test until we've re-enabled the warning.
    it.skip(`has secret config warnings`, async () => {
        const program = async () => {
            const config = new Config();

            config.get("plainstr1");
            config.require("plainstr2");
            config.getSecret("plainstr3");
            config.requireSecret("plainstr4");

            config.getBoolean("plainbool1");
            config.requireBoolean("plainbool2");
            config.getSecretBoolean("plainbool3");
            config.requireSecretBoolean("plainbool4");

            config.getNumber("plainnum1");
            config.requireNumber("plainnum2");
            config.getSecretNumber("plainnum3");
            config.requireSecretNumber("plainnum4");

            config.getObject("plainobj1");
            config.requireObject("plainobj2");
            config.getSecretObject("plainobj3");
            config.requireSecretObject("plainobj4");

            config.get("str1");
            config.require("str2");
            config.getSecret("str3");
            config.requireSecret("str4");

            config.getBoolean("bool1");
            config.requireBoolean("bool2");
            config.getSecretBoolean("bool3");
            config.requireSecretBoolean("bool4");

            config.getNumber("num1");
            config.requireNumber("num2");
            config.getSecretNumber("num3");
            config.requireSecretNumber("num4");

            config.getObject("obj1");
            config.requireObject("obj2");
            config.getSecretObject("obj3");
            config.requireSecretObject("obj4");
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        const stackConfig: ConfigMap = {
            plainstr1: { value: "1" },
            plainstr2: { value: "2" },
            plainstr3: { value: "3" },
            plainstr4: { value: "4" },
            plainbool1: { value: "true" },
            plainbool2: { value: "true" },
            plainbool3: { value: "true" },
            plainbool4: { value: "true" },
            plainnum1: { value: "1" },
            plainnum2: { value: "2" },
            plainnum3: { value: "3" },
            plainnum4: { value: "4" },
            plainobj1: { value: "{}" },
            plainobj2: { value: "{}" },
            plainobj3: { value: "{}" },
            plainobj4: { value: "{}" },
            str1: { value: "1", secret: true },
            str2: { value: "2", secret: true },
            str3: { value: "3", secret: true },
            str4: { value: "4", secret: true },
            bool1: { value: "true", secret: true },
            bool2: { value: "true", secret: true },
            bool3: { value: "true", secret: true },
            bool4: { value: "true", secret: true },
            num1: { value: "1", secret: true },
            num2: { value: "2", secret: true },
            num3: { value: "3", secret: true },
            num4: { value: "4", secret: true },
            obj1: { value: "{}", secret: true },
            obj2: { value: "{}", secret: true },
            obj3: { value: "{}", secret: true },
            obj4: { value: "{}", secret: true },
        };
        await stack.setAllConfig(stackConfig);

        let events: string[] = [];
        const findDiagnosticEvents = (event: EngineEvent) => {
            if (event.diagnosticEvent?.severity === "warning") {
                events.push(event.diagnosticEvent.message);
            }
        };

        const expectedWarnings = [
            "Configuration 'inline_node:str1' value is a secret; use `getSecret` instead of `get`",
            "Configuration 'inline_node:str2' value is a secret; use `requireSecret` instead of `require`",
            "Configuration 'inline_node:bool1' value is a secret; use `getSecretBoolean` instead of `getBoolean`",
            "Configuration 'inline_node:bool2' value is a secret; use `requireSecretBoolean` instead of `requireBoolean`",
            "Configuration 'inline_node:num1' value is a secret; use `getSecretNumber` instead of `getNumber`",
            "Configuration 'inline_node:num2' value is a secret; use `requireSecretNumber` instead of `requireNumber`",
            "Configuration 'inline_node:obj1' value is a secret; use `getSecretObject` instead of `getObject`",
            "Configuration 'inline_node:obj2' value is a secret; use `requireSecretObject` instead of `requireObject`",
        ];

        // These keys should not be in any warning messages.
        const unexpectedWarnings = [
            "plainstr1",
            "plainstr2",
            "plainstr3",
            "plainstr4",
            "plainbool1",
            "plainbool2",
            "plainbool3",
            "plainbool4",
            "plainnum1",
            "plainnum2",
            "plainnum3",
            "plainnum4",
            "plainobj1",
            "plainobj2",
            "plainobj3",
            "plainobj4",
            "str3",
            "str4",
            "bool3",
            "bool4",
            "num3",
            "num4",
            "obj3",
            "obj4",
        ];

        const validate = (warnings: string[]) => {
            for (const expected of expectedWarnings) {
                let found = false;
                for (const warning of warnings) {
                    if (warning.includes(expected)) {
                        found = true;
                        break;
                    }
                }
                assert.strictEqual(found, true, `expected warning not found`);
            }
            for (const unexpected of unexpectedWarnings) {
                for (const warning of warnings) {
                    assert.strictEqual(
                        warning.includes(unexpected),
                        false,
                        `Unexpected '${unexpected}' found in warning`,
                    );
                }
            }
        };

        // pulumi preview
        await stack.preview({ onEvent: findDiagnosticEvents });
        validate(events);

        // pulumi up
        events = [];
        await stack.up({ onEvent: findDiagnosticEvents });
        validate(events);

        await stack.workspace.removeStack(stackName);
    });
    it(`imports and exports stacks`, async () => {
        const program = async () => {
            const config = new Config();
            return {
                exp_static: "foo",
                exp_cfg: config.get("bar"),
                exp_secret: config.getSecret("buzz"),
            };
        };
        const projectName = "import_export_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "import_export_node"),
        );

        try {
            await stack.setAllConfig({
                bar: { value: "abc" },
                buzz: { value: "secret", secret: true },
            });
            await stack.up();

            // export stack
            const state = await stack.exportStack();

            // import stack
            await stack.importStack(state);
            const configVal = await stack.getConfig("bar");
            assert.strictEqual(configVal.value, "abc");
        } finally {
            const destroyRes = await stack.destroy();
            assert.strictEqual(destroyRes.summary.kind, "destroy");
            assert.strictEqual(destroyRes.summary.result, "succeeded");
            await stack.workspace.removeStack(stackName);
        }
    });
    // TODO[pulumi/pulumi#8061] flaky test
    xit(`supports stack outputs`, async () => {
        const program = async () => {
            const config = new Config();
            return {
                exp_static: "foo",
                exp_cfg: config.get("bar"),
                exp_secret: config.getSecret("buzz"),
            };
        };
        const projectName = "import_export_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "import_export_node"),
        );

        const assertOutputs = (outputs: OutputMap) => {
            assert.strictEqual(Object.keys(outputs).length, 3, "expected to have 3 outputs");
            assert.strictEqual(outputs["exp_static"].value, "foo");
            assert.strictEqual(outputs["exp_static"].secret, false);
            assert.strictEqual(outputs["exp_cfg"].value, "abc");
            assert.strictEqual(outputs["exp_cfg"].secret, false);
            assert.strictEqual(outputs["exp_secret"].value, "secret");
            assert.strictEqual(outputs["exp_secret"].secret, true);
        };

        try {
            await stack.setAllConfig({
                bar: { value: "abc" },
                buzz: { value: "secret", secret: true },
            });

            const initialOutputs = await stack.outputs();
            assert.strictEqual(Object.keys(initialOutputs).length, 0, "expected initialOutputs to be empty");

            // pulumi up
            const upRes = await stack.up();
            assert.strictEqual(upRes.summary.kind, "update");
            assert.strictEqual(upRes.summary.result, "succeeded");
            assertOutputs(upRes.outputs);

            const outputsAfterUp = await stack.outputs();
            assertOutputs(outputsAfterUp);

            const destroyRes = await stack.destroy();
            assert.strictEqual(destroyRes.summary.kind, "destroy");
            assert.strictEqual(destroyRes.summary.result, "succeeded");

            const outputsAfterDestroy = await stack.outputs();
            assert.strictEqual(Object.keys(outputsAfterDestroy).length, 0, "expected outputsAfterDestroy to be empty");
        } finally {
            await stack.workspace.removeStack(stackName);
        }
    });
    it(`runs an inline program that exits gracefully`, async () => {
        const program = async () => ({});
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        // pulumi up
        await assert.doesNotReject(stack.up());

        // pulumi destroy
        const destroyRes = await stack.destroy();
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });
    it(`runs an inline program that rejects a promise and exits gracefully`, async () => {
        const program = async () => {
            Promise.reject(new Error());
            return {};
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        // pulumi up
        await assert.rejects(stack.up());

        // pulumi destroy
        const destroyRes = await stack.destroy();
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });
    it(`runs successfully after a previous failure`, async () => {
        let shouldFail = true;
        const program = async () => {
            if (shouldFail) {
                Promise.reject(new Error());
            }
            return {};
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        // pulumi up rejects the first time
        await assert.rejects(stack.up());

        // pulumi up succeeds the 2nd time
        shouldFail = false;
        await assert.doesNotReject(stack.up());

        // pulumi destroy
        const destroyRes = await stack.destroy();
        assert.strictEqual(destroyRes.summary.kind, "destroy");
        assert.strictEqual(destroyRes.summary.result, "succeeded");

        await stack.workspace.removeStack(stackName);
    });

    it("can import resources into a stack using resource definitions", async () => {
        const workDir = upath.joinSafe(__dirname, "data", "import");
        const stackName = `int_test${getTestSuffix()}`;
        const stack = await LocalWorkspace.createStack({ workDir, stackName }, withTestBackend({}));
        const pulumiRandomVersion = "4.16.3";
        await stack.workspace.installPlugin("random", pulumiRandomVersion);
        const result = await stack.import({
            protect: false,
            resources: [
                {
                    type: "random:index/randomPassword:RandomPassword",
                    name: "randomPassword",
                    id: "supersecret",
                },
            ],
        });
        let kind = "resource-import";
        if (process.env.PULUMI_ACCESS_TOKEN) {
            // The service handles this slightly differently, and we just get "update" as "kind"
            kind = "update";
        }
        assert.strictEqual(result.summary.kind, kind);
        assert.strictEqual(result.summary.result, "succeeded");

        const expectedGeneratedCode = fs.readFileSync(upath.joinSafe(workDir, "expected_generated_code.txt"), "utf8");
        assert.strictEqual(result.generatedCode, expectedGeneratedCode);
        await stack.destroy();
        await stack.workspace.removeStack(stackName);
        await stack.workspace.removePlugin("random", pulumiRandomVersion);
    });

    it("can import resources into a stack without generating code", async () => {
        const workDir = upath.joinSafe(__dirname, "data", "import");
        const stackName = `int_test${getTestSuffix()}`;
        const stack = await LocalWorkspace.createStack({ workDir, stackName }, withTestBackend({}));
        const pulumiRandomVersion = "4.16.3";
        await stack.workspace.installPlugin("random", pulumiRandomVersion);
        const result = await stack.import({
            protect: false,
            generateCode: false,
            resources: [
                {
                    type: "random:index/randomPassword:RandomPassword",
                    name: "randomPassword",
                    id: "supersecret",
                },
            ],
        });
        let kind = "resource-import";
        if (process.env.PULUMI_ACCESS_TOKEN) {
            // The service handles this slightly differently, and we just get "update" as "kind"
            kind = "update";
        }
        assert.strictEqual(result.summary.kind, kind);
        assert.strictEqual(result.summary.result, "succeeded");
        assert.strictEqual(result.generatedCode, "");
        await stack.destroy();
        await stack.workspace.removeStack(stackName);
        await stack.workspace.removePlugin("random", pulumiRandomVersion);
    });

    it(`sets pulumi version`, async () => {
        const ws = await LocalWorkspace.create(withTestBackend({}));
        assert(ws.pulumiVersion);
        assert.strictEqual(versionRegex.test(ws.pulumiVersion), true);
    });
    it("sets pulumi version when using a custom CLI instance", async () => {
        const tmpDir = tmp.dirSync({ prefix: "automation-test-", unsafeCleanup: true });
        try {
            const cmd = await PulumiCommand.get();
            const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: cmd }));
            assert.strictEqual(versionRegex.test(ws.pulumiVersion), true);
        } finally {
            tmpDir.removeCallback();
        }
    });
    it("throws when attempting to retrieve an invalid pulumi version", async () => {
        const mockWithNoVersion = {
            command: "pulumi",
            version: null,
            run: async () => new CommandResult("some output", "", 0),
        };
        const ws = await LocalWorkspace.create(
            withTestBackend({
                pulumiCommand: mockWithNoVersion,
                envVars: {
                    PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK: "true",
                },
            }),
        );
        assert.throws(() => ws.pulumiVersion);
    });
    it("fails creation if remote operation is not supported", async () => {
        const mockWithNoRemoteSupport = {
            command: "pulumi",
            version: new semver.SemVer("2.0.0"),
            // We inspect the output of `pulumi preview --help` to determine
            // if the CLI supports remote operations, see
            // `LocalWorkspace.checkRemoteSupport`.
            run: async () => new CommandResult("some output", "", 0),
        };
        await assert.rejects(
            LocalWorkspace.create(withTestBackend({ pulumiCommand: mockWithNoRemoteSupport, remote: true })),
        );
    });
    it("bypasses remote support check", async () => {
        const mockWithNoRemoteSupport = {
            command: "pulumi",
            version: new semver.SemVer("2.0.0"),
            // We inspect the output of `pulumi preview --help` to determine
            // if the CLI supports remote operations, see
            // `LocalWorkspace.checkRemoteSupport`.
            run: async () => new CommandResult("some output", "", 0),
        };
        await assert.doesNotReject(
            LocalWorkspace.create(
                withTestBackend({
                    pulumiCommand: mockWithNoRemoteSupport,
                    remote: true,
                    envVars: {
                        PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK: "true",
                    },
                }),
            ),
        );
    });
    it(`respects existing project settings`, async () => {
        const projectName = "correct_project";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            {
                stackName,
                projectName,
                program: async () => {
                    return;
                },
            },
            withTestBackend(
                { workDir: upath.joinSafe(__dirname, "data", "correct_project") },
                "correct_project",
                "This is a description",
            ),
        );
        const projectSettings = await stack.workspace.projectSettings();
        assert.strictEqual(projectSettings.name, "correct_project");
        // the description check is enough to verify that the stack wasn't overwritten
        assert.strictEqual(projectSettings.description, "This is a description");
        await stack.workspace.removeStack(stackName);
    });
    it(`correctly sets config on multiple stacks concurrently`, async () => {
        const dones = [];
        const stacks = ["dev", "dev2", "dev3", "dev4", "dev5"];
        const workDir = upath.joinSafe(__dirname, "data", "tcfg");
        const ws = await LocalWorkspace.create({
            workDir,
            projectSettings: {
                name: "concurrent-config",
                runtime: "nodejs",
                backend: { url: "file://~" },
            },
            envVars: {
                PULUMI_CONFIG_PASSPHRASE: "test",
            },
        });
        for (let i = 0; i < stacks.length; i++) {
            await Stack.create(stacks[i], ws);
        }
        for (let i = 0; i < stacks.length; i++) {
            const x = i;
            const s = stacks[i];
            dones.push(
                (async () => {
                    for (let j = 0; j < 20; j++) {
                        await ws.setConfig(s, "var-" + j, { value: (x * 20 + j).toString() });
                    }
                })(),
            );
        }
        await Promise.all(dones);

        for (let i = 0; i < stacks.length; i++) {
            const stack = await LocalWorkspace.selectStack({
                stackName: stacks[i],
                workDir,
            });
            const config = await stack.getAllConfig();
            assert.strictEqual(Object.keys(config).length, 20);
            await stack.workspace.removeStack(stacks[i]);
        }
    });

    it(`runs the install command`, async () => {
        let recordedArgs: string[] = [];
        const mockCommand = {
            command: "pulumi",
            // Version high enough to support --use-language-version-tools
            version: semver.parse("3.130.0"),
            run: async (
                args: string[],
                cwd: string,
                additionalEnv: { [key: string]: string },
                onOutput?: (data: string) => void,
            ): Promise<CommandResult> => {
                recordedArgs = args;
                return new CommandResult("some output", "", 0);
            },
        };
        const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: mockCommand }));

        await ws.install();
        assert.deepStrictEqual(recordedArgs, ["install"]);

        await ws.install({ noPlugins: true });
        assert.deepStrictEqual(recordedArgs, ["install", "--no-plugins"]);

        await ws.install({ noDependencies: true });
        assert.deepStrictEqual(recordedArgs, ["install", "--no-dependencies"]);

        await ws.install({ reinstall: true });
        assert.deepStrictEqual(recordedArgs, ["install", "--reinstall"]);

        await ws.install({ useLanguageVersionTools: true });
        assert.deepStrictEqual(recordedArgs, ["install", "--use-language-version-tools"]);

        await ws.install({
            noDependencies: true,
            noPlugins: true,
            reinstall: true,
            useLanguageVersionTools: true,
        });
        assert.deepStrictEqual(recordedArgs, [
            "install",
            "--use-language-version-tools",
            "--no-plugins",
            "--no-dependencies",
            "--reinstall",
        ]);
    });

    it(`install requires version >= 3.91`, async () => {
        const mockCommand = {
            command: "pulumi",
            version: semver.parse("3.90.0"),
            run: async (
                args: string[],
                cwd: string,
                additionalEnv: { [key: string]: string },
                onOutput?: (data: string) => void,
            ): Promise<CommandResult> => {
                return new CommandResult("some output", "", 0);
            },
        };
        const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: mockCommand }));

        assert.rejects(() => ws.install());
    });

    it(`install --use-language-version-tools requires version >= 3.130`, async () => {
        const mockCommand = {
            command: "pulumi",
            version: semver.parse("3.129.0"),
            run: async (
                args: string[],
                cwd: string,
                additionalEnv: { [key: string]: string },
                onOutput?: (data: string) => void,
            ): Promise<CommandResult> => {
                return new CommandResult("some output", "", 0);
            },
        };
        const ws = await LocalWorkspace.create(withTestBackend({ pulumiCommand: mockCommand }));

        assert.rejects(() => ws.install());
    });

    it("sends SIGINT when aborted", async () => {
        const controller = new AbortController();
        const program = async () => {
            await new Promise((f) => setTimeout(f, 60000));
            return {};
        };
        const projectName = "inline_node";
        const stackName = fullyQualifiedStackName(getTestOrg(), projectName, `int_test${getTestSuffix()}`);
        const stack = await LocalWorkspace.createStack(
            { stackName, projectName, program },
            withTestBackend({}, "inline_node"),
        );

        new Promise((f) => setTimeout(f, 500)).then(() => controller.abort());
        try {
            // pulumi preview
            const previewRes = await stack.preview({
                signal: controller.signal,
            });
            assert.fail("expected canceled preview to throw");
        } catch (err) {
            assert.match(err.toString(), /stderr: Command was killed with SIGINT|error: preview canceled/);
            assert.match(err.toString(), /CommandError: code: -2/);
        }

        await stack.workspace.removeStack(stackName);
    });
});

const normalizeConfigKey = (key: string, projectName: string) => {
    const parts = key.split(":");
    if (parts.length < 2) {
        return `${projectName}:${key}`;
    }
    return "";
};

/**
 * Augments the provided {@link LocalWorkspaceOptions} so that they reference a
 * either a file backend or a cloud backend, depending on whether PULUMI_ACCESS_TOKEN
 * is set in the environment.
 */
function withTestBackend(
    opts?: LocalWorkspaceOptions,
    name?: string,
    description?: string,
    runtime?: string,
): LocalWorkspaceOptions {
    if (process.env.PULUMI_ACCESS_TOKEN) {
        return withCloudBackend(opts, name, description, runtime);
    }
    return withTemporaryFileBackend(opts, name, description, runtime);
}

function withCloudBackend(
    opts?: LocalWorkspaceOptions,
    name?: string,
    description?: string,
    runtime?: string,
): LocalWorkspaceOptions {
    const backend = {
        url: "https://api.pulumi.com",
    };
    if (name === undefined) {
        name = "node_test";
    }
    if (runtime === undefined) {
        runtime = "nodejs";
    }
    return {
        ...opts,
        projectSettings: {
            // We are obliged to provide a name and runtime if we provide project
            // settings, so we do so, but we spread in the provided project settings
            // afterwards so that the caller can override them if need be.
            name: name,
            runtime: runtime as ProjectRuntime,
            description: description,

            ...opts?.projectSettings,
            backend,
        },
    };
}

function withTemporaryFileBackend(
    opts?: LocalWorkspaceOptions,
    name?: string,
    description?: string,
    runtime?: string,
): LocalWorkspaceOptions {
    const tmpDir = tmp.dirSync({
        prefix: "nodejs-tests-automation-",
        unsafeCleanup: true,
    });

    const backend = { url: `file://${tmpDir.name}` };

    if (name === undefined) {
        name = "node_test";
    }
    if (runtime === undefined) {
        runtime = "nodejs";
    }

    return withTestConfigPassphrase({
        ...opts,
        pulumiHome: tmpDir.name,
        projectSettings: {
            // We are obliged to provide a name and runtime if we provide project
            // settings, so we do so, but we spread in the provided project settings
            // afterwards so that the caller can override them if need be.
            name: name,
            runtime: runtime as ProjectRuntime,
            description: description,

            ...opts?.projectSettings,
            backend,
        },
    });
}

/**
 * Augments the provided {@link LocalWorkspaceOptions} so that they set up an
 * environment containing a test `PULUMI_CONFIG_PASSPHRASE` variable suitable
 * for use with a local file backend.
 */
function withTestConfigPassphrase(opts?: LocalWorkspaceOptions): LocalWorkspaceOptions {
    return {
        ...opts,
        envVars: {
            ...opts?.envVars,
            PULUMI_CONFIG_PASSPHRASE: "test",
        },
    };
}