import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
  composeFontFamilyStack,
  getDefaultCjkFallback,
  getRecommendedCjkFor,
  splitFontFamilyList,
  CJK_SYSTEM_FALLBACK_STACK,
} from './cjkFonts';

describe('composeFontFamilyStack', () => {
  it('puts the primary font first', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: 'Menlo, monospace',
      userFallback: '',
      latinFontId: 'menlo',
      platform: 'darwin',
    });
    assert.match(stack, /^Menlo,\s*/);
  });

  it('inserts user fallback right after primary when provided', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: '"Fira Code", monospace',
      userFallback: 'Sarasa Mono SC',
      latinFontId: 'fira-code',
      platform: 'darwin',
    });
    const firaIdx = stack.indexOf('Fira Code');
    const userIdx = stack.indexOf('Sarasa Mono SC');
    assert.ok(firaIdx >= 0 && userIdx > firaIdx, 'user fallback after primary');
  });

  it('uses per-Latin-font recommended CJK when user fallback is empty', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: '"Cascadia Code", monospace',
      userFallback: '',
      latinFontId: 'cascadia-code',
      platform: 'win32',
    });
    // Cascadia Code now recommends Sarasa Mono SC (true monospace).
    assert.match(stack, /Sarasa Mono SC/);
  });

  it('falls back to OS default when Latin font has no recommendation', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: '"Unknown Font", monospace',
      userFallback: '',
      latinFontId: 'unknown',
      platform: 'darwin',
    });
    // macOS no-recommendation default is now Sarasa Mono SC (bundled).
    assert.match(stack, /Sarasa Mono SC/);
  });

  it('quotes multi-word user fallback names', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: 'Menlo, monospace',
      userFallback: 'Source Han Mono SC',
      latinFontId: 'menlo',
      platform: 'linux',
    });
    assert.match(stack, /"Source Han Mono SC"/);
  });

  it('does not duplicate identical fallback entries', () => {
    // User explicitly picks the same font the per-font pairing would,
    // and that font also lives in the system stack — should appear once.
    const stack = composeFontFamilyStack({
      primaryFamily: '"Cascadia Code", monospace',
      userFallback: 'Sarasa Mono SC',
      latinFontId: 'cascadia-code',
      platform: 'win32',
    });
    const matches = stack.match(/Sarasa Mono SC/g) || [];
    assert.equal(matches.length, 1);
  });

  it('inserts JetBrains Mono as Latin-only fallback right after the primary family', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: 'Menlo',
      userFallback: '',
      latinFontId: 'menlo',
      platform: 'darwin',
    });
    const families = stack.split(',').map((s) => s.trim().replace(/^"|"$/g, ''));
    assert.equal(families[0], 'Menlo');
    assert.equal(families[1], 'JetBrains Mono');
  });

  it('Latin fallback (JetBrains Mono) precedes every CJK family', () => {
    // Regression guard for codex P1 review on PR #940 (first round):
    // when the primary font isn't installed, Latin glyphs must fall to
    // a Latin-only monospace face — NOT a CJK font's full-width Latin
    // variant — to keep xterm's fixed cell grid aligned. JetBrains Mono
    // is bundled via @fontsource and contains no CJK glyphs, so it
    // catches Latin while letting CJK glyphs flow past.
    const stack = composeFontFamilyStack({
      primaryFamily: '"Fira Code", monospace',
      userFallback: 'LXGW WenKai Mono',
      latinFontId: 'fira-code',
      platform: 'darwin',
    });
    const jbIdx = stack.indexOf('JetBrains Mono');
    const sarasaIdx = stack.indexOf('Sarasa Mono SC');
    const userFallbackIdx = stack.indexOf('LXGW WenKai Mono');
    const simSunIdx = stack.indexOf('SimSun');
    assert.ok(jbIdx > 0, 'JetBrains Mono must appear in the stack');
    assert.ok(jbIdx < userFallbackIdx, 'JetBrains Mono before user CJK');
    assert.ok(jbIdx < sarasaIdx, 'JetBrains Mono before Sarasa system fallback');
    assert.ok(jbIdx < simSunIdx, 'JetBrains Mono before SimSun system fallback');
  });

  it('preserves a quoted primary family name that contains a comma', () => {
    // Regression guard for codex P2 review on PR #940: when the primary
    // family is something like `"Foo, Inc. Mono"`, the composed stack
    // must keep that token intact rather than splitting on the internal
    // comma and emitting fragmented pieces.
    const stack = composeFontFamilyStack({
      primaryFamily: '"Foo, Inc. Mono", monospace',
      userFallback: '',
      latinFontId: 'foo-inc-mono',
      platform: 'darwin',
    });
    assert.ok(
      stack.includes('"Foo, Inc. Mono"'),
      'quoted family with comma stays a single token',
    );
    assert.ok(
      !stack.includes('"Foo,') || stack.includes('"Foo, Inc. Mono"'),
      'must not produce a dangling `"Foo,` fragment',
    );
  });

  it('user-chosen CJK fallback precedes generic monospace', () => {
    // Regression guard for codex P1 review on PR #940 (second round):
    // generic `monospace` on macOS Chrome resolves Chinese glyphs to
    // PingFang via Chromium's CJK system fallback. If `monospace`
    // appeared in the chain BEFORE the user's CJK pick, CSS per-glyph
    // fallback would stop at monospace for CJK characters and never
    // consult the user's choice, silently nullifying the CJK picker.
    const stack = composeFontFamilyStack({
      primaryFamily: '"Fira Code", monospace',
      userFallback: 'LXGW WenKai Mono',
      latinFontId: 'fira-code',
      platform: 'darwin',
    });
    const userFallbackIdx = stack.indexOf('LXGW WenKai Mono');
    // Match `monospace` as a standalone token (after the comma+space).
    const monospaceIdx = stack.lastIndexOf(', monospace');
    assert.ok(userFallbackIdx > 0, 'user CJK must appear');
    assert.ok(monospaceIdx > userFallbackIdx, 'user CJK must come before generic monospace');
  });

  it('explicit user fallback overrides the per-font recommendation', () => {
    const stack = composeFontFamilyStack({
      primaryFamily: '"JetBrains Mono", monospace',
      userFallback: 'LXGW WenKai Mono',
      latinFontId: 'jetbrains-mono',
      platform: 'darwin',
    });
    // User chose LXGW WenKai Mono; the JetBrains Mono recommendation
    // (Sarasa Mono SC) should be suppressed, so Sarasa only shows up
    // later in the system fallback stack, AFTER the user choice.
    const userIdx = stack.indexOf('LXGW WenKai Mono');
    const sarasaIdx = stack.indexOf('Sarasa Mono SC');
    assert.ok(userIdx >= 0);
    assert.ok(sarasaIdx > userIdx, 'system Sarasa appears after explicit user choice');
  });
});

describe('getDefaultCjkFallback', () => {
  it('returns SimSun on Windows (always installed, monospace)', () => {
    assert.equal(getDefaultCjkFallback('win32'), 'SimSun');
  });
  it('returns Sarasa Mono SC on macOS (bundled by app)', () => {
    assert.equal(getDefaultCjkFallback('darwin'), 'Sarasa Mono SC');
  });
  it('returns Noto Sans Mono CJK SC on Linux', () => {
    assert.equal(getDefaultCjkFallback('linux'), 'Noto Sans Mono CJK SC');
  });
  it('never returns a known proportional font', () => {
    const proportional = ['PingFang SC', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Hiragino Sans GB'];
    for (const platform of ['darwin', 'win32', 'linux'] as const) {
      const v = getDefaultCjkFallback(platform);
      assert.ok(!proportional.includes(v), `${platform} default ${v} must not be proportional`);
    }
  });
});

describe('getRecommendedCjkFor', () => {
  it('returns null for unknown fonts', () => {
    assert.equal(getRecommendedCjkFor('unknown-font-id', 'darwin'), null);
  });
  it('returns a non-empty string for known fonts', () => {
    const v = getRecommendedCjkFor('jetbrains-mono', 'darwin');
    assert.ok(v && v.length > 0);
  });
});

describe('splitFontFamilyList', () => {
  it('splits a simple comma-separated list', () => {
    assert.deepEqual(
      splitFontFamilyList('Menlo, monospace'),
      ['Menlo', 'monospace'],
    );
  });

  it('keeps quoted family names with commas intact', () => {
    // Regression guard for codex P2 review on PR #940: a font family
    // name like `"Foo, Inc. Mono"` is a single token in CSS, not two.
    assert.deepEqual(
      splitFontFamilyList('"Foo, Inc. Mono", monospace'),
      ['"Foo, Inc. Mono"', 'monospace'],
    );
  });

  it('handles a single unquoted name', () => {
    assert.deepEqual(splitFontFamilyList('Iosevka'), ['Iosevka']);
  });

  it('handles single quotes too', () => {
    assert.deepEqual(
      splitFontFamilyList("'Foo, Inc.', serif"),
      ["'Foo, Inc.'", 'serif'],
    );
  });

  it('drops empty segments produced by double commas', () => {
    assert.deepEqual(
      splitFontFamilyList('Menlo,, monospace'),
      ['Menlo', 'monospace'],
    );
  });
});

describe('CJK_SYSTEM_FALLBACK_STACK', () => {
  it('contains true-monospace CJK fonts only', () => {
    assert.match(CJK_SYSTEM_FALLBACK_STACK, /Sarasa Mono SC/);
    assert.match(CJK_SYSTEM_FALLBACK_STACK, /Noto Sans Mono CJK SC/);
    assert.match(CJK_SYSTEM_FALLBACK_STACK, /SimSun/);
  });

  it('does not include known proportional CJK fonts', () => {
    assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /PingFang SC/);
    assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /Microsoft YaHei UI/);
    assert.doesNotMatch(CJK_SYSTEM_FALLBACK_STACK, /Hiragino Sans GB/);
  });
});
