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

import type { MeasureWidthImpl } from '../../measure/types';
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from '../../scoring/index';
import { searchBestLayoutWidget } from '../index';

function t(text: string, start: number) {
  return { text, start, end: start + text.length };
}

describe('textWrap search-engine-widget', () => {
  it('确定性：同输入多次调用 breaks/lines 必须一致', async () => {
    const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
    const input = {
      tokens: [t('I', 0), t('am', 2), t('so', 5), t('tired', 8)],
      lang: 'EN' as const,
      breakpoints: [
        { pos: 1, kind: 'SPACE', priority: 10 },
        { pos: 2, kind: 'SPACE', priority: 10 },
        { pos: 3, kind: 'SPACE', priority: 10 },
      ],
      availableWidth: 6,
      maxLines: 2,
      context: 'WIDGET' as const,
      measure: {
        widthMode: 'MEASURE' as const,
        contextProfile: 'WIDGET|small|test',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
        widgetEnableMeasure: true,
      },
      scoring: {
        config: {
          weights: DEFAULT_WEIGHTS,
          idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
          ellipsisToken: '…',
          tcParticleWhitelist: [],
        },
        lexicons: DEFAULT_LEXICONS,
        debug: true,
      },
      config: { beamK: 5, expandM: 12 },
    };

    const a = await searchBestLayoutWidget(input as any);
    const b = await searchBestLayoutWidget(input as any);
    const c = await searchBestLayoutWidget(input as any);

    expect(a).toEqual(b);
    expect(b).toEqual(c);
  });

  it('widthMode=APPROX：仍可输出，并标记 meta.reason=WIDTH_UNKNOWN', async () => {
    const res = await searchBestLayoutWidget({
      tokens: [t('我', 0), t('好', 1), t('累', 2)],
      lang: 'TC',
      breakpoints: [{ pos: 1, kind: 'BALANCE', priority: 5 }],
      // APPROX 模式下：availableWidth 与 width 单位都按“token 数”理解
      availableWidth: 10,
      maxLines: 2,
      context: 'WIDGET',
      measure: { widthMode: 'APPROX' },
      scoring: {
        config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
        lexicons: DEFAULT_LEXICONS,
        debug: true,
      },
      config: { beamK: 3, expandM: 2 },
    });

    expect(res.ok).toBe(true);
    if (res.ok) {
      expect(res.bestLayout.meta?.reason).toBe('WIDTH_UNKNOWN');
    }
  });

  it('性能约束：expandM/beamK 生效（通过测量调用次数粗略验证）', async () => {
    const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
    const res = await searchBestLayoutWidget({
      tokens: [t('a', 0), t('b', 2), t('c', 4), t('d', 6), t('e', 8)],
      lang: 'EN',
      breakpoints: [
        { pos: 1, kind: 'SPACE', priority: 10 },
        { pos: 2, kind: 'SPACE', priority: 10 },
        { pos: 3, kind: 'SPACE', priority: 10 },
        { pos: 4, kind: 'SPACE', priority: 10 },
      ],
      availableWidth: 100,
      maxLines: 3,
      context: 'WIDGET',
      measure: {
        widthMode: 'MEASURE',
        contextProfile: 'WIDGET|perf',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
        widgetEnableMeasure: true,
      },
      scoring: {
        config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
        lexicons: DEFAULT_LEXICONS,
      },
      config: { beamK: 2, expandM: 2 },
    });

    expect(res.ok).toBe(true);
    // 每轮最多 beamK 个 beam，每个 beam 最多 expandM 次扩展 -> 粗略上限：maxLines*beamK*expandM
    expect((impl as any).mock.calls.length).toBeLessThanOrEqual(3 * 2 * 2 + 2);
  });

  it('TC H6：行首标点导致的转移必须被禁止', async () => {
    const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
    const res = await searchBestLayoutWidget({
      tokens: [t('我', 0), t('好', 1), t('，', 2), t('累', 3)],
      lang: 'TC',
      breakpoints: [{ pos: 2, kind: 'PUNCT', priority: 30 }],
      // 让“一行放下全文”不可能（触发超宽过滤），从而必须依赖断点切分
      availableWidth: 3,
      maxLines: 2,
      context: 'WIDGET',
      measure: {
        widthMode: 'MEASURE',
        contextProfile: 'WIDGET|tc',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
        widgetEnableMeasure: true,
      },
      scoring: {
        config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [], tcPunctuations: ['，'] },
        lexicons: DEFAULT_LEXICONS,
      },
      config: { beamK: 3, expandM: 3 },
    });

    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('NO_CANDIDATE');
  });
});

