import React from 'react';
import { render } from '@testing-library/react';
import { AppConfigContext } from '@theorchard/suite-frontend';
import { mockAppConfig } from 'lib/test-helpers/factories';
import { createIdentity, createRole } from 'lib/test-utils/identity-factory';
import PropTypes from 'prop-types';
import { MemoryRouter } from 'react-router-dom';
import {
    BRAND_AWAL,
    FEATURE_REACT_LEGAL_PAGES,
    PRIVACY_LINK,
} from 'src/constants';
import IdentityContext from 'src/context';
import { redirectOffReact } from '../../../utils';
import * as api from '../../../utils/api';
import * as environment from '../../../utils/environment';
import Routes, {
    offReactRoutes,
    offReactPrefixRoutes,
    accountingRoutesWithPages,
} from '../routes';

jest.mock('src/utils', () => ({
    ...jest.requireActual('src/utils'),
    formatMessage: (msg: string) => msg,
    redirectOffReact: jest.fn(() => null),
}));

interface LegacyProviderProps {
    identity: ReturnType<typeof createIdentity>;
    children: React.ReactNode;
}

class LegacyContextProvider extends React.Component<LegacyProviderProps> {
    static childContextTypes = { identity: PropTypes.object };
    getChildContext() {
        return { identity: this.props.identity };
    }
    render() {
        return this.props.children;
    }
}

describe('Routes', () => {
    const defaultProps = {
        onLogout: jest.fn(),
        config: {
            brand: 'orchard',
            environment: 'prod',
        },
    };
    const defaultIdentity = createIdentity({ id: 1 });

    const renderDeep = (
        props: object,
        pathname: string,
        identity = defaultIdentity
    ) =>
        render(
            <AppConfigContext.Provider
                value={{ config: { ...mockAppConfig, ...defaultProps.config } }}
            >
                <LegacyContextProvider identity={identity}>
                    <IdentityContext.Provider
                        value={{ identity, hasAuthentication: () => false }}
                    >
                        <MemoryRouter initialEntries={[{ pathname }]}>
                            <Routes {...defaultProps} {...props} />
                        </MemoryRouter>
                    </IdentityContext.Provider>
                </LegacyContextProvider>
            </AppConfigContext.Provider>
        );

    beforeEach(() => {
        jest.clearAllMocks();
    });

    test('routes with module renders <ModuleLoader>', () => {
        const url = 'matched';
        const module = 'test';
        const { container } = renderDeep({ siteMap: [{ url, module }] }, url);
        expect(
            container.querySelector(`.module-${module}`)
        ).toBeInTheDocument();
    });

    test('routes with pageTitle renders <ModuleLoader> with pageTitle prop', () => {
        const url = 'matched';
        const module = 'test';
        const pageTitle = 'test-title';
        renderDeep({ siteMap: [{ url, module, pageTitle }] }, url);
        expect(document.title).toContain(pageTitle);
    });

    describe('identity is not authenticated', () => {
        test('renders empty output', () => {
            const { container } = renderDeep({}, '/', createIdentity({}));
            expect(container.firstChild).toBeNull();
        });
    });

    describe('for routes with child routes', () => {
        const child = { url: 'child route', key: 'child' };
        const parent = { url: 'parent route', key: 'parent', items: [child] };

        test('renders <SubHeader>', () => {
            const { container } = renderDeep({ siteMap: [parent] }, parent.url);
            expect(container.querySelector('.SubHeader')).toBeInTheDocument();
        });

        test('className is applied', () => {
            const className = 'test-class';
            const { container } = renderDeep(
                { siteMap: [{ ...parent, className }] },
                parent.url
            );
            expect(
                container.querySelector(`.${className}`)
            ).toBeInTheDocument();
        });

        describe('children does not define urls', () => {
            const withoutUrls = {
                url: 'parent route',
                items: [{ match: 'path1' }, { match: 'path2' }],
            };

            test('does not render <SubHeader>', () => {
                const { container } = renderDeep(
                    { siteMap: [withoutUrls] },
                    'some-url'
                );
                expect(container.querySelector('.SubHeader')).toBeNull();
            });
        });
    });

    describe('matching non reactified pages', () => {
        const url = 'to be handled by php';

        test('renders ErrorMessage component', () => {
            const { getByText } = renderDeep({ siteMap: [{ url }] }, url);
            expect(getByText('errors.nonReactifiedPage')).toBeInTheDocument();
        });
    });

    describe('routes that are not accessible for user', () => {
        const identity = createIdentity({
            id: 1,
            roles: [
                createRole('analytics', 'some'),
                createRole('feature_fm', 'publish'),
            ],
            featureFlags: {
                some_feature: 'enabled',
                other_feature: 'control',
            },
            restrictedFeatures: ['view_stuff'],
        });

        const siteMap = [
            { url: 'url1', permission: 'catalog' },
            { url: 'url2', featureFlag: 'other_feature' },
            { url: 'url3', featureControl: 'view_stuff' },
            { url: 'url4', permission: 'analytics', privilege: 'other' },
            {
                url: 'url5',
                permission: 'feature_fm',
                privilege: 'index',
                variants: [
                    { url: 'url51', permission: 'analytics' },
                    { url: 'url52', featureFlag: 'some_feature' },
                    { url: 'url53', featureControl: 'publish' },
                ],
            },
        ];

        test('are not rendered - none of the restricted routes match', () => {
            const { container } = renderDeep({ siteMap }, 'url1', identity);
            expect(container.querySelector('.module-url1')).toBeNull();
        });
    });

    describe('/logoff route', () => {
        const siteMap = [{ url: '/test' }];
        const identity = createIdentity({ id: 1 });

        beforeEach(() => {
            jest.spyOn(api, 'postPublic').mockResolvedValue(true);
            jest.spyOn(console, 'log').mockImplementation();
        });

        test('calls logout callback', () => {
            const onLogout = jest.fn();
            renderDeep({ siteMap, onLogout }, '/logoff', identity);
            expect(onLogout).toHaveBeenCalledTimes(1);
        });
    });

    describe('legal routes', () => {
        const siteMap = [{ url: '/legal/privacy' }, { url: '/legal/terms' }];

        test('renders legal page when FF is ON', () => {
            const identity = createIdentity({
                id: 1,
                featureFlags: { [FEATURE_REACT_LEGAL_PAGES]: 'enabled' },
            });

            const { getByText } = renderDeep(
                { siteMap, config: { brand: 'orchard', environment: 'prod' } },
                PRIVACY_LINK,
                identity
            );
            expect(getByText('Privacy Policy')).toBeInTheDocument();
        });
    });

    describe('offReactRoutes', () => {
        test('offReactRoutes array contains analytics/overview', () => {
            expect(offReactRoutes).toContain('/analytics/overview');
        });
    });

    describe('offReactPrefixRoutes', () => {
        test('offReactPrefixRoutes array contains /artistinfo', () => {
            expect(offReactPrefixRoutes).toContain('/artistinfo');
        });
    });

    describe('when its an "offReactPrefix" route', () => {
        test('calls redirectOffReact with the full path for /artistinfo sub-route', () => {
            jest.spyOn(console, 'error').mockImplementation(() => {});
            try {
                renderDeep(
                    { siteMap: [] },
                    '/artistinfo/index/artist_id/1737138'
                );
            } catch {
                // React throws for returning undefined from component
            }
            // Must forward the full path, not just the prefix '/artistinfo'
            expect(redirectOffReact).toHaveBeenCalledWith(
                '/artistinfo/index/artist_id/1737138'
            );
        });

        test('does NOT call redirectOffReact with just the prefix', () => {
            jest.spyOn(console, 'error').mockImplementation(() => {});
            try {
                renderDeep(
                    { siteMap: [] },
                    '/artistinfo/index/artist_id/1737138'
                );
            } catch {
                // no-op
            }
            expect(redirectOffReact).not.toHaveBeenCalledWith('/artistinfo');
        });
    });

    describe('accountingRoutesWithPages', () => {
        test('contains expected accounting routes', () => {
            const paths = accountingRoutesWithPages.map(r => r.route);
            expect(paths).toContain('/accounting/statementshistory');
            expect(paths).toContain('/accounting/physicalreserves');
            expect(paths).toContain('/accounting');
            expect(paths).toContain('/alw/accounting');
        });
    });

    test('includes fallback route to 404 result', () => {
        const { getByText } = renderDeep(
            { siteMap: [] },
            '/some-nonexistent-route'
        );
        expect(getByText('errors.pageNotFound')).toBeInTheDocument();
    });

    describe('when its an AWAL user', () => {
        test('redirects to catalog at index', () => {
            const identity = createIdentity({ id: 1 });
            const catalogModule = 'frontend-catalog';
            const { container } = renderDeep(
                {
                    siteMap: [{ url: '/catalog', module: catalogModule }],
                    config: { brand: BRAND_AWAL, environment: 'prod' },
                },
                '/',
                identity
            );
            // AWAL brand causes Redirect from / to /catalog
            expect(
                container.querySelector(`.module-${catalogModule}`)
            ).toBeInTheDocument();
        });
    });

    describe('when its an "offReact" route', () => {
        test('calls redirectOffReact for /analytics/overview', () => {
            // redirectToPhp returns undefined which React complains about,
            // but the important behavior is that redirectOffReact is called
            jest.spyOn(console, 'error').mockImplementation(() => {});
            try {
                renderDeep({ siteMap: [] }, '/analytics/overview');
            } catch {
                // React throws for returning undefined from component
            }
            expect(redirectOffReact).toHaveBeenCalledWith(
                '/analytics/overview'
            );
        });
    });

    describe('collaborators route', () => {
        test('redirects /accounting/collaborators externally', () => {
            renderDeep({ siteMap: [] }, '/accounting/collaborators');
            expect(window.location.href).toContain('collaborators');
        });
    });

    describe('accounting redirect routes', () => {
        test.each([
            ['/accounting/statementshistory', 'statements'],
            ['/accounting/physicalreserves', 'physical-reserves'],
            ['/accounting', 'highlights/overview'],
            ['/alw/accounting', 'highlights/overview'],
        ])('redirects %s to accounting app', (route, page) => {
            renderDeep({ siteMap: [] }, route);
            expect(window.location.href).toContain(page);
        });
    });

    describe('legal routes when FF is OFF', () => {
        test('does not render legal page', () => {
            const identity = createIdentity({ id: 1 });
            const { getByText } = renderDeep(
                {
                    siteMap: [],
                    config: { brand: 'orchard', environment: 'prod' },
                },
                PRIVACY_LINK,
                identity
            );
            // Without the FF, the legal route is not registered,
            // so the fallback 404 renders instead
            expect(getByText('errors.pageNotFound')).toBeInTheDocument();
        });
    });

    describe('in dev mode', () => {
        test('includes route to named modules', () => {
            jest.spyOn(environment, 'isDev').mockReturnValue(true);

            const { container } = renderDeep(
                { siteMap: [] },
                '/module/test-module'
            );
            expect(
                container.querySelector('.module-test-module')
            ).toBeInTheDocument();

            jest.mocked(environment.isDev).mockRestore();
        });
    });
});
