import { describe, it, expect } from 'vitest';
import mergeSettings from './mergeSettings';
import type { CommandItem, EditorSettings, EditorSettingsOverride } from '../types';

const noopIsDisabled: CommandItem['isDisabled'] = () => false;

const createCommand = (overrides: Partial<CommandItem> & Pick<CommandItem, 'name'>): CommandItem => ({
  label: overrides.name,
  description: overrides.name,
  iconName: 'title',
  isTextMenu: false,
  isDisabled: noopIsDisabled,
  ...overrides,
});

const createBase = (): EditorSettings => ({
  features: {
    textItalic: true,
    textUnderline: true,
    textStrike: true,
    textCode: true,
    textMarker: true,
    textColor: true,
    fontSize: true,
    fontFamily: true,
    textSubscript: true,
    textSuperscript: true,
    customClass: false,
    tableBgColor: true,
    imageDefaultLightbox: true,
  },
  blockMenus: [
    {
      name: 'text',
      title: 'テキスト',
      commands: [
        // class なし paragraph (identifier = 'paragraph')
        createCommand({
          name: 'paragraph',
          identifier: 'paragraph',
          label: '段落',
          iconName: 'title',
          isTextMenu: true,
        }),
        // class=lead の paragraph (identifier = 'paragraph.lead')
        createCommand({
          name: 'paragraph',
          identifier: 'paragraph.lead',
          label: 'リード',
          iconName: 'title',
          isTextMenu: true,
          class: 'lead',
        }),
        createCommand({
          name: 'heading2',
          identifier: 'heading2',
          label: '見出し2',
          iconName: 'format_h2',
          isTextMenu: true,
        }),
      ],
    },
    {
      name: 'media',
      title: 'メディア',
      commands: [
        createCommand({ name: 'image', identifier: 'image', label: '画像', iconName: 'photo' }),
        createCommand({ name: 'file', identifier: 'file', label: 'ファイル', iconName: 'draft' }),
      ],
    },
    {
      name: 'layout',
      title: 'レイアウト',
      commands: [
        createCommand({ name: 'table', identifier: 'table', label: 'テーブル', iconName: 'table' }),
        createCommand({ name: 'columns2', identifier: 'columns2', label: '2カラム', iconName: 'view_column' }),
      ],
    },
  ],
  fontSize: [{ label: 'base-font', value: '16px' }],
  fontFamily: [],
  customClass: [{ label: 'base', value: 'base' }],
  imageSizes: [],
  colorPalette: ['#000000'],
});

const identifiersOf = (settings: EditorSettings): string[] =>
  settings.blockMenus.flatMap((group) => group.commands.map((c) => c.identifier ?? c.name));

describe('mergeSettings', () => {
  it('override が未指定なら base をそのまま返す', () => {
    const base = createBase();

    const result = mergeSettings(base);

    expect(result).toEqual(base);
  });

  it('空の override では base と等価のまま', () => {
    const base = createBase();

    const result = mergeSettings(base, {});

    expect(result).toEqual(base);
  });

  it('features の一部だけを上書きし、他のフラグは base を維持する', () => {
    const base = createBase();
    const override: EditorSettingsOverride = {
      features: { textItalic: false, customClass: true },
    };

    const result = mergeSettings(base, override);

    expect(result.features.textItalic).toBe(false);
    expect(result.features.customClass).toBe(true);
    expect(result.features.textUnderline).toBe(true);
    expect(result.features.tableBgColor).toBe(true);
    // override に含めなかったフラグは base のまま維持される
    expect(result.features.imageDefaultLightbox).toBe(true);
  });

  it('imageDefaultLightbox だけを false に上書きできる', () => {
    const base = createBase();
    const override: EditorSettingsOverride = {
      features: { imageDefaultLightbox: false },
    };

    const result = mergeSettings(base, override);

    expect(result.features.imageDefaultLightbox).toBe(false);
    // 他のフラグは base を維持
    expect(result.features.textItalic).toBe(true);
  });

  it('customClass を override すると配列が丸ごと差し替わる', () => {
    const base = createBase();
    const override: EditorSettingsOverride = {
      customClass: [{ label: 'test', value: 'test' }],
    };

    const result = mergeSettings(base, override);

    expect(result.customClass).toEqual([{ label: 'test', value: 'test' }]);
  });

  it('override 後も base のオブジェクトは変更されない', () => {
    const base = createBase();
    const snapshot = JSON.parse(JSON.stringify(base));

    mergeSettings(base, {
      features: { textItalic: false },
      customClass: [{ label: 'test', value: 'test' }],
      blockMenus: { deny: ['table'] },
    });

    expect(JSON.parse(JSON.stringify(base))).toEqual(snapshot);
  });

  it('override に null/非オブジェクトが渡されても base を返す', () => {
    const base = createBase();

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(mergeSettings(base, null as any)).toEqual(base);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(mergeSettings(base, 'foo' as any)).toEqual(base);
  });

  it('fontSize / fontFamily / imageSizes / colorPalette は配列差し替えで上書きできる', () => {
    const base = createBase();
    const override: EditorSettingsOverride = {
      fontSize: [{ label: 'override-font', value: '20px' }],
      fontFamily: [{ label: 'override-family', value: 'serif' }],
      imageSizes: [{ label: 'override-size', value: 'medium' }],
      colorPalette: ['#ffffff', '#ff0000'],
    };

    const result = mergeSettings(base, override);

    expect(result.fontSize).toEqual([{ label: 'override-font', value: '20px' }]);
    expect(result.fontFamily).toEqual([{ label: 'override-family', value: 'serif' }]);
    expect(result.imageSizes).toEqual([{ label: 'override-size', value: 'medium' }]);
    expect(result.colorPalette).toEqual(['#ffffff', '#ff0000']);
  });

  describe('blockMenus.allow (識別子ベース)', () => {
    it('識別子を厳密マッチで残す', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { allow: ['paragraph', 'image'] },
      });

      expect(identifiersOf(result)).toEqual(['paragraph', 'image']);
    });

    it('paragraph (class なし) と paragraph.lead は別エントリとして扱われる', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { allow: ['paragraph.lead'] },
      });

      expect(identifiersOf(result)).toEqual(['paragraph.lead']);
    });

    it('paragraph と paragraph.lead を両方 allow すれば両方残る', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { allow: ['paragraph', 'paragraph.lead'] },
      });

      expect(identifiersOf(result)).toEqual(['paragraph', 'paragraph.lead']);
    });

    it('空 allow は「何も残さない」', () => {
      const base = createBase();

      const result = mergeSettings(base, { blockMenus: { allow: [] } });

      expect(identifiersOf(result)).toEqual([]);
    });

    it('base に存在しない識別子は無視される', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { allow: ['paragraph', 'unknown.identifier'] },
      });

      expect(identifiersOf(result)).toEqual(['paragraph']);
    });
  });

  describe('blockMenus.deny (識別子ベース)', () => {
    it('識別子を厳密マッチで除外する', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { deny: ['table', 'columns2'] },
      });

      expect(identifiersOf(result)).toEqual(['paragraph', 'paragraph.lead', 'heading2', 'image', 'file']);
    });

    it('paragraph (class なし) だけ消しても paragraph.lead は残る', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { deny: ['paragraph'] },
      });

      expect(identifiersOf(result)).toContain('paragraph.lead');
      expect(identifiersOf(result)).not.toContain('paragraph');
    });

    it('paragraph.lead だけ消しても class なし paragraph は残る', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { deny: ['paragraph.lead'] },
      });

      expect(identifiersOf(result)).toContain('paragraph');
      expect(identifiersOf(result)).not.toContain('paragraph.lead');
    });

    it('base に存在しない識別子は黙って無視', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { deny: ['unknown'] },
      });

      expect(identifiersOf(result)).toEqual([
        'paragraph',
        'paragraph.lead',
        'heading2',
        'image',
        'file',
        'table',
        'columns2',
      ]);
    });
  });

  describe('blockMenus.allow と deny の併用', () => {
    it('allow → deny の順で重ね適用される', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: {
          allow: ['paragraph', 'paragraph.lead', 'image'],
          deny: ['paragraph.lead'],
        },
      });

      expect(identifiersOf(result)).toEqual(['paragraph', 'image']);
    });

    it('allow と deny に同じ識別子があれば deny が勝つ', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { allow: ['paragraph', 'image'], deny: ['image'] },
      });

      expect(identifiersOf(result)).toEqual(['paragraph']);
    });
  });

  describe('blockMenus.blocks (完全置換)', () => {
    it('配列を指定すると base / allow / deny を無視して列挙した type だけがメニューになる', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: {
          allow: ['table'], // 無視されるはず
          deny: ['heading2'], // 無視されるはず
          blocks: [
            { type: 'paragraph', label: '本文', group: 'テキスト' },
            { type: 'image', label: '画像', group: 'メディア' },
          ],
        },
      });

      expect(identifiersOf(result)).toEqual(['paragraph', 'image']);
      expect(result.blockMenus.map((g) => g.title)).toEqual(['テキスト', 'メディア']);
    });

    it('label / icon が未指定ならマスター値を使う', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { blocks: [{ type: 'image' }] },
      });

      const command = result.blockMenus[0].commands[0];
      expect(command.label).toBe('画像'); // master 由来
      expect(command.iconName).toBe('photo'); // master 由来
    });

    it('icon の上書きを反映する', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { blocks: [{ type: 'image', icon: 'image' }] },
      });

      expect(result.blockMenus[0].commands[0].iconName).toBe('image');
    });

    it('class を指定すると identifier は type.class 形式になり、class フィールドも反映される', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { blocks: [{ type: 'paragraph', class: 'lead' }] },
      });

      const command = result.blockMenus[0].commands[0];
      expect(command.identifier).toBe('paragraph.lead');
      expect(command.class).toBe('lead');
      expect(command.name).toBe('paragraph');
    });

    it('class を指定しないと identifier は type と同じ', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { blocks: [{ type: 'paragraph' }] },
      });

      const command = result.blockMenus[0].commands[0];
      expect(command.identifier).toBe('paragraph');
      expect(command.class).toBeUndefined();
    });

    it('group 未指定の連続したエントリは直前の group を引き継ぐ', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: {
          blocks: [
            { type: 'paragraph', group: 'テキスト' },
            { type: 'heading2' }, // group 引き継ぎ
            { type: 'image', group: 'メディア' },
            { type: 'file' }, // group 引き継ぎ
          ],
        },
      });

      expect(result.blockMenus.map((g) => g.title)).toEqual(['テキスト', 'メディア']);
      expect(result.blockMenus[0].commands.map((c) => c.name)).toEqual(['paragraph', 'heading2']);
      expect(result.blockMenus[1].commands.map((c) => c.name)).toEqual(['image', 'file']);
    });

    it('最初に group を指定しなかった場合は「その他」グループに入る', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { blocks: [{ type: 'paragraph' }] },
      });

      expect(result.blockMenus[0].title).toBe('その他');
    });

    it('マスターに存在しない type は黙って無視', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: {
          blocks: [
            { type: 'paragraph', group: 'A' },
            { type: 'unknownType', group: 'A' },
          ],
        },
      });

      expect(identifiersOf(result)).toEqual(['paragraph']);
    });

    it('空の blocks 配列を指定するとメニューが空になる (allow/deny は無視)', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: { allow: ['paragraph'], blocks: [] },
      });

      expect(result.blockMenus).toEqual([]);
    });

    it('blocks の配列順がそのまま出力順になる', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: {
          blocks: [
            { type: 'image', group: 'A' },
            { type: 'paragraph', group: 'B' },
            { type: 'heading2', group: 'B' },
          ],
        },
      });

      expect(result.blockMenus.map((g) => g.title)).toEqual(['A', 'B']);
      expect(result.blockMenus[0].commands.map((c) => c.name)).toEqual(['image']);
      expect(result.blockMenus[1].commands.map((c) => c.name)).toEqual(['paragraph', 'heading2']);
    });

    describe('同 type の class 違いを複数並べる', () => {
      it('class が異なる同 type は別エントリとして並列に出力される', () => {
        const base = createBase();

        const result = mergeSettings(base, {
          blockMenus: {
            blocks: [
              { type: 'paragraph', label: '本文', group: 'テキスト' },
              { type: 'paragraph', class: 'lead', label: 'リード', group: 'テキスト' },
              { type: 'paragraph', class: 'note', label: '注釈', group: 'テキスト' },
            ],
          },
        });

        expect(result.blockMenus[0].commands).toHaveLength(3);
        expect(result.blockMenus[0].commands.map((c) => c.identifier)).toEqual([
          'paragraph',
          'paragraph.lead',
          'paragraph.note',
        ]);
        expect(result.blockMenus[0].commands.map((c) => c.label)).toEqual(['本文', 'リード', '注釈']);
      });
    });

    describe('identifier の重複は後勝ち', () => {
      it('同 identifier を 2 回書いたら最後の entry の内容で上書きされる', () => {
        const base = createBase();

        const result = mergeSettings(base, {
          blockMenus: {
            blocks: [
              { type: 'paragraph', label: '段落', group: 'A' },
              { type: 'paragraph', label: '本文', group: 'B' },
            ],
          },
        });

        // 後勝ちで 1 件、group も後勝ち
        expect(result.blockMenus).toHaveLength(1);
        expect(result.blockMenus[0].title).toBe('B');
        expect(result.blockMenus[0].commands).toHaveLength(1);
        expect(result.blockMenus[0].commands[0].label).toBe('本文');
      });

      it('class 違いがあれば identifier が違うので両方残る', () => {
        const base = createBase();

        const result = mergeSettings(base, {
          blockMenus: {
            blocks: [
              { type: 'paragraph', label: '段落', group: 'テキスト' },
              { type: 'paragraph', label: 'リード', class: 'lead', group: 'テキスト' },
            ],
          },
        });

        expect(result.blockMenus[0].commands).toHaveLength(2);
        expect(result.blockMenus[0].commands.map((c) => c.identifier)).toEqual(['paragraph', 'paragraph.lead']);
      });
    });

    it('entry に type が無い・空文字なら黙って除外', () => {
      const base = createBase();

      const result = mergeSettings(base, {
        blockMenus: {
          blocks: [
            { type: 'paragraph', group: 'テキスト' },
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            { type: '' as any, group: 'テキスト' },
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            { label: '不正' } as any,
          ],
        },
      });

      expect(identifiersOf(result)).toEqual(['paragraph']);
    });
  });

  it('blockMenus が空オブジェクトのとき base をそのまま返す', () => {
    const base = createBase();

    const result = mergeSettings(base, { blockMenus: {} });

    expect(result.blockMenus).toEqual(base.blockMenus);
  });

  it('blockMenus に不正な型（配列など）が来たら base を維持する', () => {
    const base = createBase();

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result = mergeSettings(base, { blockMenus: [] as any });

    expect(result.blockMenus).toEqual(base.blockMenus);
  });
});
