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

import {
  applyFieldsCommand,
  cloneRepeatRowCommand,
  inspectFieldsCommand,
  removeRepeatRowCommand,
} from '../../src/onlyoffice/commands';

function control(
  tag: string,
  kind: 'checkbox' | 'date' | 'multiline' | 'select' | 'text',
  initial: boolean | string,
  initialLock = 'unlocked',
  dateFormat = 'yyyy-MM-dd',
  initialTextPr: Record<string, unknown> = {},
) {
  let value = initial;
  let lock = initialLock;
  let textPr = initialTextPr;
  const setLock = vi.fn((next: string) => {
    lock = next;
    return true;
  });
  const options = [
    { GetText: () => '合格', GetValue: () => 'qualified' },
    { GetText: () => '不合格', GetValue: () => 'unqualified' },
  ];
  return {
    GetTag: () => tag,
    GetAlias: () => '字段',
    GetInternalId: () => `id-${tag}`,
    GetLock: () => lock,
    GetRange: () => {
      const range = {
        GetText: () => {
          if (typeof value === 'boolean') return value ? '☒' : '☐';
          return kind === 'multiline' && value ? `${value.replaceAll('\n', '\r\n')}\r\n` : value;
        },
        GetTextPr: () => textPr,
        SetTextPr(next: Record<string, unknown>) {
          textPr = next;
          return range;
        },
      };
      return range;
    },
    IsCheckBox: () => kind === 'checkbox',
    IsComboBox: () => kind === 'select',
    IsDropDownList: () => false,
    IsDatePicker: () => kind === 'date',
    IsBlockLevel: () => kind === 'multiline',
    GetDropdownList: () => ({ GetElementsCount: () => options.length, GetItem: (index: number) => options[index] }),
    SetCheckBoxChecked(next: boolean) {
      value = next;
      return true;
    },
    SelectListItem(next: string) {
      value = options.find((item) => item.GetValue() === next)?.GetText() ?? '';
      return true;
    },
    SetDate(next: Date) {
      const date = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}-${String(next.getDate()).padStart(2, '0')}`;
      value = /h/iu.test(dateFormat)
        ? `${date} ${String(next.getHours()).padStart(2, '0')}:${String(next.getMinutes()).padStart(2, '0')}:${String(next.getSeconds()).padStart(2, '0')}`
        : date;
      return true;
    },
    SetLock: setLock,
    ToJSON: () => JSON.stringify({ sdtPr: { date: { dateFormat } } }),
    RemoveAllElements() {
      value = '';
      textPr = {};
      return true;
    },
    AddElement(item: { text?: string }) {
      const text = item.text ?? '';
      value = kind === 'multiline' && value !== '' ? `${value}\n${text}` : `${value}${text}`;
      return true;
    },
  };
}

function install(controls: unknown[], changes: unknown[] = []) {
  const createParagraph = vi.fn(() => ({
    text: '',
    AddText(text: string) {
      this.text += text;
      return true;
    },
  }));
  vi.stubGlobal('Asc', { scope: { elnRecordFillInspect: {}, elnRecordFillChanges: changes } });
  vi.stubGlobal('Api', {
    GetDocument: () => ({ GetAllContentControls: () => controls }),
    CreateRun: () => ({
      text: '',
      AddText(value: string) {
        this.text = value;
        return true;
      },
    }),
    CreateParagraph: createParagraph,
  });
  return { createParagraph };
}

function serializeCommand(command: () => string): () => string {
  // The ONLYOFFICE command is intentionally re-evaluated without module scope.
  // eslint-disable-next-line no-new-func
  return new Function(`"use strict"; return (${command.toString()});`)() as () => string;
}

function installRepeatDocument(options: { failTag?: string } = {}) {
  const controls: any[] = [];
  const rows: any[] = [];
  let targetSequence = 0;
  let table: any;

  function makeControl(initialTag: string, cell: any) {
    let tag = initialTag;
    return {
      GetParentTableCell: () => cell,
      GetTag: () => tag,
      SetTag: vi.fn((next: string) => {
        if (next === options.failTag) return false;
        tag = next;
        return true;
      }),
    };
  }

  function makeRow(id: string, sourceTags: Array<null | string>) {
    const row: any = {
      GetCellsCount: () => sourceTags.length,
      GetInternalId: () => id,
      GetCell: (index: number) => row.cells[index],
      Remove: vi.fn(() => {
        const index = rows.indexOf(row);
        if (index >= 0) rows.splice(index, 1);
        for (let controlIndex = controls.length - 1; controlIndex >= 0; controlIndex -= 1) {
          if (controls[controlIndex].GetParentTableCell().GetParentRow() === row) controls.splice(controlIndex, 1);
        }
        return true;
      }),
      cells: [] as any[],
    };
    row.cells = sourceTags.map((sourceTag) => {
      const elements = sourceTag ? [{ Copy: () => ({ tag: sourceTag }) }] : [{ Copy: () => ({ tag: null }) }];
      const cell: any = {
        Cell: { SetVMerge: vi.fn() },
        AddElement: vi.fn((_index: number, element: { tag: null | string }) => {
          if (element.tag) controls.push(makeControl(element.tag, cell));
          return true;
        }),
        Clear: vi.fn(() => true),
        GetContent: () => ({ GetContent: () => elements }),
        GetParentRow: () => row,
        GetParentTable: () => table,
      };
      return cell;
    });
    return row;
  }

  const sourceTags = ['eln.repeat.results.prototype.value', 'eln.repeat.results.prototype.unit'];
  const sourceRow = makeRow('source-row', sourceTags);
  table = {
    AddRow: vi.fn((anchorCell: any, isBefore = false) => {
      targetSequence += 1;
      const target = makeRow(`target-row-${targetSequence}`, sourceTags.map(() => null));
      const anchorIndex = rows.indexOf(anchorCell.GetParentRow());
      rows.splice(anchorIndex + (isBefore ? 0 : 1), 0, target);
      return target;
    }),
    GetInternalId: () => 'table-1',
    GetRow: (index: number) => rows[index],
    GetRowsCount: () => rows.length,
  };
  rows.push(sourceRow);
  sourceTags.forEach((tag, index) => controls.push(makeControl(tag, sourceRow.GetCell(index))));
  vi.stubGlobal('Asc', { scope: { elnRecordFillRepeatRow: { groupCode: 'results', rowId: 'row-1' } } });
  vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => controls }) });
  return { controls, rows, table };
}

// eslint-disable-next-line test/prefer-lowercase-title -- ONLYOFFICE is the product's canonical spelling.
describe('ONLYOFFICE fill commands', () => {
  afterEach(() => vi.unstubAllGlobals());

  it('inspects typed primary and reference ELN controls and ignores other tags', () => {
    install([
      control('eln.field.name', 'text', 'A'),
      control('eln.ref.name.instance_1', 'text', 'A'),
      control('eln.field.ok', 'checkbox', false),
      control('other', 'text', 'x'),
    ]);
    const result = JSON.parse(inspectFieldsCommand());
    expect(result.fields).toHaveLength(3);
    expect(result.fields[1]).toMatchObject({ tag: 'eln.ref.name.instance_1', type: 'text', currentValue: 'A' });
    expect(result.fields[2]).toMatchObject({ tag: 'eln.field.ok', type: 'checkbox', currentValue: false });
  });

  it('inspects dotted shared field codes for primary and reference controls', () => {
    install([
      control('eln.field.inspection.result', 'text', '合格'),
      control('eln.ref.inspection.result.instance_1', 'text', '合格'),
    ]);

    expect(JSON.parse(inspectFieldsCommand()).fields).toEqual([
      expect.objectContaining({ tag: 'eln.field.inspection.result', currentValue: '合格' }),
      expect.objectContaining({ tag: 'eln.ref.inspection.result.instance_1', currentValue: '合格' }),
    ]);
  });

  it('treats ONLYOFFICE placeholder text as an empty field value', () => {
    const item = control('eln.field.observation', 'text', '请输入检测观察');
    item.ToJSON = () => JSON.stringify({ sdtPr: { showingPlcHdr: true } });
    install([item]);

    expect(JSON.parse(inspectFieldsCommand()).fields).toEqual([
      expect.objectContaining({ tag: 'eln.field.observation', currentValue: '' }),
    ]);
  });

  it('inspects fields after ONLYOFFICE serializes the command without module closures', () => {
    install([control('eln.field.name', 'text', 'A')]);

    expect(JSON.parse(serializeCommand(inspectFieldsCommand)())).toMatchObject({
      ok: true,
      fields: [{ tag: 'eln.field.name', type: 'text', currentValue: 'A' }],
    });
  });

  it('keeps a datetime control format when inspecting fields', () => {
    install([control('eln.field.tested_at', 'date', '2026-08-13 09:10:11', 'unlocked', 'yyyy-MM-dd HH:mm:ss')]);

    expect(JSON.parse(inspectFieldsCommand()).fields).toEqual([
      expect.objectContaining({ dateFormat: 'yyyy-MM-dd HH:mm:ss', tag: 'eln.field.tested_at', type: 'date' }),
    ]);
  });

  it('preserves Chinese date formats and applies canonical datetime values', () => {
    const item = control('eln.field.tested_at', 'date', '', 'unlocked', 'yyyy年MM月dd日 HH:mm:ss');
    install([item], [{ tag: 'eln.field.tested_at', type: 'date', value: '2026-09-05 14:30:25' }]);

    expect(JSON.parse(inspectFieldsCommand()).fields).toEqual([
      expect.objectContaining({ dateFormat: 'yyyy年MM月dd日 HH:mm:ss', tag: 'eln.field.tested_at', type: 'date' }),
    ]);
    expect(JSON.parse(applyFieldsCommand()).results).toEqual([{ tag: 'eln.field.tested_at', ok: true }]);
  });

  it('recognizes a block control from JSON when ONLYOFFICE reports IsBlockLevel as false', () => {
    const item = control('eln.field.result', 'text', '检验结果');
    item.ToJSON = () => JSON.stringify({ sdtPr: {}, type: 'blockLvlSdt' });
    install([item]);

    expect(JSON.parse(inspectFieldsCommand()).fields).toEqual([
      expect.objectContaining({ tag: 'eln.field.result', type: 'multiline' }),
    ]);
  });

  it('applies text, checkbox, select and date values', () => {
    const controls = [
      control('eln.field.name', 'text', ''),
      control('eln.field.ok', 'checkbox', false),
      control('eln.field.result', 'select', '合格'),
      control('eln.field.date', 'date', '2026-08-12'),
    ];
    install(controls, [
      { tag: 'eln.field.name', type: 'text', value: '样品' },
      { tag: 'eln.field.ok', type: 'checkbox', value: true },
      { tag: 'eln.field.result', type: 'select', value: 'unqualified' },
      { tag: 'eln.field.date', type: 'date', value: '2026-08-13' },
    ]);
    expect(JSON.parse(applyFieldsCommand()).results).toEqual([
      { tag: 'eln.field.name', ok: true },
      { tag: 'eln.field.ok', ok: true },
      { tag: 'eln.field.result', ok: true },
      { tag: 'eln.field.date', ok: true },
    ]);
  });

  it.each([
    { kind: 'text', value: '样品' },
    { kind: 'multiline', value: '第一行\n第二行' },
    { kind: 'checkbox', value: true },
    { kind: 'select', value: 'unqualified' },
    { kind: 'date', value: '2026-08-13' },
  ] as const)('填写 $kind 控件后保留原字符格式', ({ kind, value }) => {
    const textPr = { fontFamily: 'SimSun', fontSize: 14, underline: true };
    const item = control(`eln.field.${kind}`, kind, '', 'unlocked', 'yyyy-MM-dd', textPr);
    install([item], [{ tag: `eln.field.${kind}`, type: kind, value }]);

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([{ tag: `eln.field.${kind}`, ok: true }]);
    expect(item.GetRange().GetTextPr()).toEqual(textPr);
  });

  it('applies datetime values without dropping the time component', () => {
    const item = control('eln.field.tested_at', 'date', '2026-08-12 01:02:03', 'unlocked', 'yyyy-MM-dd HH:mm:ss');
    install([item], [{ tag: 'eln.field.tested_at', type: 'date', value: '2026-08-13 09:10:11' }]);

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([{ tag: 'eln.field.tested_at', ok: true }]);
    expect(item.GetRange().GetText()).toBe('2026-08-13 09:10:11');
  });

  it.each([
    { kind: 'select', initial: '合格', format: 'yyyy-MM-dd' },
    { kind: 'date', initial: '2026-08-13', format: 'yyyy-MM-dd' },
    { kind: 'date', initial: '2026-08-13 09:10:11', format: 'yyyy-MM-dd HH:mm:ss' },
  ] as const)('clears $kind ($format) without replacing its definition or lock', ({ kind, initial, format }) => {
    const tag = 'eln.field.source';
    const textPr = { fontFamily: 'SimSun', fontSize: 14 };
    const item = control(tag, kind, initial, 'sdtContentLocked', format, textPr);
    install([item], [{ tag, type: kind, value: '' }]);
    const before = JSON.parse(serializeCommand(inspectFieldsCommand)()).fields[0];

    expect(JSON.parse(serializeCommand(applyFieldsCommand)()).results).toEqual([{ tag, ok: true }]);
    const after = JSON.parse(serializeCommand(inspectFieldsCommand)()).fields[0];
    expect(after).toEqual({ ...before, currentValue: '' });
    expect(item.GetLock()).toBe('sdtContentLocked');
    expect(item.GetRange().GetTextPr()).toEqual(textPr);
  });

  it('applies fields after ONLYOFFICE serializes the command without module closures', () => {
    const item = control('eln.field.name', 'text', '');
    install([item], [{ tag: 'eln.field.name', type: 'text', value: '样品' }]);

    expect(JSON.parse(serializeCommand(applyFieldsCommand)()).results).toEqual([{ tag: 'eln.field.name', ok: true }]);
    expect(item.GetRange().GetText()).toBe('样品');
  });

  it('applies synchronized data to a reference control without treating it as an independent value', () => {
    const item = control('eln.ref.name.instance_1', 'text', 'old');
    install([item], [{ tag: 'eln.ref.name.instance_1', type: 'text', value: '同步值' }]);

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([{ tag: 'eln.ref.name.instance_1', ok: true }]);
    expect(item.GetRange().GetText()).toBe('同步值');
  });

  it('restores a locked reference control after synchronizing it', () => {
    const item = control('eln.ref.name.instance_1', 'text', 'old', 'contentLocked');
    install([item], [{ tag: 'eln.ref.name.instance_1', type: 'text', value: '同步值' }]);

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([{ tag: 'eln.ref.name.instance_1', ok: true }]);
    expect(item.GetRange().GetText()).toBe('同步值');
    expect(item.GetLock()).toBe('contentLocked');
    expect(item.SetLock).toHaveBeenNthCalledWith(1, 'unlocked');
    expect(item.SetLock).toHaveBeenNthCalledWith(2, 'contentLocked');
  });

  it('temporarily unlocks formula result primary and reference controls while restoring current data', () => {
    const primary = control('eln.field.formula_result', 'text', 'old-primary', 'sdtContentLocked');
    const reference = control('eln.ref.formula_result.instance_1', 'text', 'old-reference', 'sdtContentLocked');
    install([primary, reference], [
      { tag: 'eln.field.formula_result', type: 'text', value: '42' },
      { tag: 'eln.ref.formula_result.instance_1', type: 'text', value: '42' },
    ]);

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([
      { tag: 'eln.field.formula_result', ok: true },
      { tag: 'eln.ref.formula_result.instance_1', ok: true },
    ]);
    for (const item of [primary, reference]) {
      expect(item.GetRange().GetText()).toBe('42');
      expect(item.GetLock()).toBe('sdtContentLocked');
      expect(item.SetLock).toHaveBeenNthCalledWith(1, 'unlocked');
      expect(item.SetLock).toHaveBeenNthCalledWith(2, 'sdtContentLocked');
    }
  });

  it('rejects unknown select values without changing the field', () => {
    const item = control('eln.field.result', 'select', '合格');
    install([item], [{ tag: 'eln.field.result', type: 'select', value: 'missing' }]);
    const result = JSON.parse(applyFieldsCommand()).results[0];
    expect(result).toMatchObject({ tag: 'eln.field.result', ok: false, error: '下拉值不在文档选项中' });
    expect(item.GetRange().GetText()).toBe('合格');
  });

  it('writes multiline values as one paragraph per line', () => {
    const item = control('eln.field.notes', 'multiline', '');
    const { createParagraph } = install([item], [{ tag: 'eln.field.notes', type: 'multiline', value: '第一行\n第二行' }]);

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([{ tag: 'eln.field.notes', ok: true }]);
    expect(createParagraph).toHaveBeenCalledTimes(2);
    expect(item.GetRange().GetText()).toBe('第一行\r\n第二行\r\n');
  });

  it('continues applying later fields after one field fails', () => {
    const select = control('eln.field.result', 'select', '合格');
    const text = control('eln.field.name', 'text', '');
    install(
      [select, text],
      [
        { tag: 'eln.field.result', type: 'select', value: 'missing' },
        { tag: 'eln.field.name', type: 'text', value: '样品' },
      ],
    );

    expect(JSON.parse(applyFieldsCommand()).results).toEqual([
      { tag: 'eln.field.result', ok: false, error: '下拉值不在文档选项中' },
      { tag: 'eln.field.name', ok: true },
    ]);
    expect(text.GetRange().GetText()).toBe('样品');
  });

  it('clones the prototype row at the table end and rewrites every repeat tag', () => {
    const { controls, rows, table } = installRepeatDocument();

    const result = JSON.parse(serializeCommand(cloneRepeatRowCommand)());

    expect(result).toEqual({
      ok: true,
      tags: ['eln.repeat.results.row-1.value', 'eln.repeat.results.row-1.unit'],
    });
    expect(table.AddRow).toHaveBeenCalledTimes(1);
    expect(table.AddRow).toHaveBeenCalledWith(expect.anything(), false);
    expect(rows).toHaveLength(2);
    expect(rows[1].GetCell(0).Clear).toHaveBeenCalledTimes(1);
    expect(rows[1].GetCell(1).Clear).not.toHaveBeenCalled();
    expect(rows[1].GetCell(0).Cell.SetVMerge).toHaveBeenCalledWith(undefined);
    expect(rows[1].GetCell(1).Cell.SetVMerge).toHaveBeenCalledWith(undefined);
    expect(controls.map((item) => item.GetTag())).toEqual([
      'eln.repeat.results.prototype.value',
      'eln.repeat.results.prototype.unit',
      'eln.repeat.results.row-1.value',
      'eln.repeat.results.row-1.unit',
    ]);

    expect(JSON.parse(cloneRepeatRowCommand())).toMatchObject({ ok: true });
    expect(table.AddRow).toHaveBeenCalledTimes(1);
  });

  it('removes the appended row when tag rewriting fails', () => {
    const { controls, rows } = installRepeatDocument({ failTag: 'eln.repeat.results.row-1.unit' });

    expect(JSON.parse(cloneRepeatRowCommand())).toMatchObject({ ok: false, error: '重写重复行 Tag 失败' });
    expect(rows).toHaveLength(1);
    expect(controls.map((item) => item.GetTag())).toEqual([
      'eln.repeat.results.prototype.value',
      'eln.repeat.results.prototype.unit',
    ]);
  });

  it('removes exactly one complete runtime row by stable group and row identity', () => {
    const { controls, rows } = installRepeatDocument();
    expect(JSON.parse(cloneRepeatRowCommand())).toMatchObject({ ok: true });
    (globalThis as any).Asc.scope.elnRecordFillRepeatRow = { groupCode: 'results', rowId: 'row-1' };

    const result = JSON.parse(serializeCommand(removeRepeatRowCommand)());

    expect(result).toEqual({
      ok: true,
      tags: ['eln.repeat.results.row-1.value', 'eln.repeat.results.row-1.unit'],
    });
    expect(rows).toHaveLength(1);
    expect(controls.map((item) => item.GetTag())).toEqual([
      'eln.repeat.results.prototype.value',
      'eln.repeat.results.prototype.unit',
    ]);
  });

  it('recreates a removed row before the next logical runtime row', () => {
    const { controls, rows } = installRepeatDocument();
    expect(JSON.parse(cloneRepeatRowCommand())).toMatchObject({ ok: true });
    (globalThis as any).Asc.scope.elnRecordFillRepeatRow = { groupCode: 'results', rowId: 'row-2' };
    expect(JSON.parse(cloneRepeatRowCommand())).toMatchObject({ ok: true });
    (globalThis as any).Asc.scope.elnRecordFillRepeatRow = { groupCode: 'results', rowId: 'row-1' };
    expect(JSON.parse(removeRepeatRowCommand())).toMatchObject({ ok: true });
    (globalThis as any).Asc.scope.elnRecordFillRepeatRow = {
      beforeRowId: 'row-2', groupCode: 'results', rowId: 'row-1',
    };

    expect(JSON.parse(serializeCommand(cloneRepeatRowCommand)())).toMatchObject({ ok: true });
    expect(rows).toHaveLength(3);
    expect(controls.filter((item) => item.GetTag().startsWith('eln.repeat.results.row-1.'))[0]
      .GetParentTableCell().GetParentRow()).toBe(rows[1]);
    expect(controls.filter((item) => item.GetTag().startsWith('eln.repeat.results.row-2.'))[0]
      .GetParentTableCell().GetParentRow()).toBe(rows[2]);
  });
});
