import routeMap from 'src/routeMap';

/**
 * Guards the lazy-route wiring in routeMap.jsx. Every route's `mainComponent`
 * is a `React.lazy(() => import('...').then(m => ({ default: m.Named })))`.
 * If `m.Named` is a typo or a renamed export, webpack still resolves the
 * MODULE, so the build is green — but the route renders `undefined` and throws
 * "Element type is invalid" at runtime, caught only after merge. This test
 * invokes each factory and asserts it resolves to a real component, so that
 * mistake fails in CI instead.
 */

const REACT_LAZY_TYPE = Symbol.for('react.lazy');

type LazyComponent = {
    $$typeof?: symbol;
    _payload?: { _result?: unknown };
};

const isLazy = (c: unknown): c is Required<LazyComponent> =>
    !!c &&
    (c as LazyComponent).$$typeof === REACT_LAZY_TYPE &&
    !!(c as LazyComponent)._payload;

// A renderable component is a function (function/class component) or an object
// with $$typeof (React.memo / forwardRef / further lazy). `undefined` — the
// symptom of a bad named export — is neither.
const isRenderableComponent = (c: unknown): boolean =>
    typeof c === 'function' ||
    (typeof c === 'object' && c !== null && '$$typeof' in c);

// Returns the component that a route ultimately renders: for a lazy route, the
// resolved `default` export; for a plain component, the component itself.
const resolveRouteComponent = async (component: unknown): Promise<unknown> => {
    if (!isLazy(component)) {
        return component;
    }
    // An uninitialised React.lazy payload holds the import factory in
    // `_result` (React 18/19). Throw — not skip — if that ever changes, so the
    // guarantee can't silently lapse.
    const factory = component._payload._result;
    if (typeof factory !== 'function') {
        throw new Error(
            'React.lazy internals changed: expected _payload._result to be the import factory'
        );
    }
    const moduleObject = await (
        factory as () => Promise<{ default: unknown }>
    )();
    return moduleObject?.default;
};

// Every routed component, deduped, paired with a representative path so a
// failure names the offending route.
const routes = routeMap.sections.flatMap(
    (section: { routes?: { path?: unknown; mainComponent?: unknown }[] }) =>
        section.routes ?? []
);
const byComponent = new Map<unknown, string>();
for (const route of routes) {
    if (route.mainComponent && !byComponent.has(route.mainComponent)) {
        byComponent.set(route.mainComponent, String(route.path));
    }
}
const cases: [string, unknown][] = [...byComponent].map(([component, path]) => [
    path,
    component,
]);

describe('routeMap route components', () => {
    it('discovers routed components to check', () => {
        expect(cases.length).toBeGreaterThan(0);
    });

    it.each(cases)(
        'route "%s" resolves to a valid component',
        async (_path, component) => {
            const resolved = await resolveRouteComponent(component);
            expect(isRenderableComponent(resolved)).toBe(true);
        }
    );
});
