// Tests for the stored-state half of the non-master extended key problem.
//
// Refusing a non-master xprv at import time does nothing for a wallet that is
// already in storage: the import that created it ran before the refusal
// existed. Such a wallet used to sign for the wrong tree and now throws on the
// send screen instead. These tests pin down that it is named and explained in
// the wallet list, that nothing on the way there throws, and that a wallet
// imported from a real master key is untouched by any of it.

const { HDNodeWallet, Mnemonic } = require("ethers");

const wallet = require("../src/shared/wallet");
const {
    walletDefect,
    walletDefectHtml,
    NON_MASTER_XPRV,
} = require("../src/shared/walletDefects");

// BIP-39 test vector phrase, published; never used for real funds.
const VECTOR_PHRASE =
    "test test test test test test test test test test test junk";

function seedNode(phrase) {
    return HDNodeWallet.fromSeed(Mnemonic.fromPhrase(phrase, "").computeSeed());
}

// The master (depth-0) key, which is what the import flow accepts today.
function masterXprv(phrase) {
    return seedNode(phrase).extendedKey;
}

// The account-level (depth-3) key m/44'/60'/0'. A normal thing for a user to
// hold, and what the import flow used to accept.
function accountXprv(phrase) {
    return seedNode(phrase).derivePath("m/44'/60'/0'").extendedKey;
}

// The wallet record the CURRENT import path writes for a master key: the
// neutered m/44'/60'/0'/0 node, four levels below a depth-0 key.
function healthyXprvWallet(name = "Master") {
    const { xpub, firstAddress } = wallet.hdWalletFromXprv(
        masterXprv(VECTOR_PHRASE),
    );
    return {
        name,
        type: "xprv",
        xpub,
        nextIndex: 1,
        encryptedSecret: "irrelevant-to-these-tests",
        addresses: [{ address: firstAddress, balance: "0.0000" }],
    };
}

// The wallet record the PRE-#210 import path wrote for an account-level key:
// the same four levels, but derived as a relative path *beneath* the key, so
// the stored xpub sits at depth 3 + 4 = 7. Built here the way the old code
// built it rather than by calling the module under test, which now refuses.
function brokenXprvWallet(name = "Imported xprv") {
    const node = HDNodeWallet.fromExtendedKey(
        accountXprv(VECTOR_PHRASE),
    ).derivePath("44'/60'/0'/0");
    return {
        name,
        type: "xprv",
        xpub: node.neuter().extendedKey,
        nextIndex: 1,
        encryptedSecret: "irrelevant-to-these-tests",
        addresses: [
            { address: node.deriveChild(0).address, balance: "0.0000" },
        ],
    };
}

describe("the fixtures are what the two import paths actually produced", () => {
    test("a master import stores a depth-4 xpub", () => {
        expect(
            HDNodeWallet.fromExtendedKey(healthyXprvWallet().xpub).depth,
        ).toBe(4);
    });

    test("the pre-fix account-level import stored a depth-7 xpub", () => {
        expect(
            HDNodeWallet.fromExtendedKey(brokenXprvWallet().xpub).depth,
        ).toBe(7);
    });
});

describe("walletDefect", () => {
    test("names the defect on a stored non-master xprv wallet", () => {
        const defect = walletDefect(brokenXprvWallet());

        expect(defect).not.toBeNull();
        expect(defect.id).toBe(NON_MASTER_XPRV);
    });

    test("a depth-0 xprv wallet has no defect", () => {
        expect(walletDefect(healthyXprvWallet())).toBeNull();
    });

    test("hd and key wallets are never assessed", () => {
        expect(
            walletDefect({ type: "hd", xpub: brokenXprvWallet().xpub }),
        ).toBe(null);
        expect(walletDefect({ type: "key" })).toBeNull();
    });

    test("an xprv wallet whose xpub cannot be parsed makes no claim", () => {
        // No basis to say the key was non-master, so nothing is asserted
        // about it rather than guessing.
        expect(walletDefect({ type: "xprv", xpub: "not-a-key" })).toBeNull();
        expect(walletDefect({ type: "xprv" })).toBeNull();
    });

    test("nothing about the wallet record is modified by the check", () => {
        const w = brokenXprvWallet();
        const before = JSON.stringify(w);

        walletDefect(w);

        expect(JSON.stringify(w)).toBe(before);
    });
});

describe("the explanatory copy", () => {
    const defect = walletDefect(brokenXprvWallet());

    test("every sentence of it is a full sentence", () => {
        for (const text of [defect.heading, ...defect.paragraphs]) {
            expect(text).toMatch(/^[A-Z]/);
            expect(text.trimEnd()).toMatch(/\.$/);
        }
    });

    test("it says what was derived wrongly and that these are not the standard addresses", () => {
        const body = defect.paragraphs.join(" ");

        expect(body).toContain("not a master key");
        expect(body).toMatch(/standard path/);
    });

    test("it does not claim the funds are safe and does not claim a loss", () => {
        const all = [defect.heading, ...defect.paragraphs].join(" ");

        expect(all).not.toMatch(/\bsafe\b/i);
        expect(all).not.toMatch(/\blost\b|\bstolen\b|\bgone\b/i);
    });

    test("it says the wallet is not deleted and what the user can do", () => {
        const body = defect.paragraphs.join(" ");

        expect(body).toMatch(/until you delete it yourself/);
        expect(body).toMatch(/recovery phrase/);
    });

    test("it uses the project's vocabulary", () => {
        const all = [
            defect.heading,
            ...defect.paragraphs,
            defect.shortMessage,
        ].join(" ");

        expect(all).not.toMatch(/seed phrase|mnemonic|passphrase/i);
        expect(all).not.toMatch(/\baccounts?\b/i);
    });
});

describe("walletDefectHtml", () => {
    test("renders the heading and every paragraph for a defective wallet", () => {
        const defect = walletDefect(brokenXprvWallet());
        const html = walletDefectHtml(brokenXprvWallet());

        expect(html).toContain(defect.heading);
        for (const p of defect.paragraphs) {
            expect(html).toContain(p);
        }
    });

    test("renders nothing at all for a healthy wallet", () => {
        expect(walletDefectHtml(healthyXprvWallet())).toBe("");
    });
});

describe("the wallet list", () => {
    let home;
    let state;

    beforeAll(() => {
        global.chrome = {
            storage: { local: { get: async () => ({}), set: async () => {} } },
            runtime: { sendMessage: () => {} },
        };
        home = require("../src/popup/views/home");
        state = require("../src/shared/state").state;
    });

    afterEach(() => {
        state.wallets = [];
        state.activeAddress = null;
    });

    test("a stored depth-3 xprv wallet renders the explanation", () => {
        state.wallets = [brokenXprvWallet("Imported xprv")];

        const html = home.walletListHtml();

        expect(html).toContain(walletDefect(state.wallets[0]).heading);
        expect(html).toContain("Imported xprv");
    });

    test("it does not offer to derive further addresses from that wallet", () => {
        state.wallets = [brokenXprvWallet()];

        expect(home.walletListHtml()).not.toContain("btn-add-address");
    });

    test("a normal depth-0 xprv wallet renders exactly as it did before", () => {
        state.wallets = [healthyXprvWallet("Master")];

        const html = home.walletListHtml();

        expect(html).not.toContain(walletDefect(brokenXprvWallet()).heading);
        expect(html).toContain("btn-add-address");
        expect(html).toContain(state.wallets[0].addresses[0].address);
    });

    test("the defective wallet's notice does not bleed onto a healthy one", () => {
        state.wallets = [brokenXprvWallet("Broken"), healthyXprvWallet("Fine")];

        const html = home.walletListHtml();
        const healthyPart = html.slice(html.indexOf("Fine"));

        expect(html).toContain(walletDefect(state.wallets[0]).heading);
        expect(healthyPart).not.toContain(
            walletDefect(state.wallets[0]).heading,
        );
        expect(healthyPart).toContain("btn-add-address");
    });
});

describe("no path throws an unhandled error for a defective wallet", () => {
    test("address derivation from the stored xpub still works", () => {
        // The stored xpub is at a non-standard depth but is a valid extended
        // key; deriving from it is what the list render already does.
        const w = brokenXprvWallet();

        expect(() => wallet.deriveAddressFromXpub(w.xpub, 0)).not.toThrow();
        expect(wallet.deriveAddressFromXpub(w.xpub, 0)).toBe(
            w.addresses[0].address,
        );
    });

    test("the wallet list renders without throwing", () => {
        const { state } = require("../src/shared/state");
        const home = require("../src/popup/views/home");
        state.wallets = [brokenXprvWallet()];

        expect(() => home.walletListHtml()).not.toThrow();

        state.wallets = [];
    });

    test("signing refuses with the named defect rather than a bare failure", () => {
        // getSignerForAddress is the backstop behind the UI gate. It must
        // still refuse, and it must say why in a sentence the user can read.
        let thrown = null;
        try {
            wallet.getSignerForAddress(
                { type: "xprv" },
                0,
                accountXprv(VECTOR_PHRASE),
            );
        } catch (e) {
            thrown = e;
        }

        expect(thrown).not.toBeNull();
        expect(thrown.message).toMatch(/master key/);
        expect(thrown.message.trimEnd()).toMatch(/\.$/);
    });

    test("a healthy xprv wallet signs as it always did", () => {
        const signer = wallet.getSignerForAddress(
            { type: "xprv" },
            0,
            masterXprv(VECTOR_PHRASE),
        );

        expect(signer.address).toBe(healthyXprvWallet().addresses[0].address);
    });
});
