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

import type { FieldDefinition } from '../../src/domain/types';
import { createOnlyOfficeBridge } from '../../src/onlyoffice/bridge';
import { disableTrackRevisionsCommand, insertTypedControlCommand, listContentControlsCommand, writeTextDefaultCommand } from '../../src/onlyoffice/commands';
import type { AscGlobal, OfficeContentControl } from '../../src/onlyoffice/types';

interface AscFixture {
  asc: AscGlobal;
  callCommands: Array<{ command: () => string; payload: unknown; scopeKey: string }>;
  closeCalls: Array<[string, string]>;
  listCommands: Array<{ command: () => string; payload: unknown; scopeKey: string }>;
  methodCalls: Array<[string, unknown[]]>;
}

interface AscFixtureOptions {
  commandResults?: unknown[];
  methodResults?: unknown[];
}

const singleColor = { R: 23, G: 109, B: 92 };
type TextFieldDefinition = FieldDefinition & { controlType: 'text' };

function control(Tag: string, InternalId = `id-${Tag}`): OfficeContentControl {
  return { Alias: Tag, InternalId, Tag };
}

function textDefinition(overrides: Partial<TextFieldDefinition> = {}): FieldDefinition {
  return {
    mode: 'single',
    controlType: 'text',
    fieldCode: 'result',
    alias: '结果',
    groupCode: '',
    rowId: '',
    tag: 'eln.field.result',
    color: singleColor,
    placeholder: '请输入结果',
    defaultValue: '',
    defaultChecked: false,
    ...overrides,
  };
}

function typedDefinition(controlType: 'checkbox' | 'date' | 'dropdown' | 'multiline'): FieldDefinition {
  const base = {
    mode: 'single' as const,
    fieldCode: 'result',
    alias: '结果',
    groupCode: '',
    rowId: '',
    tag: 'eln.field.result',
    color: singleColor,
  };

  if (controlType === 'dropdown') {
    return {
      ...base,
      controlType,
      placeholder: '请选择结果',
      options: [
        { display: '合格', value: 'pass' },
        { display: '不合格', value: 'fail' },
      ],
      selectedIndex: 1,
    };
  }

  return {
    ...base,
    controlType,
    placeholder: controlType === 'checkbox' ? '结果' : '请输入结果',
    defaultValue: controlType === 'date' ? '2026-08-11' : controlType === 'multiline' ? '第一行\n第二行' : '',
    defaultChecked: controlType === 'checkbox',
  };
}

function createAscFixture(options: AscFixtureOptions = {}): AscFixture {
  const methodResults = [...(options.methodResults ?? [])];
  const commandResults = [...(options.commandResults ?? [])];
  const methodCalls: Array<[string, unknown[]]> = [];
  const callCommands: AscFixture['callCommands'] = [];
  const listCommands: AscFixture['listCommands'] = [];
  const closeCalls: Array<[string, string]> = [];
  const scope: AscGlobal['scope'] = {};

  const asc: AscGlobal = {
    scope,
    plugin: {
      executeMethod(name, args, callback) {
        methodCalls.push([name, args]);
        callback(methodResults.shift());
        return true;
      },
      callCommand(command, _isClose, _isCalc, callback) {
        const scopeKey = Object.keys(scope)[0] ?? '';
        if (command === listContentControlsCommand) {
          listCommands.push({ command, scopeKey, payload: scope[scopeKey] });
          callback({ ok: true, controls: methodResults.shift() });
          return true;
        }
        callCommands.push({ command, scopeKey, payload: scope[scopeKey] });
        callback(commandResults.length > 0 ? commandResults.shift() : { ok: true });
        return true;
      },
      executeCommand(command, data) {
        closeCalls.push([command, data]);
      },
      onThemeChangedBase: vi.fn(),
    },
  };

  return { asc, callCommands, closeCalls, listCommands, methodCalls };
}

describe('ONLYOFFICE bridge', () => {
  it('原样读取插件运行 options', () => {
    const fixture = createAscFixture();
    const options = { context: true };
    fixture.asc.plugin.info = { options };

    expect(createOnlyOfficeBridge(fixture.asc).getRuntimeOptions()).toBe(options);
  });

  it('通过扫描 command 回读具体类型并清理列表 scope', async () => {
    const expected = [{ ...control('eln.field.result'), ControlType: 'date' as const }];
    const fixture = createAscFixture({ methodResults: [expected] });

    await expect(createOnlyOfficeBridge(fixture.asc).listControls()).resolves.toEqual(expected);
    expect(fixture.listCommands).toEqual([{ command: listContentControlsCommand, scopeKey: 'elnTemplateList', payload: {} }]);
    expect(fixture.asc.scope.elnTemplateList).toBeUndefined();
    expect(fixture.methodCalls).toHaveLength(0);
  });

  it('拒绝扫描 command 的非数组内容控件结果', async () => {
    const fixture = createAscFixture({ methodResults: [null] });

    await expect(createOnlyOfficeBridge(fixture.asc).listControls()).rejects.toThrow('编辑器未返回内容控件列表');
  });

  it('拒绝扫描 command 的同步异常', async () => {
    const fixture = createAscFixture();
    fixture.asc.plugin.callCommand = () => {
      throw new Error('method crashed');
    };

    await expect(createOnlyOfficeBridge(fixture.asc).listControls()).rejects.toThrow('method crashed');
  });
});

describe('ONLYOFFICE command bridge', () => {
  it('关闭文档内部的修订跟踪状态', async () => {
    const fixture = createAscFixture({ commandResults: [{ ok: true }] });

    await expect(createOnlyOfficeBridge(fixture.asc).disableTrackRevisions()).resolves.toBeUndefined();
    expect(fixture.callCommands).toEqual([
      {
        command: disableTrackRevisionsCommand,
        scopeKey: 'elnTemplateReview',
        payload: {},
      },
    ]);
  });

  it.each([
    ['JSON 字符串', JSON.stringify({ ok: true })],
    ['对象', { ok: true }],
  ])('写入 scope，并接受%s命令结果', async (_label, commandResult) => {
    const tag = 'eln.field.result';
    const fixture = createAscFixture({ methodResults: [[], [control(tag)]], commandResults: [commandResult] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(typedDefinition('checkbox'))).resolves.toEqual([control(tag)]);

    expect(fixture.callCommands).toHaveLength(1);
    expect(fixture.callCommands[0]).toMatchObject({
      command: insertTypedControlCommand,
      scopeKey: 'elnTemplateInsert',
    });
    expect(fixture.asc.scope.elnTemplateInsert).toBeUndefined();
  });

  it.each([
    [{ ok: false, error: '业务失败' }, '业务失败'],
    [{ ok: false }, '编辑器命令执行失败'],
    [{ ok: false, error: 500 }, '编辑器命令执行失败'],
  ])('归一化命令失败 %#', async (commandResult, message) => {
    const fixture = createAscFixture({ methodResults: [[]], commandResults: [commandResult] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(typedDefinition('checkbox'))).rejects.toThrow(message);
    expect(fixture.asc.scope.elnTemplateInsert).toBeUndefined();
  });

  it.each([
    ['非法 JSON', 'not-json'],
    ['null', null],
    ['数组', []],
    ['数字', 1],
    ['缺少 ok 的对象', { error: '业务失败' }],
    ['ok 非布尔值', { ok: 'false', error: '业务失败' }],
  ])('拒绝%s命令返回值', async (_label, commandResult) => {
    const fixture = createAscFixture({ methodResults: [[]], commandResults: [commandResult] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(typedDefinition('checkbox'))).rejects.toThrow('编辑器命令返回无效结果');
    expect(fixture.asc.scope.elnTemplateInsert).toBeUndefined();
  });

  it('归一化 callCommand 同步异常并清理 scope', async () => {
    const fixture = createAscFixture({ methodResults: [[]] });
    fixture.asc.plugin.callCommand = () => {
      throw new Error('command crashed');
    };

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(typedDefinition('checkbox'))).rejects.toThrow('command crashed');
    expect(fixture.asc.scope.elnTemplateInsert).toBeUndefined();
  });
});

describe('字段插入', () => {
  it('插入前拒绝已存在的 Tag，且不执行插入', async () => {
    const fixture = createAscFixture({ methodResults: [[control('eln.field.result')]] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(textDefinition())).rejects.toThrow('字段 ID 已存在');
    expect(fixture.methodCalls).toHaveLength(0);
    expect(fixture.listCommands).toHaveLength(1);
    expect(fixture.callCommands).toHaveLength(0);
  });

  it('插入前拒绝现有 ELN 控件的同名 Alias，且忽略首尾空格', async () => {
    const fixture = createAscFixture({
      methodResults: [[{ Alias: ' 结果 ', InternalId: 'existing-1', Tag: 'eln.field.other_result' }]],
    });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(textDefinition())).rejects.toThrow('字段名称已存在');
    expect(fixture.methodCalls).toHaveLength(0);
    expect(fixture.listCommands).toHaveLength(1);
    expect(fixture.callCommands).toHaveLength(0);
  });

  it('非 ELN 内容控件的同名 Alias 不参与字段名称校验', async () => {
    const definition = textDefinition();
    const after = [
      { Alias: '结果', InternalId: 'custom-1', Tag: 'custom.control' },
      control(definition.tag),
    ];
    const fixture = createAscFixture({
      methodResults: [[{ Alias: '结果', InternalId: 'custom-1', Tag: 'custom.control' }], { InternalId: 'created-1' }, after],
    });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(definition)).resolves.toBe(after);
  });

  it('文本字段使用固定 AddContentControl 参数并返回插入后的列表', async () => {
    const definition = textDefinition();
    const after = [control(definition.tag)];
    const fixture = createAscFixture({ methodResults: [[], { InternalId: 'created-1' }, after] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(definition)).resolves.toBe(after);
    expect(fixture.methodCalls).toEqual([
      [
        'AddContentControl',
        [
          2,
          {
            Tag: definition.tag,
            Alias: definition.alias,
            Lock: 3,
            PlaceHolderText: definition.placeholder,
            Appearance: 1,
            Color: definition.color,
          },
        ],
      ],
    ]);
    expect(fixture.listCommands).toHaveLength(2);
  });

  it('文本字段创建结果缺少 InternalId 时失败', async () => {
    const fixture = createAscFixture({ methodResults: [[], {}] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(textDefinition())).rejects.toThrow('编辑器未返回新内容控件');
  });

  it('文本默认值通过独立 command 写入', async () => {
    const definition = textDefinition({ defaultValue: 'S-001' });
    const fixture = createAscFixture({
      methodResults: [[], { InternalId: 'created-1' }, [control(definition.tag)]],
      commandResults: [{ ok: true }],
    });

    await createOnlyOfficeBridge(fixture.asc).insertField(definition);

    expect(fixture.callCommands).toEqual([
      {
        command: writeTextDefaultCommand,
        scopeKey: 'elnTemplateTextDefault',
        payload: { tag: definition.tag, value: 'S-001' },
      },
    ]);
  });

  it('文本默认值写入失败时说明控件已创建', async () => {
    const fixture = createAscFixture({
      methodResults: [[], { InternalId: 'created-1' }],
      commandResults: [{ ok: false, error: '无法回读' }],
    });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(textDefinition({ defaultValue: 'S-001' }))).rejects.toThrow(
      '控件已创建，但默认值写入失败：无法回读',
    );
  });

  it.each([
    [
      'dropdown' as const,
      {
        controlType: 'dropdown',
        tag: 'eln.field.result',
        alias: '结果',
        placeholder: '请选择结果',
        defaultValue: '',
        defaultChecked: false,
        options: [
          { display: '合格', value: 'pass' },
          { display: '不合格', value: 'fail' },
        ],
        selectedIndex: 1,
        color: singleColor,
      },
    ],
    [
      'checkbox' as const,
      {
        controlType: 'checkbox',
        tag: 'eln.field.result',
        alias: '结果',
        placeholder: '结果',
        defaultValue: '',
        defaultChecked: true,
        options: [],
        selectedIndex: -1,
        color: singleColor,
      },
    ],
    [
      'date' as const,
      {
        controlType: 'date',
        tag: 'eln.field.result',
        alias: '结果',
        placeholder: '请输入结果',
        defaultValue: '2026-08-11',
        defaultChecked: false,
        options: [],
        selectedIndex: -1,
        color: singleColor,
      },
    ],
    [
      'multiline' as const,
      {
        controlType: 'multiline',
        tag: 'eln.field.result',
        alias: '结果',
        placeholder: '请输入结果',
        defaultValue: '第一行\n第二行',
        defaultChecked: false,
        options: [],
        selectedIndex: -1,
        color: singleColor,
      },
    ],
  ])('%s 字段使用 typed command 的完整兼容 payload', async (controlType, expectedPayload) => {
    const definition = typedDefinition(controlType);
    const fixture = createAscFixture({ methodResults: [[], [control(definition.tag)]] });

    await createOnlyOfficeBridge(fixture.asc).insertField(definition);

    expect(fixture.callCommands).toEqual([
      {
        command: insertTypedControlCommand,
        scopeKey: 'elnTemplateInsert',
        payload: expectedPayload,
      },
    ]);
  });

  it.each([
    ['没有增加', []],
    ['异常增加两个', [control('eln.field.result', 'first'), control('eln.field.result', 'second')]],
  ])('插入后 Tag 计数%s时失败', async (_label, after) => {
    const definition = typedDefinition('checkbox');
    const fixture = createAscFixture({ methodResults: [[], after] });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(definition)).rejects.toThrow('插入后 Tag 计数未增加');
  });

  it.each([
    ['text', textDefinition(), [[], { InternalId: 'created-1' }, null]],
    ['typed', typedDefinition('checkbox'), [[], null]],
  ])('%s 字段插入后枚举结果非数组时使用插入后错误', async (_label, definition, methodResults) => {
    const fixture = createAscFixture({ methodResults });

    await expect(createOnlyOfficeBridge(fixture.asc).insertField(definition)).rejects.toThrow('插入后无法读取内容控件列表');
  });
});

describe('定位、关闭与生命周期', () => {
  it('删除拒绝空的内部 ID', async () => {
    const fixture = createAscFixture();

    await expect(createOnlyOfficeBridge(fixture.asc).deleteControl('')).rejects.toThrow('该内容控件没有可用的内部 ID');
    expect(fixture.methodCalls).toHaveLength(0);
  });

  it('解除目标不存在时不调用移除方法', async () => {
    const fixture = createAscFixture({ methodResults: [[control('eln.field.other', 'other-id')]] });

    await expect(createOnlyOfficeBridge(fixture.asc).deleteControl('stale-id')).rejects.toThrow('删除目标已不存在，请刷新字段列表');
    expect(fixture.methodCalls).toHaveLength(0);
    expect(fixture.listCommands).toHaveLength(1);
  });

  it.each([
    ['移除外壳失败', [[control('eln.field.result', 'control-1')], false], '编辑器未能移除字段控件'],
    ['解除后仍存在', [[control('eln.field.result', 'control-1')], true, [control('eln.field.result', 'control-1')]], '解除后字段仍然存在'],
  ])('%s时拒绝解除', async (_label, methodResults, message) => {
    const fixture = createAscFixture({ methodResults });

    await expect(createOnlyOfficeBridge(fixture.asc).deleteControl('control-1')).rejects.toThrow(message);
  });

  it('只移除内容控件外壳、保留内容并返回解除后的列表', async () => {
    const remaining = [control('eln.field.other', 'other-id')];
    const fixture = createAscFixture({ methodResults: [[control('eln.field.result', 'control-1'), ...remaining], undefined, remaining] });

    await expect(createOnlyOfficeBridge(fixture.asc).deleteControl('control-1')).resolves.toBe(remaining);
    expect(fixture.methodCalls).toEqual([['RemoveContentControl', ['control-1']]]);
    expect(fixture.listCommands).toHaveLength(2);
  });

  it('拒绝空的内部 ID', async () => {
    const fixture = createAscFixture();

    await expect(createOnlyOfficeBridge(fixture.asc).selectControl('')).rejects.toThrow('该内容控件没有可用的内部 ID');
    expect(fixture.methodCalls).toHaveLength(0);
  });

  it('目标已不存在时不调用定位方法', async () => {
    const fixture = createAscFixture({ methodResults: [[control('eln.field.other', 'other-id')]] });

    await expect(createOnlyOfficeBridge(fixture.asc).selectControl('stale-id')).rejects.toThrow('定位目标已不存在，请刷新字段列表');
    expect(fixture.methodCalls).toHaveLength(0);
    expect(fixture.listCommands).toHaveLength(1);
  });

  it('编辑器明确返回 false 时报告定位失败', async () => {
    const fixture = createAscFixture({ methodResults: [[control('eln.field.result', 'control-1')], false] });

    await expect(createOnlyOfficeBridge(fixture.asc).selectControl('control-1')).rejects.toThrow('编辑器未能定位字段');
    expect(fixture.methodCalls).toEqual([['SelectContentControl', ['control-1']]]);
    expect(fixture.listCommands).toHaveLength(1);
  });

  it.each([undefined, true])('通过内部 ID 定位内容控件并兼容返回 %s', async (result) => {
    const fixture = createAscFixture({ methodResults: [[control('eln.field.result', 'control-1')], result] });

    await expect(createOnlyOfficeBridge(fixture.asc).selectControl('control-1')).resolves.toBeUndefined();
    expect(fixture.methodCalls).toEqual([['SelectContentControl', ['control-1']]]);
    expect(fixture.listCommands).toHaveLength(1);
  });

  it('关闭插件窗口', () => {
    const fixture = createAscFixture();

    createOnlyOfficeBridge(fixture.asc).close();

    expect(fixture.closeCalls).toEqual([['close', '']]);
  });

  it('绑定 init、button 和主题回调，并在 cleanup 时恢复旧引用', () => {
    const events: string[] = [];
    const fixture = createAscFixture();
    const oldInit = vi.fn();
    const oldButton = vi.fn();
    const oldTheme = vi.fn();
    fixture.asc.plugin.init = oldInit;
    fixture.asc.plugin.button = oldButton;
    fixture.asc.plugin.onThemeChanged = oldTheme;
    fixture.asc.plugin.onThemeChangedBase = vi.fn(() => events.push('base'));
    const cleanup = createOnlyOfficeBridge(fixture.asc).bindLifecycle({
      onReady: () => events.push('ready'),
      onThemeChanged: () => events.push('theme'),
    });

    fixture.asc.plugin.init?.();
    fixture.asc.plugin.button?.(0);
    fixture.asc.plugin.button?.(-1);
    fixture.asc.plugin.onThemeChanged?.({ type: 'dark' });

    expect(events).toEqual(['ready', 'base', 'theme']);
    expect(fixture.closeCalls).toEqual([['close', '']]);
    cleanup();
    expect(fixture.asc.plugin.init).toBe(oldInit);
    expect(fixture.asc.plugin.button).toBe(oldButton);
    expect(fixture.asc.plugin.onThemeChanged).toBe(oldTheme);
  });

  it('旧实例 cleanup 不覆盖较新实例的生命周期函数', () => {
    const fixture = createAscFixture();
    const firstCleanup = createOnlyOfficeBridge(fixture.asc).bindLifecycle({ onReady: vi.fn(), onThemeChanged: vi.fn() });
    createOnlyOfficeBridge(fixture.asc).bindLifecycle({ onReady: vi.fn(), onThemeChanged: vi.fn() });
    const secondLifecycle = {
      init: fixture.asc.plugin.init,
      button: fixture.asc.plugin.button,
      onThemeChanged: fixture.asc.plugin.onThemeChanged,
    };

    firstCleanup();

    expect(fixture.asc.plugin.init).toBe(secondLifecycle.init);
    expect(fixture.asc.plugin.button).toBe(secondLifecycle.button);
    expect(fixture.asc.plugin.onThemeChanged).toBe(secondLifecycle.onThemeChanged);
  });

  it('交错 cleanup 后恢复绑定前引用且已释放实例不再响应', () => {
    const fixture = createAscFixture();
    const oldInit = vi.fn();
    const oldButton = vi.fn();
    const oldTheme = vi.fn();
    const firstReady = vi.fn();
    const firstTheme = vi.fn();
    const secondReady = vi.fn();
    const secondTheme = vi.fn();
    fixture.asc.plugin.init = oldInit;
    fixture.asc.plugin.button = oldButton;
    fixture.asc.plugin.onThemeChanged = oldTheme;
    const firstCleanup = createOnlyOfficeBridge(fixture.asc).bindLifecycle({
      onReady: firstReady,
      onThemeChanged: firstTheme,
    });
    const secondCleanup = createOnlyOfficeBridge(fixture.asc).bindLifecycle({
      onReady: secondReady,
      onThemeChanged: secondTheme,
    });

    fixture.asc.plugin.init?.();
    fixture.asc.plugin.onThemeChanged?.({ type: 'contrast' });
    expect(firstReady).not.toHaveBeenCalled();
    expect(firstTheme).not.toHaveBeenCalled();
    expect(secondReady).toHaveBeenCalledTimes(1);
    expect(secondTheme).toHaveBeenCalledTimes(1);

    firstCleanup();
    fixture.asc.plugin.init?.();
    fixture.asc.plugin.onThemeChanged?.({ type: 'dark' });

    expect(firstReady).not.toHaveBeenCalled();
    expect(firstTheme).not.toHaveBeenCalled();
    expect(secondReady).toHaveBeenCalledTimes(2);
    expect(secondTheme).toHaveBeenCalledTimes(2);

    secondCleanup();
    expect(fixture.asc.plugin.init).toBe(oldInit);
    expect(fixture.asc.plugin.button).toBe(oldButton);
    expect(fixture.asc.plugin.onThemeChanged).toBe(oldTheme);

    fixture.asc.plugin.init?.();
    fixture.asc.plugin.button?.(-1);
    fixture.asc.plugin.onThemeChanged?.({ type: 'light' });
    expect(firstReady).not.toHaveBeenCalled();
    expect(firstTheme).not.toHaveBeenCalled();
    expect(secondReady).toHaveBeenCalledTimes(2);
    expect(secondTheme).toHaveBeenCalledTimes(2);
    expect(oldInit).toHaveBeenCalledTimes(1);
    expect(oldButton).toHaveBeenCalledWith(-1);
    expect(oldTheme).toHaveBeenCalledWith({ type: 'light' });
  });
});
