import React from 'react';
import { act, cleanup, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';

vi.mock('@tanstack/react-query', () => import('@nexuscrm/test-real-react-query'));
import { QueryClient, QueryClientProvider, notifyManager } from '@tanstack/react-query';
import { stored, workspace, revision } from './helpers/layoutAuthoringFixture';

const ports = vi.hoisted(() => ({ request: vi.fn(), listeners: new Set<{
    onCleared?: () => void; onAuthenticated?: (event: { userId: string }) => void;
}>() }));
vi.mock('@nexuscrm/host-sdk', async original => ({
    ...await original<any>(),
    registerHostSessionLifecycleListener: (listener: any) => {
        ports.listeners.add(listener);
        return () => ports.listeners.delete(listener);
    },
}));
vi.mock('../src/platform/infrastructure/api/client', () => ({
    apiClient: { getSessionConnectionKind: () => 'backend', request: ports.request },
}));
vi.mock('../src/platform/infrastructure/api/schemas', () => ({ getRecordTypes: async () => [] }));
vi.mock('../src/platform/infrastructure/api/users', () => ({ usersAPI: { getProfiles: async () => [{ Id: 'profile-a', Name: 'Standard User' }] } }));
vi.mock('react-router-dom', async original => ({ ...await original<any>(), useParams: () => ({ objectApiName: 'Account' }), useNavigate: () => vi.fn() }));

import { useEditableLayout } from '../src/platform/infrastructure/api/objectMetadataHooks';
import { storedLayoutToEditor } from '../src/platform/infrastructure/api/layoutAuthoring';
import { LayoutManager } from '../src/platform/features/admin/components/layout/LayoutManager';

const nextRevision = `sha256:${'b'.repeat(64)}`;
const clients: QueryClient[] = [];
beforeEach(() => { ports.request.mockReset(); });
afterEach(async () => {
    cleanup();
    for (const client of clients.splice(0)) client.clear();
    await drain();
    expect(ports.listeners.size).toBe(0);
});
const drain = () => new Promise<void>(resolve => notifyManager.schedule(resolve));

function scenario(boundary: 'mutate' | 'workspace' | 'stored', initialRead = false, failReadback = false) {
    const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity }, mutations: { retry: false } } });
    clients.push(client);
    const wrapper = ({ children }: { children: React.ReactNode }) => <QueryClientProvider client={client}>{children}</QueryClientProvider>;
    let release!: () => void;
    const held = new Promise<void>(resolve => { release = resolve; });
    let started = false, committed = false, session: string | null = 'actor-a';
    let heldSignal: AbortSignal | undefined;
    ports.request.mockImplementation(async (path: string, options: { signal?: AbortSignal }) => {
        // Authentication and the response snapshot are fixed at request start.
        // The transport deliberately ignores abort while delivering a response
        // already in flight; the real API/operation must reject that completion.
        if (!session) throw new Error('INVALID_SESSION_ID');
        const action = path.split('/').at(-1);
        if (action === 'mutate') committed = true;
        const currentWorkspace = { ...workspace, layoutRevision: committed ? nextRevision : revision,
            layouts: [...workspace.layouts, { layout_id: 'layout-b', object_id: 'Account', name: 'Secondary Layout', is_default: false }],
            assignments: [{ assignment_id: 'assignment-a', object_id: 'Account', layout_id: 'layout-b', dimensions: [{ key: 'profile', value: 'profile-a' }], specificity: 1 }],
        };
        const response = action === 'workspace' ? currentWorkspace
            : action === 'stored' ? { ...stored, layoutRevision: committed ? nextRevision : revision }
            : { changed: true, layoutRevision: nextRevision, layoutId: 'layout-a' };
        if (action === boundary && (initialRead || committed)) {
            heldSignal = options.signal; started = true; await held;
        }
        if (failReadback && committed && action === 'workspace') throw new Error('readback unavailable');
        return response;
    });
    return { client, wrapper, release, started: () => started, signal: () => heldSignal,
        transition: (event: 'clear' | 'replace') => {
            session = event === 'clear' ? null : 'actor-b';
            for (const listener of ports.listeners) {
                if (event === 'clear') listener.onCleared?.();
                else listener.onAuthenticated?.({ userId: 'actor-b' });
            }
            // Host clears cached state at session expiry. No document reload.
            client.clear();
        },
    };
}

it.each((['clear', 'replace'] as const).flatMap(event => (['workspace', 'stored'] as const).map(boundary => ({ event, boundary }))))('discards initial editor $boundary reads after session $event', async ({event, boundary}) => {
    const run = scenario(boundary, true);
    const editor = renderHook(() => useEditableLayout('Account', 'layout-a'), { wrapper: run.wrapper });
    await waitFor(() => expect(run.started()).toBe(true));
    editor.unmount();
    run.transition(event);
    const requestCount = ports.request.mock.calls.length;
    await act(async () => { run.release(); await drain(); });
    expect(ports.request.mock.calls).toHaveLength(requestCount);
    expect(run.client.getQueryCache().getAll()).toHaveLength(0);
    expect(run.signal()?.aborted).toBe(true);
});

it.each((['clear', 'replace'] as const).flatMap(event => (['mutate', 'workspace', 'stored'] as const).map(boundary => ({ event, boundary }))))(
    'rejects an old editor $boundary completion after session $event', async ({ event, boundary }) => {
        const run = scenario(boundary);
        const editor = renderHook(() => useEditableLayout('Account', 'layout-a'), { wrapper: run.wrapper });
        await waitFor(() => expect(editor.result.current.layout).toBeTruthy());
        let outcome!: Promise<unknown>;
        act(() => { outcome = editor.result.current.save(storedLayoutToEditor(stored), ['Name']).catch(error => error); });
        await waitFor(() => expect(run.started()).toBe(true));
        editor.unmount();
        run.transition(event);
        const requestCount = ports.request.mock.calls.length;
        let result: unknown;
        await act(async () => { run.release(); result = await outcome; await drain(); });
        expect(result).toMatchObject({ name: 'AbortError' });
        expect(run.signal()?.aborted).toBe(true);
        expect(ports.request.mock.calls).toHaveLength(requestCount);
        expect(run.client.getQueryCache().getAll()).toHaveLength(0);
    },
);

const actions = ['clone', 'delete', 'default', 'assign', 'unassign'] as const;
function startManagerAction(action: typeof actions[number]) {
    if (action === 'clone') {
        fireEvent.click(screen.getByRole('button', { name: 'Clone Layout' }));
        fireEvent.click(screen.getAllByRole('button', { name: 'Clone', exact: true }).at(-1)!);
    } else if (action === 'delete') {
        vi.spyOn(window, 'confirm').mockReturnValue(true);
        fireEvent.click(screen.getByRole('button', { name: 'Delete Secondary Layout' }));
    } else if (action === 'default') {
        fireEvent.click(screen.getByRole('button', { name: 'Set Secondary Layout as default' }));
    } else {
        fireEvent.change(screen.getByRole('combobox'), { target: { value: action === 'assign' ? 'layout-a' : '__default__' } });
    }
}
it.each(actions.flatMap(action => (['mutate', 'workspace'] as const).flatMap(boundary => (['clear', 'replace'] as const).map(event => ({ action, boundary, event })))))('does not publish a manager $action $boundary completion after session $event', async ({ action, boundary, event }) => {
    const run = scenario(boundary);
    const manager = render(<LayoutManager />, { wrapper: run.wrapper });
    await screen.findByRole('button', { name: 'Set Secondary Layout as default' });
    startManagerAction(action);
    await waitFor(() => expect(run.started()).toBe(true));
    manager.unmount();
    run.transition(event);
    const requestCount = ports.request.mock.calls.length;
    await act(async () => { run.release(); await drain(); });
    expect(ports.request.mock.calls).toHaveLength(requestCount);
    expect(run.client.getQueryCache().getAll()).toHaveLength(0);
    expect(run.signal()?.aborted).toBe(true);
});


it('a committed manager change invalidates business views even when readback fails', async () => {
    const run = scenario('workspace', false, true);
    render(<LayoutManager />, { wrapper: run.wrapper });
    await screen.findByRole('button', { name: 'Set Secondary Layout as default' });
    run.client.setQueryData(['metadata', 'schema', 'Account'], { old: true });
    startManagerAction('default');
    await waitFor(() => expect(run.started()).toBe(true));
    await act(async () => { run.release(); await drain(); });
    await screen.findByText(/readback unavailable/);
    expect(run.client.getQueryState(['metadata', 'schema', 'Account'])?.isInvalidated).toBe(true);
});

it('a live session can still finish a save after its editor unmounts', async () => {
    const run = scenario('stored');
    const editor = renderHook(() => useEditableLayout('Account', 'layout-a'), { wrapper: run.wrapper });
    await waitFor(() => expect(editor.result.current.layout).toBeTruthy());
    run.client.setQueryData(['metadata', 'schema', 'Account'], { old: true });
    let saving!: Promise<unknown>;
    act(() => { saving = editor.result.current.save(storedLayoutToEditor(stored), ['Name']); });
    await waitFor(() => expect(run.started()).toBe(true));
    editor.unmount();
    await act(async () => { run.release(); expect(await saving).toBe('saved'); await drain(); });
    expect(run.client.getQueryState(['metadata', 'schema', 'Account'])?.isInvalidated).toBe(true);
    expect(run.signal()?.aborted).toBe(false);
});


it.each(actions)('publishes a successful manager %s change in its live session', async action => {
    const run = scenario('workspace');
    render(<LayoutManager />, { wrapper: run.wrapper });
    await screen.findByRole('button', { name: 'Set Secondary Layout as default' });
    run.client.setQueryData(['metadata', 'schema', 'Account'], { old: true });
    startManagerAction(action);
    await waitFor(() => expect(run.started()).toBe(true));
    await act(async () => { run.release(); await drain(); });
    await waitFor(() => expect(run.client.getQueryData(['metadata', 'layout-workspace', 'account'])).toMatchObject({ layoutRevision: nextRevision }));
    expect(run.client.getQueryState(['metadata', 'schema', 'Account'])?.isInvalidated).toBe(true);
    expect(screen.getByRole('button', { name: 'Reload layouts' })).not.toBeDisabled();
});
