import React from 'react';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { apiClient } from '../src/platform/infrastructure/api/client';
import { hostRoutes, setHostSessionTransportBridgeForTest } from '@nexuscrm/host-sdk';
import { STORAGE_KEYS } from '../src/platform/infrastructure/storage/storageKeys';

const session = vi.hoisted(() => ({ visibleApps: [] as any[], selectApp: vi.fn(), frontendAppId: 'crm' as string | null, listeners: new Set<any>() }));

vi.mock('react-router-dom', () => import('@nexuscrm/test-real-router'));
vi.mock('@nexuscrm/host-sdk', async original => ({
    ...await original<typeof import('@nexuscrm/host-sdk')>(),
    registerHostSessionLifecycleListener: (listener: any) => { session.listeners.add(listener); return () => session.listeners.delete(listener); },
    useApp: () => ({ visibleApps: session.visibleApps, setCurrentAppId: session.selectApp, currentFrontendAppId: session.frontendAppId, refreshApps: async () => {} }),
}));
vi.mock('../src/platform/infrastructure/api/metadata', async original => {
    const actual = await original<typeof import('../src/platform/infrastructure/api/metadata')>();
    return { ...actual, metadataAPI: { ...actual.metadataAPI,
        getSchemas: async () => ({ schemas: [] }), getDashboards: async () => ({ dashboards: [] }),
    } };
});
vi.mock('../src/platform/features/studio/components/StudioObjectEditor', () => ({ StudioObjectEditor: () => null }));
vi.mock('../src/platform/features/studio/components/StudioDashboardEditor', () => ({ StudioDashboardEditor: () => null }));
vi.mock('../src/platform/features/studio/components/StudioPermissions', () => ({ StudioPermissions: () => null }));
import { AppManager } from '../src/platform/pages/AppManager';
import { AppStudio } from '../src/platform/pages/AppStudio';
import { clearAppNavigationCapabilitiesForTest, registerAppNavigationCapabilities } from '../src/platform/core/navigation/appNavigationCapabilities';

beforeEach(() => {
    session.frontendAppId = 'crm'; session.visibleApps = []; session.selectApp.mockClear();
    const values = new Map<string, string>();
    vi.stubGlobal('localStorage', {
        getItem: (key: string) => values.get(key) ?? null,
        setItem: (key: string, value: string) => values.set(key, value),
        removeItem: (key: string) => values.delete(key), clear: () => values.clear(),
        key: (index: number) => [...values.keys()][index] ?? null,
        get length() { return values.size; },
    });
    apiClient.clearSession();
});
afterEach(() => { cleanup(); expect(session.listeners.size).toBe(0); hostRoutes.clearForTest(); clearAppNavigationCapabilitiesForTest(); setHostSessionTransportBridgeForTest(undefined); apiClient.clearSession(); localStorage.clear(); vi.unstubAllGlobals(); });

it('keeps settings visible when the navigation HTTP request fails', async () => {
    localStorage.setItem(STORAGE_KEYS.CONNECTION_MODE, 'backend');
    apiClient.setSession('backend-test-token', '');
    setHostSessionTransportBridgeForTest({ prepareRequest: request => ({
        route: 'host-backend', credentials: 'include', headers: request.headers, consumeResponse: () => {},
    }) });
    const requests: string[] = [];
    vi.stubGlobal('fetch', vi.fn(async (url: string, options: RequestInit) => {
        expect(options.method).toBe('GET');
        requests.push(String(url));
        if (String(url).endsWith('/metadata/applications')) return new Response(JSON.stringify({ apps: [{ id: 'Sales', label: 'Sales' }] }), { headers: { 'content-type': 'application/json' } });
        if (String(url).endsWith('/app-navigation/Sales')) return new Response(JSON.stringify([{ message: 'Navigation unavailable', errorCode: 'UNKNOWN_EXCEPTION' }]), { status: 503, headers: { 'content-type': 'application/json' } });
        throw new Error(`Unexpected endpoint ${url}`);
    }));
    render(<MemoryRouter initialEntries={['/studio/Sales']}><Routes><Route path="/studio/:appId" element={<AppStudio dashboardWidgets={{} as any} />} /></Routes></MemoryRouter>);
    await screen.findByText(/Navigation editing is unavailable/);
    expect(screen.getByRole('button', { name: 'Save navigation' })).toBeDisabled();
    fireEvent.click(screen.getByRole('button', { name: 'App Settings' }));
    expect(await screen.findByLabelText('App Name')).toHaveValue('Sales');
    expect(screen.getByLabelText('App Name')).toBeDisabled();
    expect(requests).toHaveLength(2);
});

// Also walk App Manager's metadata ID through real adapters to the visible serving ID.
it('shows external applications read-only without navigation or settings writes', async () => {
    localStorage.setItem(STORAGE_KEYS.CONNECTION_MODE, 'salesforce');
    apiClient.setSession('external-test-token', 'https://example.salesforce.com');
    const json = (value: unknown) => new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } });
    const fetchMock = vi.fn(async (url: string, options: RequestInit) => {
        expect(options.method).toBe('GET');
        const path = decodeURIComponent(String(url));
        if (path.includes('/ui-api/apps')) return json({ apps: { '06m-app:1': { appId: '06m-app:1', frontendAppId: 'crm', label: 'External app', navItems: [{ id: 'leads', itemType: 'object', objectApiName: 'Lead', label: 'Leads' }] } } });
        if (path.includes('FROM CustomApplication')) return json({ records: [{ Id: '02u-app', DeveloperName: 'Sales', NamespacePrefix: 'vendor', Label: 'External app' }] });
        if (path.includes('FROM AppDefinition')) {
            expect(path).toContain("DeveloperName = 'Sales' AND NamespacePrefix = 'vendor'");
            return json({ records: [{ DurableId: '06m-app' }] });
        }
        if (path.endsWith('/CustomApplication/02u-app')) return json({ Metadata: { label: 'External app', description: 'Read only' } });
        throw new Error(`Unexpected endpoint ${path}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    const { metadataAPI } = await import('../src/platform/infrastructure/api/metadata');
    session.visibleApps = (await metadataAPI.getApps()).apps;
    hostRoutes.register('crm.studio', { id: 'crm.studio', path: '/studio/:appId', slot: 'standalone', component: () => null });
    registerAppNavigationCapabilities('crm', [{ capability: 'studio', routeId: 'crm.studio' }]);
    const open = vi.spyOn(window, 'open').mockReturnValue(null);
    try {
        render(<MemoryRouter initialEntries={['/manager']}><Routes><Route path="/manager" element={<AppManager />} /><Route path="/studio/:appId" element={<AppStudio dashboardWidgets={{} as any} />} /></Routes></MemoryRouter>);
        const studio = await screen.findByRole('link', { name: 'Open in Studio' });
        expect(studio).toHaveAttribute('href', '/studio/02u-app');
        fireEvent.click(studio);
        expect(await screen.findByLabelText('App Name')).toBeDisabled();
        expect(screen.getByLabelText('Description')).toBeDisabled();
        expect(screen.queryByRole('button', { name: 'Save settings' })).not.toBeInTheDocument();
        expect(screen.queryByRole('button', { name: 'Save navigation' })).not.toBeInTheDocument();
        expect(screen.queryByRole('button', { name: 'Edit Salesforce navigation' })).not.toBeInTheDocument();
        await waitFor(() => expect(screen.getByRole('button', { name: 'Launch App' })).toBeEnabled());
        fireEvent.click(screen.getByRole('button', { name: 'Launch App' }));
        expect(session.selectApp).toHaveBeenCalledWith('06m-app:1');
        expect(open).toHaveBeenCalledWith('/object/Lead', '_blank', 'noopener,noreferrer');
        expect(fetchMock).toHaveBeenCalledTimes(5);
    } finally { open.mockRestore(); }
});

it.each(['clear', 'replace', 'replace-then-clear'] as const)('keeps a late backend application list out of App Manager after session %s', async event => {
    localStorage.setItem(STORAGE_KEYS.CONNECTION_MODE, 'backend');
    apiClient.setSession('actor-a', '');
    setHostSessionTransportBridgeForTest({ prepareRequest: request => ({
        route: 'host-backend', credentials: 'include', headers: request.headers, consumeResponse: () => {},
    }) });
    let release!: (response: Response) => void;
    const json = (label: string) => new Response(JSON.stringify({ apps: [{ id: 'Sales', label }] }), { headers: { 'content-type': 'application/json' } });
    const fetchMock = vi.fn().mockImplementationOnce(() => new Promise<Response>(resolve => { release = resolve; }))
        .mockImplementation(async () => json('Actor B application'));
    vi.stubGlobal('fetch', fetchMock);
    render(<MemoryRouter><AppManager /></MemoryRouter>);
    await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
    await act(async () => {
        apiClient.setSession('actor-b', '');
        for (const listener of session.listeners) {
            if (event === 'clear') listener.onCleared?.();
            else listener.onAuthenticated?.({ userId: 'actor-b' });
        }
        if (event === 'replace-then-clear') {
            for (const listener of session.listeners) listener.onCleared?.();
        }
        release(json('Actor A application'));
    });
    expect(screen.queryByText('Actor A application')).not.toBeInTheDocument();
    expect(screen.queryByRole('alert')).not.toBeInTheDocument();
    if (event === 'replace') expect(await screen.findByText('Actor B application')).toBeInTheDocument();
    else expect(fetchMock).toHaveBeenCalledTimes(1);
});
