import React from 'react';
import { cleanup, fireEvent, render, 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 } from '@tanstack/react-query';
import type { AuthoringAssignment, LayoutOperation } from '../src/platform/infrastructure/api/layoutAuthoring';
import { workspace, revision } from './helpers/layoutAuthoringFixture';

const ports = vi.hoisted(() => ({ request: vi.fn(), profiles: vi.fn(), recordTypes: vi.fn() }));
vi.mock('../src/platform/infrastructure/api/client', () => ({
    apiClient: { getSessionConnectionKind: () => 'backend', request: ports.request },
}));
vi.mock('../src/platform/infrastructure/api/users', () => ({ usersAPI: {
    getProfiles: ports.profiles,
} }));
vi.mock('../src/platform/infrastructure/api/schemas', () => ({ getRecordTypes: ports.recordTypes }));
vi.mock('react-router-dom', async original => ({ ...await original<any>(),
    useParams: () => ({ objectApiName: 'Account' }), useNavigate: () => vi.fn(),
}));
import { LayoutManager } from '../src/platform/features/admin/components/layout/LayoutManager';

// Runtime's workspace projection uses "record-type"; command selectors use
// record_type_id. Exercise the real frontend HTTP adapter and cache together.
const recordTypeAssignment: AuthoringAssignment = {
    assignment_id: 'assignment-rt', object_id: 'Account', layout_id: 'layout-b', specificity: 2,
    dimensions: [{ key: 'profile', value: 'profile-a' }, { key: 'record-type', value: 'rt-a' }],
};
const profileAssignment: AuthoringAssignment = {
    assignment_id: 'assignment-profile', object_id: 'Account', layout_id: 'layout-a', specificity: 1,
    dimensions: [{ key: 'profile', value: 'profile-a' }],
};
const nextRevision = `sha256:${'b'.repeat(64)}`;
const clients: QueryClient[] = [];
beforeEach(() => {
    ports.request.mockReset();
    ports.profiles.mockResolvedValue([{ Id: 'profile-a', Name: 'Standard User' }]);
    ports.recordTypes.mockResolvedValue([
        { recordTypeId: 'master', name: 'Master', master: true, available: true },
        { recordTypeId: 'rt-a', name: 'Customer', master: false, available: true },
    ]);
});
afterEach(() => { cleanup(); clients.splice(0).forEach(client => client.clear()); });

function serve(assignments: AuthoringAssignment[], operation?: LayoutOperation, after = assignments) {
    let current = { ...workspace, assignments, layouts: [...workspace.layouts,
        { layout_id: 'layout-b', object_id: 'Account', name: 'Secondary Layout', is_default: false }],
    };
    ports.request.mockImplementation(async (path: string, options: { body: unknown }) => {
        if (path.endsWith('/workspace')) return structuredClone(current);
        expect(path).toBe('/services/data/v63.0/metadata/layout-authoring/mutate');
        expect(operation).toBeDefined();
        expect(options.body).toEqual({ objectId: 'Account', expectedLayoutRevision: revision, operation });
        current = { ...current, assignments: after, layoutRevision: nextRevision };
        return { changed: true, layoutRevision: nextRevision };
    });
}
function mount() {
    const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } });
    clients.push(client);
    return render(<QueryClientProvider client={client}><LayoutManager /></QueryClientProvider>);
}
async function cells() { return await screen.findAllByRole('combobox'); }
function mutationCalls() { return ports.request.mock.calls.filter(([path]) => path.endsWith('/mutate')); }

it('reads a specific RecordType independently from Master, including after a cold mount', async () => {
    serve([recordTypeAssignment, profileAssignment]);
    const first = mount();
    expect((await cells())[0]).toHaveValue('layout-a');
    expect((await cells())[1]).toHaveValue('layout-b');
    first.unmount();
    mount();
    expect((await cells())[0]).toHaveValue('layout-a');
    expect((await cells())[1]).toHaveValue('layout-b');
    expect(mutationCalls()).toHaveLength(0);
});

it('does not treat a specific RecordType assignment as a Master assignment', async () => {
    serve([recordTypeAssignment]);
    mount();
    expect((await cells())[0]).toHaveValue('__default__');
    expect((await cells())[1]).toHaveValue('layout-b');
});

it.each([0, 1])('Default deletes only the assignment belonging to column %i', async column => {
    const removed = column === 0 ? profileAssignment : recordTypeAssignment;
    const retained = column === 0 ? recordTypeAssignment : profileAssignment;
    serve([recordTypeAssignment, profileAssignment],
        { assignment_delete: { assignment_id: removed.assignment_id } }, [retained]);
    mount();
    fireEvent.change((await cells())[column], { target: { value: '__default__' } });
    await waitFor(() => expect(mutationCalls()).toHaveLength(1));
    await waitFor(() => expect(screen.getAllByRole('combobox')[column]).not.toBeDisabled());
    expect(screen.getAllByRole('combobox')[column]).toHaveValue('__default__');
    expect(screen.getAllByRole('combobox')[1 - column]).toHaveValue(retained.layout_id);
    expect(screen.queryByText(/Assignment save failed/)).not.toBeInTheDocument();
});

it.each([false, true])('uses the command selector name and reads the resulting dimension (replace=%s)', async replace => {
    const target = { ...recordTypeAssignment, layout_id: 'layout-a' };
    serve(replace ? [recordTypeAssignment] : [],
        { assignment_upsert: { layout_id: 'layout-a', selectors: { profile_id: 'profile-a', record_type_id: 'rt-a' } } },
        [target]);
    const first = mount();
    fireEvent.change((await cells())[1], { target: { value: 'layout-a' } });
    await waitFor(() => expect(mutationCalls()).toHaveLength(1));
    await waitFor(() => expect(screen.getAllByRole('combobox')[1]).not.toBeDisabled());
    expect(screen.getAllByRole('combobox')[0]).toHaveValue('__default__');
    expect(screen.getAllByRole('combobox')[1]).toHaveValue('layout-a');
    first.unmount();
    mount();
    expect((await cells())[1]).toHaveValue('layout-a');
    expect(screen.queryByText(/Assignment save failed/)).not.toBeInTheDocument();
});

it('keeps Role, RecordType-only and wildcard assignments outside editable profile cells', async () => {
    serve([
        { ...recordTypeAssignment, assignment_id: 'role', dimensions: [
            ...recordTypeAssignment.dimensions!, { key: 'role', value: 'role-a' }], specificity: 3 },
        { ...recordTypeAssignment, assignment_id: 'rt-only', dimensions: [{ key: 'record-type', value: 'rt-a' }], specificity: 1 },
        { ...recordTypeAssignment, assignment_id: 'wildcard', dimensions: [], specificity: 0 },
    ]);
    mount();
    (await cells()).forEach(cell => expect(cell).toHaveValue('__default__'));
    expect(screen.getByText('Role: role-a')).toBeInTheDocument();
    expect(screen.getByText('RT: rt-a')).toBeInTheDocument();
    fireEvent.change(screen.getAllByRole('combobox')[1], { target: { value: '__default__' } });
    await waitFor(() => expect(screen.getAllByRole('combobox')[1]).not.toBeDisabled());
    expect(mutationCalls()).toHaveLength(0);
});

it('matches both profile and record type rather than assignment order', async () => {
    ports.profiles.mockResolvedValue([{ Id: 'profile-a', Name: 'First' }, { Id: 'profile-b', Name: 'Second' }]);
    ports.recordTypes.mockResolvedValue([
        { recordTypeId: 'rt-a', name: 'Customer', available: true },
        { recordTypeId: 'rt-b', name: 'Partner', available: true },
    ]);
    serve([
        { ...recordTypeAssignment, assignment_id: 'other-profile', layout_id: 'layout-a', dimensions: [
            { key: 'profile', value: 'profile-b' }, { key: 'record-type', value: 'rt-a' }] },
        { ...recordTypeAssignment, assignment_id: 'other-rt', layout_id: 'layout-a', dimensions: [
            { key: 'record-type', value: 'rt-b' }, { key: 'profile', value: 'profile-a' }] },
        recordTypeAssignment,
    ]);
    mount();
    expect((await cells()).map(cell => (cell as HTMLSelectElement).value))
        .toEqual(['__default__', 'layout-b', 'layout-a', '__default__', 'layout-a', '__default__']);
});

it.each([false, true])('authors Profile-only for Master or an object without record types (none=%s)', async none => {
    if (none) ports.recordTypes.mockResolvedValue([]);
    serve([], { assignment_upsert: { layout_id: 'layout-a', selectors: { profile_id: 'profile-a' } } }, [profileAssignment]);
    mount();
    fireEvent.change((await cells())[0], { target: { value: 'layout-a' } });
    await waitFor(() => expect(mutationCalls()).toHaveLength(1));
    await waitFor(() => expect(screen.getAllByRole('combobox')[0]).not.toBeDisabled());
    expect(screen.getAllByRole('combobox')[0]).toHaveValue('layout-a');
    expect(screen.queryByText(/Assignment save failed/)).not.toBeInTheDocument();
});

it('retains the selected assignment and shows an error when deletion is rejected', async () => {
    serve([recordTypeAssignment]);
    const read = ports.request.getMockImplementation()!;
    ports.request.mockImplementation(async (path, options) => {
        if (path.endsWith('/mutate')) throw new Error('Layout revision conflict. Reload layouts.');
        return read(path, options);
    });
    mount();
    fireEvent.change((await cells())[1], { target: { value: '__default__' } });
    expect(await screen.findByText(/Assignment save failed: Layout revision conflict/)).toBeInTheDocument();
    expect(screen.getAllByRole('combobox')[1]).toHaveValue('layout-b');
    expect(screen.getAllByRole('combobox')[1]).not.toBeDisabled();
});

it.each([false, true])('labels Default as inheritance without claiming the object default is effective (none=%s)', async none => {
    if (none) ports.recordTypes.mockResolvedValue([]);
    serve([{ ...profileAssignment, layout_id: 'layout-b' }]);
    mount();
    for (const cell of await cells()) {
        const fallback = Array.from((cell as HTMLSelectElement).options).find(option => option.value === '__default__');
        expect(fallback?.textContent).toBe('— Default (inherited) —');
    }
});

const master = { recordTypeId: 'master', name: 'Master', master: true, available: true };
const customer = { recordTypeId: 'rt-a', name: 'Customer', master: false, available: true };
const partner = { recordTypeId: 'rt-b', name: 'Partner', master: false, available: true };

it.each([
    { name: 'no RecordTypes', recordTypes: [], labels: ['Profile-level assignment'] },
    { name: 'Master only', recordTypes: [master], labels: ['Profile-level assignment'] },
    { name: 'concrete RecordTypes', recordTypes: [customer, partner], labels: ['Profile-level assignment', 'Customer', 'Partner'] },
    { name: 'Master and concrete RecordTypes', recordTypes: [master, customer, partner], labels: ['Profile-level assignment', 'Customer', 'Partner'] },
    { name: 'unavailable RecordTypes', recordTypes: [{ ...customer, available: false }], labels: ['Profile-level assignment'] },
])('offers exactly one Profile-only selector with $name', async ({ recordTypes, labels }) => {
    ports.recordTypes.mockResolvedValue(recordTypes);
    serve([profileAssignment]);
    mount();
    expect(await cells()).toHaveLength(labels.length);
    for (const label of labels) {
        expect(screen.getByRole('columnheader', { name: label })).toBeInTheDocument();
        expect(screen.getByRole('combobox', { name: `Standard User: ${label}` })).toBeInTheDocument();
    }
    expect(screen.getByRole('combobox', { name: 'Standard User: Profile-level assignment' })).toHaveValue('layout-a');
    expect(screen.queryByRole('columnheader', { name: 'Master' })).not.toBeInTheDocument();
    expect(mutationCalls()).toHaveLength(0);
});

it.each(['create', 'replace', 'remove'] as const)('can %s Profile-only with concrete types while preserving other selectors', async action => {
    ports.recordTypes.mockResolvedValue([customer]);
    // Shared layout IDs and neighbouring selector kinds must not affect which
    // assignment is edited or removed.
    const neighbours: AuthoringAssignment[] = [
        recordTypeAssignment,
        { ...recordTypeAssignment, assignment_id: 'role', dimensions: [
            ...recordTypeAssignment.dimensions!, { key: 'role', value: 'role-a' }], specificity: 3 },
        { ...recordTypeAssignment, assignment_id: 'wildcard', dimensions: [], specificity: 0 },
        { ...recordTypeAssignment, assignment_id: 'rt-only', dimensions: [{ key: 'record-type', value: 'rt-a' }], specificity: 1 },
    ];
    const previous = { ...profileAssignment, layout_id: 'layout-b' };
    const operation = action === 'remove'
        ? { assignment_delete: { assignment_id: previous.assignment_id } }
        : { assignment_upsert: { layout_id: 'layout-a', selectors: { profile_id: 'profile-a' } } };
    serve(action === 'create' ? neighbours : [...neighbours, previous], operation,
        action === 'remove' ? neighbours : [...neighbours, profileAssignment]);
    const first = mount();
    const profileCell = await screen.findByRole('combobox', { name: 'Standard User: Profile-level assignment' });
    expect(profileCell).toHaveValue(action === 'create' ? '__default__' : 'layout-b');
    const expected = action === 'remove' ? '__default__' : 'layout-a';
    fireEvent.change(profileCell, { target: { value: expected } });
    await waitFor(() => expect(mutationCalls()).toHaveLength(1));
    await waitFor(() => expect(profileCell).not.toBeDisabled());
    expect(profileCell).toHaveValue(expected);
    expect(screen.getByRole('combobox', { name: 'Standard User: Customer' })).toHaveValue('layout-b');
    expect(screen.getByText('Role: role-a')).toBeInTheDocument();
    expect(screen.queryByText(/Assignment save failed/)).not.toBeInTheDocument();
    first.unmount();
    mount();
    expect(await screen.findByRole('combobox', { name: 'Standard User: Profile-level assignment' })).toHaveValue(expected);
    expect(screen.getByRole('combobox', { name: 'Standard User: Customer' })).toHaveValue('layout-b');
});
