const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";

// Address RECORDS, not bare address strings. A stored profile is validated
// against the schema on every read now (src/shared/stateSchema.js), and a bare
// string where an address record belongs is one of the shapes that refuses to
// load — as it should, since every screen dereferences addr.address.
function oneWallet() {
    return [
        {
            name: "Wallet 1",
            type: "hd",
            addresses: [{ address: ADDRESS, balance: "0", tokenBalances: [] }],
        },
    ];
}

const { makeStorageStub } = require("./support/storageStub");

// state.js resolves the storage API at require time, so the stub has to exist
// before the module is loaded, and the module registry has to be reset between
// cases because `state` is a module-level singleton.
//
// The stub clones in both directions, as the real chrome.storage.local does —
// see tests/support/storageStub.js for why an aliasing one made this file
// assert less than it appears to.
function loadModuleWith(persisted) {
    jest.resetModules();
    const storage = makeStorageStub(persisted ? { autistmask: persisted } : {});
    global.chrome = { storage };
    return {
        mod: require("../src/shared/state"),
        set: storage.set,
        stored: () => storage.read("autistmask"),
    };
}

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

describe("loadState hasWallet reconciliation", () => {
    // A profile that deleted its last wallet on a build predating the write
    // path fix keeps hasWallet: true forever. It must load as no wallet, which
    // is what sends the popup to the welcome view.
    test("stored hasWallet true with zero wallets loads as no wallet", async () => {
        const { mod } = loadModuleWith({ hasWallet: true, wallets: [] });
        await mod.loadState();
        expect(mod.state.hasWallet).toBe(false);
    });

    test("stored hasWallet true with a missing wallets key loads as no wallet", async () => {
        const { mod } = loadModuleWith({ hasWallet: true });
        await mod.loadState();
        expect(mod.state.wallets).toEqual([]);
        expect(mod.state.hasWallet).toBe(false);
    });

    test("stored hasWallet false with one wallet loads as having a wallet", async () => {
        const { mod } = loadModuleWith({
            hasWallet: false,
            wallets: oneWallet(),
        });
        await mod.loadState();
        expect(mod.state.hasWallet).toBe(true);
    });

    test("absent hasWallet with wallets present loads as having a wallet", async () => {
        const { mod } = loadModuleWith({ wallets: oneWallet() });
        await mod.loadState();
        expect(mod.state.hasWallet).toBe(true);
    });

    test("consistent stored states are preserved", async () => {
        const withWallet = loadModuleWith({
            hasWallet: true,
            wallets: oneWallet(),
        });
        await withWallet.mod.loadState();
        expect(withWallet.mod.state.hasWallet).toBe(true);

        const without = loadModuleWith({ hasWallet: false, wallets: [] });
        await without.mod.loadState();
        expect(without.mod.state.hasWallet).toBe(false);
    });

    test("empty storage leaves the default no-wallet state", async () => {
        const { mod } = loadModuleWith(null);
        await mod.loadState();
        expect(mod.state.hasWallet).toBe(false);
        expect(mod.state.wallets).toEqual([]);
    });

    // The correction is derived on every load rather than written back, so a
    // load never has a storage side effect.
    test("loadState does not write to storage", async () => {
        const { mod, set } = loadModuleWith({ hasWallet: true, wallets: [] });
        await mod.loadState();
        expect(set).not.toHaveBeenCalled();
    });

    // Deriving must not disturb the rest of the load.
    test("other persisted fields still load", async () => {
        const { mod } = loadModuleWith({
            hasWallet: false,
            wallets: oneWallet(),
            networkId: "sepolia",
            theme: "dark",
            activeAddress: ADDRESS,
        });
        await mod.loadState();
        expect(mod.state.networkId).toBe("sepolia");
        expect(mod.state.theme).toBe("dark");
        expect(mod.state.activeAddress).toBe(ADDRESS);
    });
});

// The known-symbol spoof filter is a safety filter, so an existing profile
// stored before the setting existed must load with it on rather than with
// undefined, which would read as off.
describe("hideSpoofedSymbols persistence", () => {
    test("defaults to on with empty storage", async () => {
        const { mod } = loadModuleWith(null);
        await mod.loadState();
        expect(mod.state.hideSpoofedSymbols).toBe(true);
    });

    test("a profile stored without the key loads with it on", async () => {
        const { mod } = loadModuleWith({ wallets: oneWallet() });
        await mod.loadState();
        expect(mod.state.hideSpoofedSymbols).toBe(true);
    });

    test("an explicit false survives the load", async () => {
        const { mod } = loadModuleWith({
            wallets: oneWallet(),
            hideSpoofedSymbols: false,
        });
        await mod.loadState();
        expect(mod.state.hideSpoofedSymbols).toBe(false);
    });

    test("saveState persists the flag", async () => {
        const { mod, set } = loadModuleWith(null);
        mod.state.hideSpoofedSymbols = false;
        await mod.saveState();
        expect(set).toHaveBeenCalledWith({
            autistmask: expect.objectContaining({ hideSpoofedSymbols: false }),
        });
    });

    test("the flag round-trips off through save and load", async () => {
        const first = loadModuleWith(null);
        first.mod.state.hideSpoofedSymbols = false;
        await first.mod.saveState();
        const persisted = first.set.mock.calls[0][0].autistmask;

        const second = loadModuleWith(persisted);
        await second.mod.loadState();
        expect(second.mod.state.hideSpoofedSymbols).toBe(false);
    });

    test("the flag round-trips back on through save and load", async () => {
        const first = loadModuleWith(null);
        first.mod.state.hideSpoofedSymbols = true;
        await first.mod.saveState();
        const persisted = first.set.mock.calls[0][0].autistmask;

        const second = loadModuleWith(persisted);
        await second.mod.loadState();
        expect(second.mod.state.hideSpoofedSymbols).toBe(true);
    });
});

// restoreView() refuses to reopen ONTO a non-restorable view, but the stack
// behind it was restored verbatim, so Back could still walk onto a screen
// whose content is deliberately never re-rendered — and "show-phrase" has no
// Back control of its own to leave by. The stack is filtered on load, at the
// first entry the popup would not render, and everything above it goes too:
// those entries were reached THROUGH the dropped one.
describe("restored viewStack is filtered against RESTORABLE_VIEWS", () => {
    const NON_RESTORABLE = ["export-privkey", "show-phrase"];

    function restoredStack(viewStack, currentView = "settings") {
        return loadModuleWith({
            wallets: oneWallet(),
            currentView,
            viewStack,
        });
    }

    test("a non-restorable view at the top of the stack is dropped", async () => {
        const { mod } = restoredStack(["main", "address", "export-privkey"]);
        await mod.loadState();
        expect(mod.state.viewStack).toEqual(["main", "address"]);
    });

    test("a non-restorable view in the middle truncates the stack there", async () => {
        const { mod } = restoredStack(["main", "show-phrase", "address"]);
        await mod.loadState();
        expect(mod.state.viewStack).toEqual(["main"]);
    });

    // Truncating a stack rooted at a non-restorable view leaves nothing, and
    // the restored view still needs somewhere for Back to go.
    test("a non-restorable view at the bottom leaves main to go back to", async () => {
        const { mod } = restoredStack(["export-privkey", "address", "receive"]);
        await mod.loadState();
        expect(mod.state.viewStack).toEqual(["main"]);
    });

    test("no restored stack retains a secret-bearing view", async () => {
        for (const view of NON_RESTORABLE) {
            const { mod } = restoredStack(["main", "address", view, "receive"]);
            await mod.loadState();
            expect(mod.state.viewStack).not.toContain(view);
        }
    });

    // The rule is "views the popup will render", not a blocklist of the two
    // secret screens: a name no longer in the set (or never a view at all)
    // has to go the same way.
    test("a name that is not a restorable view at all is dropped", async () => {
        const { mod } = restoredStack(["main", "welcome", "address"]);
        await mod.loadState();
        expect(mod.state.viewStack).toEqual(["main"]);
    });

    // Restorable entries are kept verbatim. That they are then unhidden
    // without being re-rendered is a separate defect, tracked in #268; this
    // filter is only about views the popup declined to restore.
    test("an ordinary restorable stack is restored unchanged", async () => {
        const stack = ["main", "address", "address-token"];
        const { mod } = restoredStack(stack);
        await mod.loadState();
        expect(mod.state.viewStack).toEqual(stack);
    });

    test("restoring onto main keeps the stack empty", async () => {
        const { mod } = restoredStack(["show-phrase"], "main");
        await mod.loadState();
        expect(mod.state.viewStack).toEqual([]);
    });

    // main is not the only view that gets no ["main"] beneath it: restoreView()
    // will not reopen onto a non-restorable view either, so nothing is left for
    // Back to sit under and the stack stays empty.
    test("restoring onto a view the popup will not reopen keeps the stack empty", async () => {
        const { mod } = restoredStack(["export-privkey"], "show-phrase");
        await mod.loadState();
        expect(mod.state.viewStack).toEqual([]);
    });

    // Not an array means nothing survives, but the never-empty rule still
    // applies: a corrupt stack must not leave a restored view with no Back
    // target of its own.
    test("a stack that is not an array still gets main beneath a restored view", async () => {
        const { mod } = restoredStack("main");
        await mod.loadState();
        expect(mod.state.viewStack).toEqual(["main"]);
    });

    test("a stack that is not an array loads as empty under main", async () => {
        const { mod } = restoredStack({ 0: "main" }, "main");
        await mod.loadState();
        expect(mod.state.viewStack).toEqual([]);
    });

    // Filtering belongs on load, not on save: the live in-session stack is
    // legitimate — the user really is one Back away from a screen that is
    // rendered right now — and only a load-side filter also cleans the
    // stacks already sitting in storage.
    test("saveState persists the live stack verbatim", async () => {
        const { mod, set } = loadModuleWith(null);
        mod.state.viewStack = ["main", "address", "export-privkey"];
        await mod.saveState();
        expect(set).toHaveBeenCalledWith({
            autistmask: expect.objectContaining({
                viewStack: ["main", "address", "export-privkey"],
            }),
        });
    });
});
