// What a chain switch is allowed to do to the endpoints the user configured.
//
// A switch used to overwrite state.rpcUrl and state.blockscoutUrl with the
// network defaults, so a user pointing the wallet at their own node lost that
// url the first time anything switched chains — with no notification and no
// way to recover it, having been moved onto a public endpoint that then sees
// every address they hold (https://git.eeqj.de/sneak/AutistMask/issues/308).
// Endpoints are now remembered per network, which is why the round trips
// below assert the ORIGINAL url comes back rather than only that the switch
// happened.

const { networkById } = require("../src/shared/networks");
const { makeStorageStub } = require("./support/storageStub");

const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";

const MAINNET = networkById("mainnet");
const SEPOLIA = networkById("sepolia");

// The user's own node: the pair the switch used to throw away.
const CUSTOM_RPC = "http://127.0.0.1:8545";
const CUSTOM_BLOCKSCOUT = "http://127.0.0.1:4000/api/v2";

function walletFixture() {
    return [
        {
            name: "Wallet 1",
            type: "hd",
            addresses: [{ address: ADDRESS, balance: "0", tokenBalances: [] }],
        },
    ];
}

// The real state module against stubbed storage, plus whatever the last
// saveState() wrote — so a case can reload a fresh module from the bytes an
// earlier one persisted, which is what an extension restart does. `state` is
// a module-level singleton, so the registry has to be reset per load.
// The stub clones in both directions, as the real chrome.storage.local does.
// It used to alias, and written() then handed the NEXT module load the live
// in-memory object of the previous one as its "persisted bytes" — an extension
// restart that never crossed a serialization boundary. See
// tests/support/storageStub.js.
function loadModuleWith(persisted) {
    jest.resetModules();
    const storage = makeStorageStub(persisted ? { autistmask: persisted } : {});
    global.chrome = { storage };
    return {
        mod: require("../src/shared/state"),
        chainSwitch: require("../src/shared/chainSwitch"),
        written: () => storage.read("autistmask"),
    };
}

afterEach(() => {
    delete global.chrome;
});

describe("a custom endpoint survives a chain switch", () => {
    test("switching away and back restores the user's rpc and blockscout urls", async () => {
        const { mod, chainSwitch } = loadModuleWith({
            wallets: walletFixture(),
            networkId: "mainnet",
            rpcUrl: CUSTOM_RPC,
            blockscoutUrl: CUSTOM_BLOCKSCOUT,
            networkEndpoints: {
                mainnet: {
                    rpcUrl: CUSTOM_RPC,
                    blockscoutUrl: CUSTOM_BLOCKSCOUT,
                },
            },
        });
        await mod.loadState();

        await chainSwitch.onChainSwitch("sepolia");
        // The new chain gets its own endpoints, not the ones belonging to the
        // chain just left: a mainnet node cannot answer for Sepolia.
        expect(mod.state.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
        expect(mod.state.blockscoutUrl).toBe(SEPOLIA.defaultBlockscoutUrl);

        await chainSwitch.onChainSwitch("mainnet");
        expect(mod.state.rpcUrl).toBe(CUSTOM_RPC);
        expect(mod.state.blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
    });

    test("an endpoint set on the network being left is remembered, not lost", async () => {
        const { mod, chainSwitch } = loadModuleWith({
            wallets: walletFixture(),
            networkId: "sepolia",
            rpcUrl: SEPOLIA.defaultRpcUrl,
            blockscoutUrl: SEPOLIA.defaultBlockscoutUrl,
            networkEndpoints: {},
        });
        await mod.loadState();

        // What the Settings screen does: write the live field, then save. The
        // map entry for the active network is stale until the switch, which
        // is what snapshotting the outgoing network exists to reconcile.
        mod.state.rpcUrl = CUSTOM_RPC;
        await mod.saveState();

        await chainSwitch.onChainSwitch("mainnet");
        expect(mod.state.rpcUrl).toBe(MAINNET.defaultRpcUrl);

        await chainSwitch.onChainSwitch("sepolia");
        expect(mod.state.rpcUrl).toBe(CUSTOM_RPC);
    });

    test("the remembered endpoints survive an extension restart", async () => {
        const first = loadModuleWith({
            wallets: walletFixture(),
            networkId: "mainnet",
            rpcUrl: CUSTOM_RPC,
            blockscoutUrl: CUSTOM_BLOCKSCOUT,
        });
        await first.mod.loadState();
        await first.chainSwitch.onChainSwitch("sepolia");

        // Reload from exactly the bytes the switch persisted.
        const second = loadModuleWith(first.written());
        await second.mod.loadState();
        expect(second.mod.state.networkId).toBe("sepolia");
        expect(second.mod.state.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);

        await second.chainSwitch.onChainSwitch("mainnet");
        expect(second.mod.state.rpcUrl).toBe(CUSTOM_RPC);
        expect(second.mod.state.blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
    });

    test("a profile written before networkEndpoints existed keeps its endpoint", async () => {
        // Exactly the stored shape the current release writes: one pair of
        // urls and no map. It is adopted as the remembered pair of the
        // network it was stored under.
        const { mod, chainSwitch } = loadModuleWith({
            wallets: walletFixture(),
            networkId: "mainnet",
            rpcUrl: CUSTOM_RPC,
            blockscoutUrl: CUSTOM_BLOCKSCOUT,
        });
        await mod.loadState();

        expect(mod.state.rpcUrl).toBe(CUSTOM_RPC);
        expect(mod.state.networkEndpoints).toEqual({
            mainnet: { rpcUrl: CUSTOM_RPC, blockscoutUrl: CUSTOM_BLOCKSCOUT },
        });

        await chainSwitch.onChainSwitch("sepolia");
        await chainSwitch.onChainSwitch("mainnet");
        expect(mod.state.rpcUrl).toBe(CUSTOM_RPC);
        expect(mod.state.blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
    });

    // A primitive is the dangerous case, not the array: assigning a property
    // to a string throws nothing and stores nothing, so a stored string would
    // be carried through loadState() and re-persisted by every save, and each
    // switch would fall back to the public default in place of the user's
    // endpoint, permanently.
    test.each([
        ["an array", ["not", "a", "map"]],
        ["a string", "junk"],
        ["a number", 7],
    ])("a stored networkEndpoints that is %s is discarded", async (_, bad) => {
        const { mod, chainSwitch } = loadModuleWith({
            wallets: walletFixture(),
            networkId: "mainnet",
            rpcUrl: CUSTOM_RPC,
            blockscoutUrl: CUSTOM_BLOCKSCOUT,
            networkEndpoints: bad,
        });
        await mod.loadState();

        // Discarded, then seeded from the live endpoints the same way an old
        // profile is — never left as something onChainSwitch() would index.
        expect(mod.state.networkEndpoints).toEqual({
            mainnet: {
                rpcUrl: CUSTOM_RPC,
                blockscoutUrl: CUSTOM_BLOCKSCOUT,
            },
        });

        // And the endpoint really survives the round trip, which is the point
        // of discarding it rather than only of the shape being right.
        await chainSwitch.onChainSwitch("sepolia");
        await chainSwitch.onChainSwitch("mainnet");
        expect(mod.state.rpcUrl).toBe(CUSTOM_RPC);
        expect(mod.state.blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
    });
});
