/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { mocked } from "jest-mock";
import { act } from "react";
import { Crypto } from "@peculiar/webcrypto";
import { type CryptoApi, deriveRecoveryKeyFromPassphrase } from "matrix-js-sdk/src/crypto-api";
import { SecretStorage } from "matrix-js-sdk/src/matrix";

import { accessSecretStorage, crossSigningCallbacks } from "../../src/SecurityManager";
import { filterConsole, stubClient } from "../test-utils";
import Modal from "../../src/Modal.tsx";
import {
    default as AccessSecretStorageDialog,
    type KeyParams,
} from "../../src/components/views/dialogs/security/AccessSecretStorageDialog.tsx";

jest.mock("react", () => {
    const React = jest.requireActual("react");
    React.lazy = (children: any) => children(); // stub out lazy for dialog test
    return React;
});

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

describe("SecurityManager", () => {
    describe("accessSecretStorage", () => {
        filterConsole("Not setting dehydration key: no SSSS key found");

        it("runs the function passed in", async () => {
            // Given a client
            const crypto = {
                bootstrapCrossSigning: () => {},
                bootstrapSecretStorage: () => {},
            } as unknown as CryptoApi;
            const client = stubClient();
            client.secretStorage.hasKey = jest.fn().mockResolvedValue(true);
            mocked(client.getCrypto).mockReturnValue(crypto);

            // When I run accessSecretStorage
            const func = jest.fn();
            await accessSecretStorage(func);

            // Then we call the passed-in function
            expect(func).toHaveBeenCalledTimes(1);
        });

        describe("expecting errors", () => {
            filterConsole("End-to-end encryption is disabled - unable to access secret storage");

            it("throws if crypto is unavailable", async () => {
                // Given a client with no crypto
                const client = stubClient();
                client.secretStorage.hasKey = jest.fn().mockResolvedValue(true);
                mocked(client.getCrypto).mockReturnValue(undefined);

                // When I run accessSecretStorage
                // Then we throw an error
                await expect(async () => {
                    await accessSecretStorage(jest.fn());
                }).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage");
            });

            it("throws if there is no 4S", async () => {
                // Given a client with no default 4S key ID
                stubClient();

                // When I run accessSecretStorage
                // Then we throw an error
                await expect(async () => {
                    await accessSecretStorage(jest.fn());
                }).rejects.toThrow("Secret storage has not been created yet");
            });
        });

        it("should show CreateSecretStorageDialog if forceReset=true", async () => {
            jest.mock("../../src/async-components/views/dialogs/security/CreateSecretStorageDialog", () => ({
                __test: true,
                __esModule: true,
                default: () => jest.fn(),
            }));
            const spy = jest.spyOn(Modal, "createDialog");
            stubClient();

            const func = jest.fn();
            accessSecretStorage(func, { forceReset: true });

            expect(spy).toHaveBeenCalledTimes(1);
            await expect(spy.mock.lastCall![0]).resolves.toEqual(expect.objectContaining({ __test: true }));
        });
    });

    describe("getSecretStorageKey", () => {
        const { getSecretStorageKey } = crossSigningCallbacks;

        /** Polyfill crypto.subtle, which is unavailable in jsdom */
        function polyFillSubtleCrypto() {
            Object.defineProperty(globalThis.crypto, "subtle", { value: new Crypto().subtle });
        }

        it("should prompt the user if the key is uncached", async () => {
            polyFillSubtleCrypto();

            const client = stubClient();
            mocked(client.secretStorage.getDefaultKeyId).mockResolvedValue("my_default_key");

            const passphrase = "s3cret";
            const { recoveryKey, keyInfo } = await deriveKeyFromPassphrase(passphrase);

            jest.spyOn(Modal, "createDialog").mockImplementation((component) => {
                expect(component).toBe(AccessSecretStorageDialog);

                const modalFunc = async () => [{ passphrase }] as [KeyParams];
                return {
                    finished: modalFunc(),
                    close: () => {},
                };
            });

            const [keyId, key] = (await act(() =>
                getSecretStorageKey!({ keys: { my_default_key: keyInfo } }, "my_secret"),
            ))!;
            expect(keyId).toEqual("my_default_key");
            expect(key).toEqual(recoveryKey);
        });

        it("should not prompt the user if the requested key is not the default", async () => {
            const client = stubClient();
            mocked(client.secretStorage.getDefaultKeyId).mockResolvedValue("my_default_key");
            const createDialogSpy = jest.spyOn(Modal, "createDialog");

            await expect(
                act(() =>
                    getSecretStorageKey!(
                        { keys: { other_key: {} as SecretStorage.SecretStorageKeyDescription } },
                        "my_secret",
                    ),
                ),
            ).rejects.toThrow("Request for non-default 4S key");
            expect(createDialogSpy).not.toHaveBeenCalled();
        });
    });
});

/** Derive a key from a passphrase, also returning the KeyInfo */
async function deriveKeyFromPassphrase(
    passphrase: string,
): Promise<{ recoveryKey: Uint8Array; keyInfo: SecretStorage.SecretStorageKeyDescription }> {
    const salt = "SALTYGOODNESS";
    const iterations = 1000;

    const recoveryKey = await deriveRecoveryKeyFromPassphrase(passphrase, salt, iterations);

    const check = await SecretStorage.calculateKeyCheck(recoveryKey);
    return {
        recoveryKey,
        keyInfo: {
            iv: check.iv,
            mac: check.mac,
            algorithm: SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES,
            name: "",
            passphrase: {
                algorithm: "m.pbkdf2",
                iterations,
                salt,
            },
        },
    };
}
