const {
    canRemoveAddress,
    removeAddressFromState,
    removeWalletFromState,
    broadcastActiveChanged,
} = require("../src/shared/walletDelete");

// Fixed addresses — never used for anything but these tests.
const A0 = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const A1 = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const A2 = "0x514910771AF9Ca656af840dff83E8264EcF986CA";
const B0 = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599";
const C0 = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

function wallet(name, addresses) {
    return {
        name,
        addresses: addresses.map((address) => ({ address })),
    };
}

// A three-wallet state; wallet A is an HD wallet with two addresses.
function makeState(overrides = {}) {
    return {
        hasWallet: true,
        wallets: [wallet("A", [A0, A1]), wallet("B", [B0]), wallet("C", [C0])],
        selectedWallet: 0,
        selectedAddress: 0,
        activeAddress: A0,
        allowedSites: {
            [A0]: ["a.example"],
            [A1]: ["b.example"],
            [B0]: ["c.example"],
        },
        deniedSites: { [A1]: ["d.example"], [C0]: ["e.example"] },
        ...overrides,
    };
}

describe("removeWalletFromState", () => {
    test("deleting the last wallet clears hasWallet", () => {
        const state = makeState({
            wallets: [wallet("A", [A0])],
            allowedSites: { [A0]: ["a.example"] },
            deniedSites: {},
        });

        const { activeAddressChanged } = removeWalletFromState(state, 0);

        expect(state.hasWallet).toBe(false);
        expect(state.wallets).toEqual([]);
        expect(state.selectedWallet).toBeNull();
        expect(state.selectedAddress).toBeNull();
        expect(state.activeAddress).toBeNull();
        expect(activeAddressChanged).toBe(true);
    });

    test("deleting a non-selected wallet leaves the selection intact", () => {
        const state = makeState({
            selectedWallet: 2,
            selectedAddress: 0,
            activeAddress: C0,
        });

        const { activeAddressChanged } = removeWalletFromState(state, 1);

        // Wallet C moved from index 2 to index 1 by the splice.
        expect(state.wallets.map((w) => w.name)).toEqual(["A", "C"]);
        expect(state.selectedWallet).toBe(1);
        expect(state.selectedAddress).toBe(0);
        expect(state.activeAddress).toBe(C0);
        expect(activeAddressChanged).toBe(false);
        expect(state.hasWallet).toBe(true);
    });

    test("deleting a wallet after the selection does not shift it", () => {
        const state = makeState({
            selectedWallet: 1,
            selectedAddress: 0,
            activeAddress: B0,
        });

        const { activeAddressChanged } = removeWalletFromState(state, 2);

        expect(state.selectedWallet).toBe(1);
        expect(state.activeAddress).toBe(B0);
        expect(activeAddressChanged).toBe(false);
    });

    test("deleting the active wallet falls back to the first remaining address", () => {
        const state = makeState({
            selectedWallet: 0,
            selectedAddress: 1,
            activeAddress: A1,
        });

        const { activeAddressChanged } = removeWalletFromState(state, 0);

        expect(state.wallets.map((w) => w.name)).toEqual(["B", "C"]);
        expect(state.selectedWallet).toBe(0);
        expect(state.selectedAddress).toBe(0);
        expect(state.activeAddress).toBe(B0);
        expect(activeAddressChanged).toBe(true);
        expect(state.hasWallet).toBe(true);
    });

    test("site permissions are dropped for every address of the wallet", () => {
        const state = makeState();

        removeWalletFromState(state, 0);

        expect(state.allowedSites).toEqual({ [B0]: ["c.example"] });
        expect(state.deniedSites).toEqual({ [C0]: ["e.example"] });
    });
});

// An HD wallet with three addresses next to a single-address key wallet.
// `nextIndex` is the wallet's derivation high-water mark, three addresses in.
function makeAddressState(overrides = {}) {
    return {
        hasWallet: true,
        wallets: [
            { ...wallet("A", [A0, A1, A2]), type: "hd", nextIndex: 3 },
            { ...wallet("B", [B0]), type: "key" },
        ],
        selectedWallet: 0,
        selectedAddress: 0,
        activeAddress: A0,
        allowedSites: { [A0]: ["a.example"], [A1]: ["b.example"] },
        deniedSites: { [A1]: ["d.example"], [B0]: ["e.example"] },
        ...overrides,
    };
}

describe("canRemoveAddress", () => {
    test("an HD wallet with more than one address may remove one", () => {
        expect(canRemoveAddress({ type: "hd", addresses: [{}, {}] })).toBe(
            true,
        );
    });

    test("an xprv wallet with more than one address may too", () => {
        expect(canRemoveAddress({ type: "xprv", addresses: [{}, {}] })).toBe(
            true,
        );
    });

    // The last address is what delete-wallet is for.
    test("a wallet holding a single address may not", () => {
        expect(canRemoveAddress({ type: "hd", addresses: [{}] })).toBe(false);
    });

    // A key wallet holds one bare private key and cannot derive more, so it
    // has no "+" button and gets no remove control either.
    test("a key wallet may not, whatever its address count", () => {
        expect(canRemoveAddress({ type: "key", addresses: [{}] })).toBe(false);
        expect(canRemoveAddress({ type: "key", addresses: [{}, {}] })).toBe(
            false,
        );
    });

    test("a missing or typeless wallet may not", () => {
        expect(canRemoveAddress(undefined)).toBe(false);
        expect(canRemoveAddress({})).toBe(false);
    });
});

describe("removeAddressFromState", () => {
    test("removing a non-selected address leaves the selection where it is", () => {
        const state = makeAddressState({
            selectedAddress: 2,
            activeAddress: A2,
        });

        const { removed, activeAddressChanged } = removeAddressFromState(
            state,
            0,
            0,
        );

        expect(removed).toBe(true);
        // A2 moved from index 2 to index 1 by the splice.
        expect(state.wallets[0].addresses.map((a) => a.address)).toEqual([
            A1,
            A2,
        ]);
        expect(state.selectedWallet).toBe(0);
        expect(state.selectedAddress).toBe(1);
        expect(state.activeAddress).toBe(A2);
        expect(activeAddressChanged).toBe(false);
        // The wallet list itself is untouched.
        expect(state.wallets).toHaveLength(2);
        expect(state.hasWallet).toBe(true);
    });

    test("removing an address after the selection does not shift it", () => {
        const state = makeAddressState({
            selectedAddress: 0,
            activeAddress: A0,
        });

        const { removed, activeAddressChanged } = removeAddressFromState(
            state,
            0,
            2,
        );

        expect(removed).toBe(true);
        expect(state.selectedAddress).toBe(0);
        expect(state.activeAddress).toBe(A0);
        expect(activeAddressChanged).toBe(false);
    });

    test("a selection in another wallet is untouched", () => {
        const state = makeAddressState({
            selectedWallet: 1,
            selectedAddress: 0,
            activeAddress: B0,
        });

        const { removed, activeAddressChanged } = removeAddressFromState(
            state,
            0,
            1,
        );

        expect(removed).toBe(true);
        expect(state.selectedWallet).toBe(1);
        expect(state.selectedAddress).toBe(0);
        expect(state.activeAddress).toBe(B0);
        expect(activeAddressChanged).toBe(false);
    });

    test("removing the selected address falls back to the wallet's first address", () => {
        const state = makeAddressState({
            selectedAddress: 1,
            activeAddress: A1,
        });

        const { removed, activeAddressChanged } = removeAddressFromState(
            state,
            0,
            1,
        );

        expect(removed).toBe(true);
        expect(state.wallets[0].addresses.map((a) => a.address)).toEqual([
            A0,
            A2,
        ]);
        expect(state.selectedWallet).toBe(0);
        expect(state.selectedAddress).toBe(0);
        expect(state.activeAddress).toBe(A0);
        expect(activeAddressChanged).toBe(true);
    });

    // The active address can be persisted in a different case than the
    // wallet's copy of it, so the comparison must not be literal.
    test("the active address is matched case-insensitively", () => {
        const state = makeAddressState({
            selectedAddress: 1,
            activeAddress: A1.toLowerCase(),
        });

        const { activeAddressChanged } = removeAddressFromState(state, 0, 1);

        expect(state.activeAddress).toBe(A0);
        expect(activeAddressChanged).toBe(true);
    });

    test("site permissions are dropped for the removed address only", () => {
        const state = makeAddressState();

        removeAddressFromState(state, 0, 1);

        expect(state.allowedSites).toEqual({ [A0]: ["a.example"] });
        expect(state.deniedSites).toEqual({ [B0]: ["e.example"] });
    });

    // The derivation counter is a high-water mark, never rewound: "+" derives
    // a fresh index rather than re-deriving the address just removed.
    test("the wallet's derivation counter is not rewound", () => {
        const state = makeAddressState();

        removeAddressFromState(state, 0, 1);

        expect(state.wallets[0].nextIndex).toBe(3);
    });

    test("the last address of a wallet is refused, and nothing changes", () => {
        const state = makeAddressState({
            selectedWallet: 1,
            selectedAddress: 0,
            activeAddress: B0,
        });

        const { removed, activeAddressChanged } = removeAddressFromState(
            state,
            1,
            0,
        );

        expect(removed).toBe(false);
        expect(activeAddressChanged).toBe(false);
        expect(state.wallets[1].addresses.map((a) => a.address)).toEqual([B0]);
        expect(state.activeAddress).toBe(B0);
        expect(state.hasWallet).toBe(true);
    });

    // The same refusal reached the other way: an HD wallet worn down to one
    // address is no more removable than a key wallet.
    test("an HD wallet down to its last address is refused too", () => {
        const state = makeAddressState();

        expect(removeAddressFromState(state, 0, 2).removed).toBe(true);
        expect(removeAddressFromState(state, 0, 1).removed).toBe(true);
        expect(removeAddressFromState(state, 0, 0).removed).toBe(false);
        expect(state.wallets[0].addresses.map((a) => a.address)).toEqual([A0]);
    });

    test("an out-of-range address index is refused", () => {
        const state = makeAddressState();

        expect(removeAddressFromState(state, 0, 7).removed).toBe(false);
        expect(removeAddressFromState(state, 7, 0).removed).toBe(false);
        expect(state.wallets[0].addresses).toHaveLength(3);
    });
});

describe("broadcastActiveChanged", () => {
    afterEach(() => {
        delete global.chrome;
    });

    test("sends AUTISTMASK_ACTIVE_CHANGED to the background", () => {
        const sendMessage = jest.fn();
        global.chrome = { runtime: { sendMessage } };

        broadcastActiveChanged();

        expect(sendMessage).toHaveBeenCalledWith({
            type: "AUTISTMASK_ACTIVE_CHANGED",
        });
    });
});
