import { afterEach, describe, expect, it, vi } from 'vitest';

import { createOnlyOfficeBridge } from '../../src/onlyoffice/bridge';

describe('filler bridge', () => {
  afterEach(() => vi.unstubAllGlobals());

  it('binds init and executes inspect command', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    const asc: any = { scope: {}, plugin: { info: { options: { ticket: 'x' } }, callCommand: vi.fn((command, _a, _b, callback) => callback(command())), executeCommand: vi.fn(), executeMethod: vi.fn(), onThemeChangedBase: vi.fn() } };
    const bridge = createOnlyOfficeBridge(asc);
    const ready = vi.fn();
    bridge.bindLifecycle({ onControlChanged: vi.fn(), onControlFocused: vi.fn(), onReady: ready, onThemeChanged: vi.fn() });
    asc.plugin.init();
    expect(ready).toHaveBeenCalled();
    await expect(bridge.inspectFields()).resolves.toEqual([]);
  });

  it('reports an invalid editor command result when the SDK callback has no value', async () => {
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((_command, _a, _b, callback) => callback(undefined)),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };

    await expect(createOnlyOfficeBridge(asc).inspectFields()).rejects.toThrow('编辑器命令返回无效结果');
    expect(asc.scope.elnRecordFillInspect).toBeUndefined();
  });

  it('does not serialize document commands outside record fill-only mode', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    const callbacks: Array<() => void> = [];
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((command, _a, _b, callback) => callbacks.push(() => callback(command()))),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);

    const first = bridge.inspectFields();
    const second = bridge.inspectFields();
    expect(asc.plugin.callCommand).toHaveBeenCalledTimes(2);
    callbacks.forEach((callback) => callback());

    await expect(Promise.all([first, second])).resolves.toEqual([[], []]);
  });

  it('temporarily lifts record fill restrictions while running controlled document commands', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    let restriction = 'none';
    const transitions: string[] = [];
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((command, _a, _b, callback) => {
          expect(restriction).toBe('none');
          callback(command());
        }),
        executeCommand: vi.fn(),
        executeMethod: vi.fn((name, args, callback) => {
          if (name === 'SetEditingRestrictions') {
            restriction = args[0];
            transitions.push(restriction);
          }
          callback(undefined);
        }),
        onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);

    await bridge.setFillOnlyMode(true);
    await expect(bridge.inspectFields()).resolves.toEqual([]);

    expect(transitions).toEqual(['forms', 'none', 'forms']);
  });

  it('falls back to read-only when restoring fill-only restrictions fails', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    let restriction = 'none';
    let formsCalls = 0;
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((command, _a, _b, callback) => callback(command())),
        executeCommand: vi.fn(),
        executeMethod: vi.fn((name, args, callback) => {
          if (name === 'SetEditingRestrictions') {
            if (args[0] === 'forms' && ++formsCalls === 2) throw new Error('restriction restore failed');
            restriction = args[0];
          }
          callback(undefined);
        }),
        onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);

    await bridge.setFillOnlyMode(true);
    await expect(bridge.inspectFields()).rejects.toThrow('restriction restore failed');

    expect(restriction).toBe('readOnly');
  });

  it('passes a repeat row identity through command scope and cleans it after callback', async () => {
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((_command, _a, _b, callback) => {
          expect(asc.scope.elnRecordFillRepeatRow).toEqual({ groupCode: 'results', rowId: 'row-1' });
          callback(JSON.stringify({ ok: true, tags: ['eln.repeat.results.row-1.value'] }));
        }),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };

    await expect(createOnlyOfficeBridge(asc).appendRepeatRow('results', 'row-1')).resolves.toEqual([
      'eln.repeat.results.row-1.value',
    ]);
    expect(asc.scope.elnRecordFillRepeatRow).toBeUndefined();

    await expect(createOnlyOfficeBridge(asc).removeRepeatRow('results', 'row-1')).resolves.toEqual([
      'eln.repeat.results.row-1.value',
    ]);
    expect(asc.scope.elnRecordFillRepeatRow).toBeUndefined();
  });

  it('binds and detaches controlled-document change and focus events', () => {
    const handlers = new Map<string, (value: unknown) => void>();
    const attachEditorEvent = vi.fn((name, handler) => handlers.set(name, handler));
    const asc: any = {
      scope: {},
      plugin: {
        attachEditorEvent,
        callCommand: vi.fn(),
        detachEditorEvent: vi.fn((name) => handlers.delete(name)),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };
    const onControlChanged = vi.fn();
    const onControlFocused = vi.fn();
    const cleanup = createOnlyOfficeBridge(asc).bindLifecycle({
      onControlChanged,
      onControlFocused,
      onReady: vi.fn(),
      onThemeChanged: vi.fn(),
    });

    expect(attachEditorEvent).not.toHaveBeenCalled();
    asc.plugin.init();
    handlers.get('onChangeContentControl')?.({ InternalId: '1_2', Tag: 'eln.field.result' });
    handlers.get('onFocusContentControl')?.({ InternalId: '1_2', Tag: 'eln.field.result' });
    expect(onControlChanged).toHaveBeenCalledWith({ InternalId: '1_2', Tag: 'eln.field.result' });
    expect(onControlFocused).toHaveBeenCalledWith({ InternalId: '1_2', Tag: 'eln.field.result' });

    cleanup();
    expect(asc.plugin.detachEditorEvent.mock.calls).toEqual([
      ['onChangeContentControl'],
      ['onFocusContentControl'],
    ]);
  });

  it('selects a controlled field by its internal id and rejects an editor failure', async () => {
    const executeMethod = vi.fn((_name, _args, callback) => callback(false));
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn(), executeCommand: vi.fn(), executeMethod, onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);

    await expect(bridge.selectControl('')).rejects.toThrow('字段控件缺少内部 ID');
    await expect(bridge.selectControl('1_2')).rejects.toThrow('编辑器未能定位字段');
    expect(executeMethod).toHaveBeenCalledWith('SelectContentControl', ['1_2'], expect.any(Function));
  });
});
