// Back after reopening the popup (#268).
//
// A reopened popup renders the wallet list and the one view it restores
// onto; every other view is still the blank static template from
// index.html. goBack() used to only unhide its target, so Back landed on
// that blank template for any view the popup had not rendered in this page
// load. These tests drive the real goBack() with the real router wired to
// recording view modules, so what is asserted is which view render ran —
// the thing that was missing.
//
// The rendering itself is asserted against the real popup in a real
// browser by tests/e2e/run.js; here the DOM is a stub, because goBack()
// only needs showView() to work.

const els = new Map();

function fakeEl() {
    return {
        textContent: "",
        innerHTML: "",
        classList: {
            toggle() {},
            add() {},
            remove() {},
            contains: () => false,
        },
        remove() {},
    };
}

globalThis.document = {
    getElementById(id) {
        if (!els.has(id)) els.set(id, fakeEl());
        return els.get(id);
    },
};

// helpers.js pulls in state.js, which reads chrome.storage.local at load.
globalThis.chrome = {
    storage: { local: { get: async () => ({}), set: async () => {} } },
};

const {
    showView,
    goBack,
    setBackRenderer,
    pushCurrentView,
} = require("../src/popup/views/helpers");
const {
    makeBackRenderer,
    markViewRendered,
    resetRenderedViews,
} = require("../src/popup/viewRouter");
const { state } = require("../src/shared/state");

const ADDRESS = "0x1111111111111111111111111111111111111111";
const TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";

let calls;

// Stand-ins for the view modules. Each records itself and then shows its
// view, which is what every real view render ends with — so the assertions
// can tell "rendered and shown" apart from "merely unhidden".
function recorder(name, view) {
    return () => {
        calls.push(name);
        showView(view);
    };
}

function makeViews() {
    return {
        main: { show: recorder("main", "main") },
        addressDetail: { show: recorder("addressDetail", "address") },
        addressToken: { show: recorder("addressToken", "address-token") },
        receive: { show: recorder("receive", "receive") },
        settings: { show: recorder("settings", "settings") },
        settingsAddToken: {
            show: recorder("settingsAddToken", "settings-addtoken"),
        },
        confirmTx: { restore: recorder("confirmTx", "confirm-tx") },
        transactionDetail: {
            render: recorder("transactionDetail", "transaction"),
        },
        txStatus: {
            restoreWait: () => {
                calls.push("waitTx");
                showView("wait-tx");
                return true;
            },
            renderSuccess: recorder("successTx", "success-tx"),
            renderError: recorder("errorTx", "error-tx"),
        },
    };
}

// The popup as it stands just after a reopen: one wallet with one address,
// the view the popup restored onto, and the stack behind it.
//
// A reopen is a fresh page load, so the record of what has been rendered
// starts empty — that emptiness is what makes the Back path render at all.
// Returns the view modules so a test can drive forward navigation through
// the same recorders the router renders through.
function reopenedOn(view, stack, extra) {
    calls = [];
    resetRenderedViews();
    state.wallets = [
        {
            name: "Wallet 1",
            addresses: [{ address: ADDRESS, balance: "0", tokenBalances: [] }],
        },
    ];
    state.selectedWallet = 0;
    state.selectedAddress = 0;
    state.selectedToken = null;
    state.viewData = null;
    state.currentView = view;
    state.viewStack = stack.slice();
    Object.assign(state, extra || {});
    // Restoring onto a view renders it, so the reopened popup has that one
    // view on the page and nothing else.
    markViewRendered(view);
    const views = makeViews();
    setBackRenderer(makeBackRenderer(state, views));
    return views;
}

// The reproduction from the issue, step for step.
describe("Back onto a view the reopened popup never rendered", () => {
    test("Back from settings renders the address detail underneath", () => {
        reopenedOn("settings", ["main", "address"]);
        goBack();
        expect(calls).toEqual(["addressDetail"]);
        expect(state.currentView).toBe("address");
        expect(state.viewStack).toEqual(["main"]);
    });

    test("Back onto the token detail renders it", () => {
        reopenedOn("settings", ["main", "address", "address-token"], {
            selectedToken: TOKEN,
        });
        goBack();
        expect(calls).toEqual(["addressToken"]);
        expect(state.currentView).toBe("address-token");
    });

    test("Back onto Receive renders it", () => {
        reopenedOn("settings", ["main", "address", "receive"]);
        goBack();
        expect(calls).toEqual(["receive"]);
        expect(state.currentView).toBe("receive");
    });

    test("Back onto the transaction detail renders it", () => {
        reopenedOn("settings", ["main", "transaction"], {
            viewData: { tx: { hash: "0xdead" } },
        });
        goBack();
        expect(calls).toEqual(["transactionDetail"]);
        expect(state.currentView).toBe("transaction");
    });

    test("Back onto the transaction confirmation restores it", () => {
        reopenedOn("settings", ["main", "confirm-tx"], {
            viewData: { pendingTx: { to: ADDRESS, amount: "1" } },
        });
        goBack();
        expect(calls).toEqual(["confirmTx"]);
        expect(state.currentView).toBe("confirm-tx");
    });

    test("Back onto the success screen renders it", () => {
        reopenedOn("settings", ["main", "success-tx"], {
            viewData: { hash: "0xdead" },
        });
        goBack();
        expect(calls).toEqual(["successTx"]);
        expect(state.currentView).toBe("success-tx");
    });

    test("Back onto the failure screen renders it", () => {
        reopenedOn("settings", ["main", "error-tx"], {
            viewData: { message: "execution reverted" },
        });
        goBack();
        expect(calls).toEqual(["errorTx"]);
        expect(state.currentView).toBe("error-tx");
    });

    test("Back onto Home renders the wallet list", () => {
        reopenedOn("settings", ["main"]);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("Back with an empty stack renders Home", () => {
        reopenedOn("settings", []);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });
});

// The guards are restoreView()'s, so a popped view whose backing data is
// gone lands on Home rather than on an empty template.
describe("Back onto a view whose backing data is gone", () => {
    test("the token detail with no token selected falls back to Home", () => {
        reopenedOn("settings", ["main", "address-token"]);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("the transaction detail with no transaction falls back to Home", () => {
        reopenedOn("settings", ["main", "transaction"]);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("the confirmation with no pending transaction falls back to Home", () => {
        reopenedOn("settings", ["main", "confirm-tx"]);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("an address view with no address selected falls back to Home", () => {
        reopenedOn("settings", ["main", "receive"], {
            selectedAddress: null,
        });
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("the success screen with no transaction hash falls back to Home", () => {
        reopenedOn("settings", ["main", "success-tx"]);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("the failure screen with no message falls back to Home", () => {
        reopenedOn("settings", ["main", "error-tx"]);
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });

    test("a wait that can no longer be resumed falls back to Home", () => {
        reopenedOn("settings", ["main", "wait-tx"]);
        const views = makeViews();
        views.txStatus.restoreWait = () => false;
        setBackRenderer(makeBackRenderer(state, views));
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });
});

// Forward navigation renders as it goes, and a second render would re-fetch
// and clobber whatever the view holds — an unsaved edit, a request in
// flight. So the Back path renders only a view this page load has never
// rendered, and merely unhides every other one: the views it does not
// render from persisted state, and the views already on the page.
describe("what the Back path leaves alone", () => {
    test("forward navigation renders nothing by itself", () => {
        reopenedOn("address", ["main"]);
        pushCurrentView();
        showView("send");
        expect(calls).toEqual([]);
        expect(state.viewStack).toEqual(["main", "address"]);
    });

    test("Back onto a live-session view only unhides it", () => {
        reopenedOn("confirm-tx", ["main", "address", "send"]);
        goBack();
        expect(calls).toEqual([]);
        expect(state.currentView).toBe("send");
    });

    test("Back renders its target exactly once", () => {
        reopenedOn("settings", ["main", "address"]);
        goBack();
        expect(calls.filter((c) => c === "addressDetail")).toHaveLength(1);
    });

    test("Back onto a view this page load already rendered only unhides it", () => {
        const views = reopenedOn("main", []);
        pushCurrentView();
        views.addressDetail.show();
        pushCurrentView();
        views.settings.show();
        calls = [];
        goBack();
        expect(calls).toEqual([]);
        expect(state.currentView).toBe("address");
    });

    // The unit mirror of the regression the browser suite pins: Settings
    // reassigns its fields from persisted state on every render, so a
    // re-render on the way back discards an edit the user has not saved.
    test("Back onto Settings visited earlier in this page load does not re-render it", () => {
        const views = reopenedOn("main", []);
        pushCurrentView();
        views.settings.show();
        pushCurrentView();
        views.settingsAddToken.show();
        calls = [];
        goBack();
        expect(calls).toEqual([]);
        expect(state.currentView).toBe("settings");
    });

    // Home is the deliberate exception, unchanged from the popup's
    // behaviour before the router existed: it re-renders on every Back so
    // the wallet list reflects what changed while the user was away.
    test("Back onto Home renders it again even when it is already on the page", () => {
        const views = reopenedOn("main", []);
        pushCurrentView();
        views.addressDetail.show();
        calls = [];
        goBack();
        expect(calls).toEqual(["main"]);
        expect(state.currentView).toBe("main");
    });
});
