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

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

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

describe('textWrap search-engine-app', () => {
  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,
      lineMode: 'AUTO' as const,
      measure: {
        contextProfile: 'APP|ios|test',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
      },
      scoring: {
        config: {
          weights: DEFAULT_WEIGHTS,
          idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
          ellipsisToken: '…',
          tcParticleWhitelist: [],
        },
        lexicons: DEFAULT_LEXICONS,
        debug: true,
      },
    };

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

    expect(a).toEqual(b);
    expect(b).toEqual(c);
    expect(impl).toHaveBeenCalled(); // 有测量发生
  });

  it('lineMode=FIXED：无解必须返回 NO_CANDIDATE', async () => {
    const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
    const res = await searchBestLayoutApp({
      tokens: [t('I', 0), t('am', 2)],
      lang: 'EN',
      breakpoints: [{ pos: 1, kind: 'SPACE', priority: 10 }],
      availableWidth: 100,
      maxLines: 3,
      lineMode: 'FIXED',
      measure: {
        contextProfile: 'APP|ios|fixed',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
      },
      scoring: {
        config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
        lexicons: DEFAULT_LEXICONS,
      },
    });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('NO_CANDIDATE');
  });

  it('TC H6：断点导致下一行行首为标点时必须禁止', async () => {
    const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
    const res = await searchBestLayoutApp({
      tokens: [t('我', 0), t('好', 1), t('，', 2), t('累', 3)],
      lang: 'TC',
      // 若选择 pos=2，则第二行行首 token 为 '，'（应禁止）
      breakpoints: [{ pos: 2, kind: 'PUNCT', priority: 30 }],
      availableWidth: 100,
      maxLines: 2,
      lineMode: 'FIXED',
      measure: {
        contextProfile: 'APP|ios|tc',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
      },
      scoring: {
        config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [], tcPunctuations: ['，'] },
        lexicons: DEFAULT_LEXICONS,
      },
    });

    // lineMode=FIXED 且唯一断点被禁止，因此无解
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('NO_CANDIDATE');
  });

  it('TOO_LONG：超过阈值直接返回 TOO_LONG', async () => {
    const impl: MeasureWidthImpl = vi.fn(async ({ text }) => text.length);
    const tokens = Array.from({ length: 31 }).map((_, i) => t('a', i));
    const res = await searchBestLayoutApp({
      tokens,
      lang: 'EN',
      breakpoints: [],
      availableWidth: 100,
      maxLines: 2,
      lineMode: 'AUTO',
      measure: {
        contextProfile: 'APP|ios|toolong',
        fontSpec: { fontFamily: 'PingFangSC-Regular', fontWeight: '400', fontSize: 16 },
        measureWidthImpl: impl,
      },
      scoring: {
        config: { weights: DEFAULT_WEIGHTS, idealWidthRatio: { APP: 0.9, WIDGET: 0.95 }, ellipsisToken: '…', tcParticleWhitelist: [] },
        lexicons: DEFAULT_LEXICONS,
      },
    });
    expect(res.ok).toBe(false);
    if (!res.ok) expect(res.reason).toBe('TOO_LONG');
  });
});

