import { TextDecoder, TextEncoder } from 'util';
import { runInAction } from 'mobx';
// The jsdom test env doesn't expose these globals, but `onFileSelected` decodes
// the uploaded bytes with `TextDecoder` (and the test encodes CSV with
// `TextEncoder`). Polyfill from Node's `util`.
const globalScope = global;
globalScope.TextEncoder = globalScope.TextEncoder ?? TextEncoder;
globalScope.TextDecoder = globalScope.TextDecoder ?? TextDecoder;
import { ImportState, } from '../ImportState';
import { JobStatus, WritePolicy, } from '@wix/bex-utils/@wix/ambassador-hub-v1-job/types';
import { createHubJobClient } from '../hubJobClient';
jest.mock('../hubJobClient');
const COMPLETED_JOB = {
    id: 'job-1',
    status: JobStatus.COMPLETED,
    progress: { itemsSuccessful: 2, itemsRejected: 0, total: 2, current: 2 },
};
function setup(clientOverrides = {}, extra = {}) {
    const client = {
        uploadFile: jest.fn(async (_file) => 'file-1'),
        submitImportJob: jest.fn(async () => 'job-1'),
        getJob: jest.fn(async () => COMPLETED_JOB),
        terminateJob: jest.fn(async () => undefined),
        listMovementLogs: jest.fn(),
        ...clientOverrides,
    };
    createHubJobClient.mockReturnValue(client);
    const importModalState = { close: jest.fn() };
    const collection = {
        clearResultAndMoveToStart: jest.fn(),
    };
    const showToast = jest.fn();
    const downloadCsv = jest.fn();
    const translate = (key, params) => params ? `${key}:${JSON.stringify(params)}` : key;
    const { detectColumnType, config: configOverride, ...configOverrides } = extra;
    const config = configOverride ?? {
        fields: [{ id: 'name', header: 'Name' }],
        collectionId: 'Movies',
        ...configOverrides,
    };
    const props = {
        config,
        httpClient: {
            request: jest.fn(),
        },
        collection,
        importModalState: importModalState,
        translate: translate,
        errorHandler: {
            withErrorHandler: (fn) => fn(),
        },
        showToast: showToast,
        downloadCsv,
        detectColumnType,
    };
    const state = new ImportState(props);
    // Seed a parsed CSV mapped to an existing field.
    runInAction(() => {
        state.fileName = 'data.csv';
        state.csvHeaders = ['Name'];
        state.csvRows = [['Apple'], ['Pear']];
        state.mappingByHeader = { Name: { kind: 'existing', fieldId: 'name' } };
    });
    return {
        state,
        client,
        importModalState,
        collection,
        showToast,
        downloadCsv,
    };
}
describe('ImportState engine (data-hub job)', () => {
    beforeEach(() => {
        // The success path awaits a real setTimeout (completion linger); make sure
        // timers are real even if the preset defaults to fake ones.
        jest.useRealTimers();
    });
    it('uploads the aligned file, submits a job, and reports success', async () => {
        const { state, client, importModalState, collection } = setup();
        await state.startImport();
        expect(client.uploadFile).toHaveBeenCalledTimes(1);
        expect(client.uploadFile.mock.calls[0][0]).toBeInstanceOf(File);
        expect(client.submitImportJob).toHaveBeenCalledWith({
            fileId: 'file-1',
            name: 'data.csv',
            destination: { wixDataCollection: { collectionId: 'Movies' } },
        });
        expect(state.result).toEqual({ succeeded: 2, failed: 0 });
        expect(state.importStatus.status).toBe('success');
        expect(importModalState.close).toHaveBeenCalled();
        expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
            force: true,
        });
    });
    it('shows a partial-success toast when some rows are rejected', async () => {
        const { state, showToast, importModalState } = setup({
            getJob: jest.fn(async () => ({
                id: 'job-1',
                status: JobStatus.PARTIALLY_SUCCESSFUL,
                progress: {
                    itemsSuccessful: 1,
                    itemsRejected: 1,
                    total: 2,
                    current: 2,
                },
            })),
        });
        await state.startImport();
        expect(state.result).toEqual({ succeeded: 1, failed: 1 });
        expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'WARNING' }));
        expect(importModalState.close).toHaveBeenCalled();
    });
    it('maps the failed "row N" by record (unaffected by an embedded-newline cell)', async () => {
        const { state, downloadCsv, showToast } = setup({
            getJob: jest.fn(async () => ({
                id: 'job-1',
                status: JobStatus.PARTIALLY_SUCCESSFUL,
                progress: { itemsSuccessful: 2, itemsRejected: 1 },
            })),
            // data-hub numbers records (csv-parse, RFC-4180): the 3rd data record is
            // "row 4" regardless of the multi-line cell in the 1st record.
            listMovementLogs: jest.fn(async () => ({
                logs: [
                    { sourceItemId: 'row 4', failure: { code: 'x', description: 'y' } },
                ],
                pagingMetadata: { hasNext: false },
            })),
        });
        // Row 0 has a quoted embedded newline; the failed record is row 2 (Mango).
        runInAction(() => {
            state.fileName = 'data.csv';
            state.csvHeaders = ['Name', 'Notes'];
            state.csvRows = [
                ['Apple', 'line1\nline2'],
                ['Pear', 'ok'],
                ['Mango', 'bad'],
            ];
            state.mappingByHeader = {
                Name: { kind: 'existing', fieldId: 'name' },
                Notes: { kind: 'existing', fieldId: 'notes' },
            };
        });
        await state.startImport();
        const toast = showToast.mock.calls[0][0];
        await toast.action.onClick();
        // "row 4" → csvRows[2] (Mango), NOT shifted by the embedded newline in row 0.
        const [content] = downloadCsv.mock.calls[0];
        expect(content).toContain('Mango');
        expect(content).not.toContain('Apple');
    });
    it('downloads a re-importable CSV of the failed rows from the partial toast action', async () => {
        const { state, client, showToast, downloadCsv } = setup({
            getJob: jest.fn(async () => ({
                id: 'job-1',
                status: JobStatus.PARTIALLY_SUCCESSFUL,
                progress: { itemsSuccessful: 1, itemsRejected: 1 },
            })),
            // csvRows[1] (the "Pear" row) failed → file line 3 → "row 3".
            listMovementLogs: jest.fn(async () => ({
                logs: [
                    { sourceItemId: 'row 3', failure: { code: 'bad', description: 'x' } },
                ],
                pagingMetadata: { hasNext: false },
            })),
        });
        await state.startImport();
        // Trigger the toast's download action.
        const toast = showToast.mock.calls[0][0];
        await toast.action.onClick();
        expect(client.listMovementLogs).toHaveBeenCalledWith('job-1', undefined);
        // Original headers + only the failed row, ready to fix and re-upload.
        expect(downloadCsv).toHaveBeenCalledWith('Name\r\nPear', 'failed-data.csv');
    });
    it('surfaces an error state with an application-error-shaped error when the job fails', async () => {
        const { state, importModalState, collection } = setup({
            getJob: jest.fn(async () => ({
                id: 'job-1',
                status: JobStatus.FAILED,
                error: { code: 'import_failed', description: 'Bad rows' },
            })),
        });
        await state.startImport();
        expect(state.importStatus.status).toBe('error');
        // HTTP-error-shaped so the UI resolves the message via
        // errorHandler.getResolvedError rather than reading the raw server string.
        // The handler reads the code from `response.data.details.applicationError`.
        const error = state.importStatus.error;
        expect(error.response?.data?.details?.applicationError?.code).toBe('import_failed');
        expect(importModalState.close).not.toHaveBeenCalled();
        expect(collection.clearResultAndMoveToStart).not.toHaveBeenCalled();
    });
    it('hands a failed job to onJobError, which surfaces the wrapped error', async () => {
        // Wiring only: the consumer forwards `run` to their withErrorHandler exactly
        // as documented, and the error it re-rejects is the one surfaced to the UI
        // (with the application-error-shaped code intact). End-to-end resolution
        // against the real error handler is covered in the cairo
        // Table.importButton spec, which has a real errorHandler instance.
        // Mirror `(run) => errorHandler.withErrorHandler(run, errorCodesMap)`;
        // a passthrough stub, so this just runs `run` and re-rejects.
        const withErrorHandler = (fn) => fn();
        const onJobError = jest.fn((run) => withErrorHandler(run));
        const { state } = setup({
            getJob: jest.fn(async () => ({
                id: 'job-1',
                status: JobStatus.FAILED,
                error: { code: 'duplicate_key', description: 'Bad rows' },
            })),
        }, { onJobError });
        await state.startImport();
        expect(onJobError).toHaveBeenCalledTimes(1);
        expect(typeof onJobError.mock.calls[0][0]).toBe('function');
        expect(state.importStatus.status).toBe('error');
        const error = state.importStatus.error;
        expect(error.response?.data?.details?.applicationError?.code).toBe('duplicate_key');
    });
    it('terminates the job when closed mid-import', async () => {
        const { state, client, importModalState } = setup();
        // Cancel during the first poll, before the job is terminal.
        client.getJob.mockImplementation(async () => {
            await state.close();
            return { id: 'job-1', status: JobStatus.IN_PROGRESS };
        });
        await state.startImport();
        expect(client.terminateJob).toHaveBeenCalledWith('job-1');
        expect(importModalState.close).toHaveBeenCalled();
    });
    it('terminates the job when cancelled while submitImportJob is in flight', async () => {
        const { state, client } = setup();
        // The user cancels while submit is still in flight: close() runs before
        // _jobId is set (so it can't terminate), then submit resolves.
        client.submitImportJob.mockImplementation(async () => {
            await state.close();
            return 'job-1';
        });
        await state.startImport();
        // The late-cancel path must terminate the orphaned job, and polling never starts.
        expect(client.terminateJob).toHaveBeenCalledWith('job-1');
        expect(client.getJob).not.toHaveBeenCalled();
    });
    it('aborts the poll loop when disposed (unmounted) mid-import', async () => {
        const { state, client } = setup();
        const dispose = state.init();
        // Simulate the component unmounting during the first poll.
        client.getJob.mockImplementation(async () => {
            dispose();
            return { id: 'job-1', status: JobStatus.IN_PROGRESS };
        });
        await state.startImport();
        // Without the disposer the loop would keep polling the IN_PROGRESS job
        // forever; it must stop after the first fetch.
        expect(client.getJob).toHaveBeenCalledTimes(1);
    });
    it('does not restore when closed during the success completion linger', async () => {
        const restoreFn = jest.fn().mockResolvedValue(undefined);
        const { state, importModalState, collection } = setup({}, { backup: async () => ({ restore: restoreFn }) });
        // A 0ms macrotask drains the pending async chain without firing the 600ms
        // linger timer.
        const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
        const promise = state.startImport();
        // Run backup → upload → submit → poll (terminal) → success, parking on the
        // 600ms completion linger (still on the 'progress' step) without firing it.
        await flush();
        await flush();
        await flush();
        expect(state.importStatus.isSuccess).toBe(true);
        expect(state.currentStep).toBe('progress');
        // User closes the modal mid-linger.
        await state.close();
        await promise;
        // A successful import must not be reverted; the modal closes and the
        // collection refreshes.
        expect(restoreFn).not.toHaveBeenCalled();
        expect(importModalState.close).toHaveBeenCalled();
        expect(collection.clearResultAndMoveToStart).toHaveBeenCalledWith({
            force: true,
        });
    });
});
describe('ImportState writePolicy (id-collision handling)', () => {
    beforeEach(() => {
        jest.useRealTimers();
    });
    // Seed a CSV column mapped to the `_id` field so collisions are possible.
    function seedIdMapping(state) {
        runInAction(() => {
            state.fileName = 'data.csv';
            state.csvHeaders = ['ID', 'Name'];
            state.csvRows = [
                ['1', 'Apple'],
                ['2', 'Pear'],
            ];
            state.mappingByHeader = {
                ID: { kind: 'existing', fieldId: '_id' },
                Name: { kind: 'existing', fieldId: 'name' },
            };
        });
    }
    it('exposes the header mapped to `_id` via idMappedHeader', () => {
        const { state } = setup();
        expect(state.idMappedHeader).toBeNull();
        seedIdMapping(state);
        expect(state.idMappedHeader).toBe('ID');
    });
    it('omits writePolicy when no column is mapped to `_id`', async () => {
        const { state, client } = setup();
        await state.startImport();
        expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
            destination: { wixDataCollection: { collectionId: 'Movies' } },
        }));
    });
    it('sends the default OVERWRITE policy when a column maps to `_id`', async () => {
        const { state, client } = setup();
        seedIdMapping(state);
        await state.startImport();
        expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
            destination: {
                wixDataCollection: {
                    collectionId: 'Movies',
                    writePolicy: WritePolicy.OVERWRITE,
                },
            },
        }));
    });
    it('sends SKIP_EXISTING after the user switches the policy', async () => {
        const { state, client } = setup();
        seedIdMapping(state);
        state.setWritePolicy(WritePolicy.SKIP_EXISTING);
        await state.startImport();
        expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
            destination: {
                wixDataCollection: {
                    collectionId: 'Movies',
                    writePolicy: WritePolicy.SKIP_EXISTING,
                },
            },
        }));
    });
    it('resets the policy back to OVERWRITE', () => {
        const { state } = setup();
        state.setWritePolicy(WritePolicy.SKIP_EXISTING);
        expect(state.writePolicy).toBe(WritePolicy.SKIP_EXISTING);
        state.reset();
        expect(state.writePolicy).toBe(WritePolicy.OVERWRITE);
    });
    it('resets the policy to OVERWRITE when a new file is uploaded mid-session', async () => {
        const { state } = setup();
        seedIdMapping(state);
        // The user switched to Skip for the first file.
        state.setWritePolicy(WritePolicy.SKIP_EXISTING);
        expect(state.writePolicy).toBe(WritePolicy.SKIP_EXISTING);
        // Uploading a different CSV in the same modal session must start from the
        // documented default — otherwise the prior Skip silently carries over and
        // rows with a colliding `_id` are skipped instead of updated.
        const csv = 'ID,Name\nid-9,Zed\n';
        const file = {
            name: 'file-b.csv',
            arrayBuffer: async () => new TextEncoder().encode(csv).buffer,
        };
        await state.onFileSelected(file);
        expect(state.writePolicy).toBe(WritePolicy.OVERWRITE);
    });
    it('keeps the chosen policy when the `_id` mapping is removed and re-added', () => {
        const { state } = setup();
        seedIdMapping(state);
        state.setWritePolicy(WritePolicy.SKIP_EXISTING);
        // Remap the ID column away: the control hides and the destination drops
        // writePolicy, but the user's choice is intentionally retained (editing
        // mappings within a file never resets it — only a new file does).
        state.setMapping('ID', null);
        expect(state.idMappedHeader).toBeNull();
        expect(state.writePolicy).toBe(WritePolicy.SKIP_EXISTING);
        // Map `_id` again — the prior choice is still in effect.
        state.setMapping('ID', '_id');
        expect(state.idMappedHeader).toBe('ID');
        expect(state.writePolicy).toBe(WritePolicy.SKIP_EXISTING);
    });
    it('omits writePolicy when the `_id` column is remapped away mid-session', async () => {
        const { state, client } = setup();
        seedIdMapping(state);
        state.setWritePolicy(WritePolicy.SKIP_EXISTING);
        // No column maps to `_id` anymore → no collisions possible → omit it.
        state.setMapping('ID', null);
        await state.startImport();
        expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
            destination: { wixDataCollection: { collectionId: 'Movies' } },
        }));
    });
});
describe('ImportState orderedHeaders (row order stability)', () => {
    beforeEach(() => {
        jest.useRealTimers();
    });
    // Parse a CSV whose `ID` column auto-maps to `_id` (a field with that id
    // exists), so the `_id` row floats to the top of the ordered list.
    async function parseWithIdColumn(state) {
        const csv = 'Name,ID,Handle\nApple,id-1,a\nPear,id-2,p\n';
        const file = {
            name: 'data.csv',
            arrayBuffer: async () => new TextEncoder().encode(csv).buffer,
        };
        await state.onFileSelected(file);
    }
    it('floats the `_id`-mapped column to the top for a freshly parsed file', async () => {
        const { state } = setup({}, { fields: [{ id: '_id', header: 'ID' }] });
        await parseWithIdColumn(state);
        expect(state.idMappedHeader).toBe('ID');
        expect(state.orderedHeaders).toEqual(['ID', 'Name', 'Handle']);
    });
    it('keeps row order stable when the `_id` mapping is cleared and re-added', async () => {
        const { state } = setup({}, { fields: [{ id: '_id', header: 'ID' }] });
        await parseWithIdColumn(state);
        // Clearing the `_id` dropdown must not reorder the row under the cursor:
        // the snapshot stays fixed even though `idMappedHeader` becomes null.
        state.setMapping('ID', null);
        expect(state.idMappedHeader).toBeNull();
        expect(state.orderedHeaders).toEqual(['ID', 'Name', 'Handle']);
        // Re-mapping `_id` also leaves the order untouched.
        state.setMapping('ID', '_id');
        expect(state.idMappedHeader).toBe('ID');
        expect(state.orderedHeaders).toEqual(['ID', 'Name', 'Handle']);
    });
    it('re-snapshots the order for a new file and clears it on reset', async () => {
        const { state } = setup({}, { fields: [{ id: '_id', header: 'ID' }] });
        await parseWithIdColumn(state);
        expect(state.orderedHeaders).toEqual(['ID', 'Name', 'Handle']);
        // A file with no `_id` column keeps plain CSV order.
        const csv = 'First,Second\n1,2\n';
        const file = {
            name: 'other.csv',
            arrayBuffer: async () => new TextEncoder().encode(csv).buffer,
        };
        await state.onFileSelected(file);
        expect(state.orderedHeaders).toEqual(['First', 'Second']);
        state.reset();
        expect(state.orderedHeaders).toEqual([]);
    });
});
describe('ImportState platformized destination (non–Wix-Data)', () => {
    beforeEach(() => {
        jest.useRealTimers();
    });
    it('sends a consumer-supplied destination verbatim, ignoring collectionId', async () => {
        const { state, client } = setup({}, { destination: { storesCatalogProducts: {} } });
        await state.startImport();
        expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
            destination: { storesCatalogProducts: {} },
        }));
    });
    it('does not add writePolicy to a platformized destination even when `_id` is mapped', async () => {
        const { state, client } = setup({}, { destination: { storesCatalogInventory: {} } });
        // Map a column to `_id` and pick a non-default policy — for a platformized
        // destination this must NOT leak into the destination (writePolicy is a
        // wixDataCollection-only concern).
        runInAction(() => {
            state.csvHeaders = ['Name', 'ID'];
            state.csvRows = [
                ['Apple', '1'],
                ['Pear', '2'],
            ];
            state.mappingByHeader = {
                Name: { kind: 'existing', fieldId: 'name' },
                ID: { kind: 'existing', fieldId: '_id' },
            };
        });
        state.setWritePolicy(WritePolicy.SKIP_EXISTING);
        await state.startImport();
        expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
            destination: { storesCatalogInventory: {} },
        }));
    });
    it('reports canConfigureWritePolicy=false for a custom destination (control hidden), true for the default path', () => {
        const platformized = setup({}, { destination: { storesCatalogProducts: {} } }).state;
        expect(platformized.canConfigureWritePolicy).toBe(false);
        const wixData = setup().state;
        expect(wixData.canConfigureWritePolicy).toBe(true);
    });
    describe('created fields (CAIRO-4400)', () => {
        const RETURNED_FIELDS = {
            name: { id: 'name', displayName: 'Name', type: 'SHORT_TEXT' },
            color: { id: 'color', displayName: 'Color', type: 'SHORT_TEXT' },
        };
        // Maps an extra column to a NEW field so `_resolveFieldMapping` calls
        // `updateSchema`. `updateSchema`'s presence is also what enables new mappings.
        function withNewFieldMapping(state) {
            runInAction(() => {
                state.csvHeaders = ['Name', 'Color'];
                state.csvRows = [
                    ['Apple', 'Red'],
                    ['Pear', 'Green'],
                ];
                state.mappingByHeader = {
                    Name: { kind: 'existing', fieldId: 'name' },
                    Color: { kind: 'new', fieldType: 'text' },
                };
            });
        }
        it('calls mergeFields with updateSchema’s returned map after a successful import', async () => {
            const mergeFields = jest.fn();
            const updateSchema = jest.fn(async () => RETURNED_FIELDS);
            const { state, collection } = setup({}, { updateSchema, mergeFields });
            withNewFieldMapping(state);
            await state.startImport();
            expect(updateSchema).toHaveBeenCalledTimes(1);
            expect(mergeFields).toHaveBeenCalledTimes(1);
            expect(mergeFields).toHaveBeenCalledWith(RETURNED_FIELDS);
            // The merge runs alongside the data refetch on the success path.
            expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
        });
        it('does not call mergeFields for a plain import (no new fields)', async () => {
            const mergeFields = jest.fn();
            const updateSchema = jest.fn(async () => RETURNED_FIELDS);
            const { state } = setup({}, { updateSchema, mergeFields });
            await state.startImport();
            expect(updateSchema).not.toHaveBeenCalled();
            expect(mergeFields).not.toHaveBeenCalled();
        });
        it('does not call mergeFields when updateSchema returns nothing', async () => {
            const mergeFields = jest.fn();
            const updateSchema = jest.fn(async () => undefined);
            const { state } = setup({}, { updateSchema, mergeFields });
            withNewFieldMapping(state);
            await state.startImport();
            expect(updateSchema).toHaveBeenCalledTimes(1);
            expect(mergeFields).not.toHaveBeenCalled();
        });
        it('does not call mergeFields when updateSchema returns an empty map', async () => {
            // A `{}` return is truthy; without a size guard it would reach a full-replace
            // mergeFields({}) and wipe every column. Treat it as "nothing to merge".
            const mergeFields = jest.fn();
            const updateSchema = jest.fn(async () => ({}));
            const { state } = setup({}, { updateSchema, mergeFields });
            withNewFieldMapping(state);
            await state.startImport();
            expect(updateSchema).toHaveBeenCalledTimes(1);
            expect(mergeFields).not.toHaveBeenCalled();
        });
        it('does not call mergeFields when the import fails after creating fields', async () => {
            const mergeFields = jest.fn();
            const updateSchema = jest.fn(async () => RETURNED_FIELDS);
            const { state } = setup({
                uploadFile: jest.fn(async () => {
                    throw new Error('upload failed');
                }),
            }, { updateSchema, mergeFields });
            withNewFieldMapping(state);
            await state.startImport();
            // Fields were created pre-upload, but a failed import must not merge them.
            expect(updateSchema).toHaveBeenCalledTimes(1);
            expect(state.importStatus.status).toBe('error');
            expect(mergeFields).not.toHaveBeenCalled();
        });
        it('keeps the import successful when mergeFields throws (no void → duplicate-rows)', async () => {
            const consoleError = jest
                .spyOn(console, 'error')
                .mockImplementation(() => { });
            const mergeFields = jest.fn(() => {
                throw new Error('merge boom');
            });
            const updateSchema = jest.fn(async () => RETURNED_FIELDS);
            const { state, importModalState, collection } = setup({}, { updateSchema, mergeFields });
            withNewFieldMapping(state);
            await state.startImport();
            expect(mergeFields).toHaveBeenCalledTimes(1);
            // A throwing merge is swallowed — the completed import is untouched.
            expect(state.importStatus.status).toBe('success');
            expect(importModalState.close).toHaveBeenCalled();
            expect(collection.clearResultAndMoveToStart).toHaveBeenCalled();
            consoleError.mockRestore();
        });
    });
    describe('schema-backed config (live getters + create-field permission)', () => {
        // A schema-backed config sets canAddFields explicitly and supplies
        // updateSchema/idFieldKey — the shape a SchemaImportRef satisfies.
        const schemaConfig = (over = {}) => ({
            fields: [{ id: 'name', header: 'Name' }],
            collectionId: 'RefCollection',
            canAddFields: false,
            ...over,
        });
        it('reads fields live from the config rather than snapshotting them', () => {
            const config = schemaConfig();
            const { state } = setup({}, { config });
            expect(state.fields).toBe(config.fields);
            // A later change to the config is reflected — values are read on demand,
            // so a live (getter-backed) config needn't be resolved before the ctor.
            const updated = [
                { id: 'name', header: 'Name' },
                { id: 'year', header: 'Year' },
            ];
            config.fields = updated;
            expect(state.fields).toBe(updated);
        });
        it('gates create-new-field on canAddFields even when updateSchema is present', () => {
            const off = setup({}, { config: schemaConfig({ canAddFields: false, updateSchema: jest.fn() }) });
            expect(off.state.canCreateNewField).toBe(false);
            const on = setup({}, { config: schemaConfig({ canAddFields: true }) });
            expect(on.state.canCreateNewField).toBe(true);
        });
        it('builds the destination from the config collectionId', async () => {
            const { state, client } = setup({}, { config: schemaConfig({ collectionId: 'RefCollection' }) });
            await state.startImport();
            expect(client.submitImportJob).toHaveBeenCalledWith(expect.objectContaining({
                destination: { wixDataCollection: { collectionId: 'RefCollection' } },
            }));
        });
        it('creates new fields through the config updateSchema', async () => {
            const updateSchema = jest.fn(async () => ({
                name: { id: 'name', displayName: 'Name', type: 'SHORT_TEXT' },
                year: { id: 'year', displayName: 'Year', type: 'NUMBER' },
            }));
            const { state } = setup({}, { config: schemaConfig({ canAddFields: true, updateSchema }) });
            runInAction(() => {
                state.csvHeaders = ['Name', 'Year'];
                state.csvRows = [['Apple', '1999']];
                state.mappingByHeader = {
                    Name: { kind: 'existing', fieldId: 'name' },
                    Year: { kind: 'new', fieldType: 'number' },
                };
            });
            await state.startImport();
            expect(updateSchema).toHaveBeenCalledTimes(1);
        });
        it('detects the id-mapped column using the config idFieldKey', () => {
            const { state } = setup({}, { config: schemaConfig({ idFieldKey: 'movieId' }) });
            runInAction(() => {
                state.mappingByHeader = {
                    Ref: { kind: 'existing', fieldId: 'movieId' },
                };
            });
            expect(state.idMappedHeader).toBe('Ref');
        });
    });
});
//# sourceMappingURL=ImportState.engine.spec.js.map