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

vi.mock('@/utils/misc', async (importOriginal) => {
  const actual = await importOriginal<Record<string, unknown>>();
  return {
    ...actual,
    getOSPlatform: vi.fn(() => 'macos' as const),
  };
});

import { getStyles, ThemeCode } from '@/utils/style';
import { CustomFont } from '@/styles/fonts';
import { ViewSettings } from '@/types/book';
import {
  DEFAULT_BOOK_FONT,
  DEFAULT_BOOK_LAYOUT,
  DEFAULT_BOOK_LANGUAGE,
  DEFAULT_BOOK_STYLE,
  DEFAULT_VIEW_CONFIG,
  DEFAULT_TTS_CONFIG,
  DEFAULT_TRANSLATOR_CONFIG,
  DEFAULT_ANNOTATOR_CONFIG,
  DEFAULT_SCREEN_CONFIG,
} from '@/services/constants';

/** Build a full ViewSettings from all DEFAULT_* constants, with overrides. */
function makeViewSettings(overrides: Partial<ViewSettings> = {}): ViewSettings {
  return {
    ...DEFAULT_BOOK_FONT,
    ...DEFAULT_BOOK_LAYOUT,
    ...DEFAULT_BOOK_LANGUAGE,
    ...DEFAULT_BOOK_STYLE,
    ...DEFAULT_VIEW_CONFIG,
    ...DEFAULT_TTS_CONFIG,
    ...DEFAULT_TRANSLATOR_CONFIG,
    ...DEFAULT_ANNOTATOR_CONFIG,
    ...DEFAULT_SCREEN_CONFIG,
    ...overrides,
  } as ViewSettings;
}

/** Build a ThemeCode for testing. */
function makeThemeCode(overrides: Partial<ThemeCode> = {}): ThemeCode {
  return {
    bg: '#ffffff',
    fg: '#000000',
    primary: '#3366cc',
    isDarkMode: false,
    palette: {
      'base-100': '#ffffff',
      'base-200': '#f0f0f0',
      'base-300': '#e0e0e0',
      'base-content': '#000000',
      neutral: '#808080',
      'neutral-content': '#ffffff',
      primary: '#3366cc',
      secondary: '#6699cc',
      accent: '#33cc99',
    },
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// getFontStyles branches
// ---------------------------------------------------------------------------
describe('getFontStyles branches (via getStyles)', () => {
  const theme = makeThemeCode();

  it('uses --serif variable when defaultFont is "Serif"', () => {
    const vs = makeViewSettings({ defaultFont: 'Serif' });
    const css = getStyles(vs, theme);
    expect(css).toContain('font-family: var(--serif)');
  });

  it('uses --sans-serif variable when defaultFont is "Sans-serif"', () => {
    const vs = makeViewSettings({ defaultFont: 'Sans-serif' });
    const css = getStyles(vs, theme);
    expect(css).toContain('font-family: var(--sans-serif)');
  });

  it('adds !important when overrideFont is true', () => {
    const vs = makeViewSettings({ overrideFont: true, defaultFont: 'Serif' });
    const css = getStyles(vs, theme);
    expect(css).toContain('font-family: var(--serif) !important');
    // The body block should also have the font-family !important
    expect(css).toContain('font-family: revert !important');
  });

  it('does not add !important when overrideFont is false', () => {
    const vs = makeViewSettings({ overrideFont: false, defaultFont: 'Serif' });
    const css = getStyles(vs, theme);
    // The html rule should NOT have !important after the var
    expect(css).toMatch(/font-family: var\(--serif\)\s*[^!]/);
    // And body block should not have font-family at all
    expect(css).not.toContain('font-family: revert !important');
  });

  it('sets font-size according to defaultFontSize', () => {
    const vs = makeViewSettings({ defaultFontSize: 20 });
    const css = getStyles(vs, theme);
    // On macos, fontScale is 1, zoomLevel defaults to 100 so zoomScale is 1
    expect(css).toContain('font-size: 20px !important');
  });

  it('sets minimum font-size via --min-font-size', () => {
    const vs = makeViewSettings({ minimumFontSize: 10 });
    const css = getStyles(vs, theme);
    expect(css).toContain('--min-font-size: 10px');
  });

  it('sets font-weight', () => {
    const vs = makeViewSettings({ fontWeight: 700 });
    const css = getStyles(vs, theme);
    expect(css).toContain('--font-weight: 700');
    expect(css).toContain('font-weight: 700');
  });

  it('includes CJK font in serif font list when defaultCJKFont differs from serifFont', () => {
    const vs = makeViewSettings({
      serifFont: 'Bitter',
      defaultCJKFont: 'Source Han Serif CN',
    });
    const css = getStyles(vs, theme);
    expect(css).toContain('"Source Han Serif CN"');
  });

  it('does not duplicate CJK font in serif list when it equals serifFont', () => {
    const vs = makeViewSettings({
      serifFont: 'LXGW WenKai GB Screen',
      defaultCJKFont: 'LXGW WenKai GB Screen',
    });
    const css = getStyles(vs, theme);
    // The font should appear only once in the --serif declaration
    const match = css.match(/--serif:([^;]+);/);
    expect(match).not.toBeNull();
    const serifDecl = match![1]!;
    const occurrences = serifDecl.split('"LXGW WenKai GB Screen"').length - 1;
    expect(occurrences).toBe(1);
  });

  it('includes CJK font in sans-serif list when defaultCJKFont differs from sansSerifFont', () => {
    const vs = makeViewSettings({
      sansSerifFont: 'Roboto',
      defaultCJKFont: 'Noto Sans SC',
    });
    const css = getStyles(vs, theme);
    const match = css.match(/--sans-serif:([^;]+);/);
    expect(match).not.toBeNull();
    expect(match![1]).toContain('"Noto Sans SC"');
  });

  it('does not duplicate CJK font in sans-serif list when it equals sansSerifFont', () => {
    const vs = makeViewSettings({
      sansSerifFont: 'Noto Sans SC',
      defaultCJKFont: 'Noto Sans SC',
    });
    const css = getStyles(vs, theme);
    const match = css.match(/--sans-serif:([^;]+);/);
    expect(match).not.toBeNull();
    const sansDecl = match![1]!;
    const occurrences = sansDecl.split('"Noto Sans SC"').length - 1;
    expect(occurrences).toBe(1);
  });

  it('puts monospace font first in the monospace list', () => {
    const vs = makeViewSettings({ monospaceFont: 'Fira Code' });
    const css = getStyles(vs, theme);
    const match = css.match(/--monospace:\s*([^;]+);/);
    expect(match).not.toBeNull();
    expect(match![1]!.trimStart().startsWith('"Fira Code"')).toBe(true);
  });

  it('applies zoomLevel scaling to font size', () => {
    const vs = makeViewSettings({ defaultFontSize: 16, zoomLevel: 200 });
    const css = getStyles(vs, theme);
    // 16 * 1 (fontScale on macos) * (200/100) = 32
    expect(css).toContain('font-size: 32px !important');
  });

  it('includes font[size] rules for all sizes 1-7', () => {
    const vs = makeViewSettings({ defaultFontSize: 16, minimumFontSize: 8 });
    const css = getStyles(vs, theme);
    expect(css).toContain('font[size="1"]');
    expect(css).toContain('font[size="7"]');
  });
});

// ---------------------------------------------------------------------------
// getLayoutStyles branches
// ---------------------------------------------------------------------------
describe('getLayoutStyles branches (via getStyles)', () => {
  const theme = makeThemeCode();

  it('sets text-align to justify when fullJustification is true', () => {
    const vs = makeViewSettings({ fullJustification: true });
    const css = getStyles(vs, theme);
    expect(css).toContain('--default-text-align: justify');
  });

  it('sets text-align to start when fullJustification is false', () => {
    const vs = makeViewSettings({ fullJustification: false });
    const css = getStyles(vs, theme);
    expect(css).toContain('--default-text-align: start');
  });

  it('sets margin CSS variables from viewSettings', () => {
    const vs = makeViewSettings({
      marginTopPx: 50,
      marginRightPx: 30,
      marginBottomPx: 40,
      marginLeftPx: 20,
    });
    const css = getStyles(vs, theme);
    expect(css).toContain('--margin-top: 50px');
    expect(css).toContain('--margin-right: 30px');
    expect(css).toContain('--margin-bottom: 40px');
    expect(css).toContain('--margin-left: 20px');
  });

  it('sets hyphens to auto when hyphenation is true', () => {
    const vs = makeViewSettings({ hyphenation: true });
    const css = getStyles(vs, theme);
    expect(css).toContain('hyphens: auto');
    expect(css).toContain('-webkit-hyphens: auto');
  });

  it('sets hyphens to manual when hyphenation is false', () => {
    const vs = makeViewSettings({ hyphenation: false });
    const css = getStyles(vs, theme);
    expect(css).toContain('hyphens: manual');
    expect(css).toContain('-webkit-hyphens: manual');
  });

  it('adds !important to layout properties when overrideLayout is true', () => {
    const vs = makeViewSettings({ overrideLayout: true, lineHeight: 1.6 });
    const css = getStyles(vs, theme);
    expect(css).toContain('line-height: 1.6 !important');
  });

  it('does not add !important to layout properties when overrideLayout is false', () => {
    const vs = makeViewSettings({ overrideLayout: false, lineHeight: 1.6 });
    const css = getStyles(vs, theme);
    // Should have line-height: 1.6 without !important after it
    expect(css).toMatch(/line-height: 1\.6\s*[^!]/);
  });

  it('sets word-spacing and letter-spacing', () => {
    const vs = makeViewSettings({ wordSpacing: 2, letterSpacing: 1 });
    const css = getStyles(vs, theme);
    expect(css).toContain('word-spacing: 2px');
    expect(css).toContain('letter-spacing: 1px');
  });

  it('sets text-indent', () => {
    const vs = makeViewSettings({ textIndent: 2 });
    const css = getStyles(vs, theme);
    expect(css).toContain('text-indent: 2em');
  });

  it('sets paragraph margins vertically (margin-left/right) when vertical is true', () => {
    const vs = makeViewSettings({ vertical: true, paragraphMargin: 1.0 });
    const css = getStyles(vs, theme);
    expect(css).toContain('margin-left: 1em');
    expect(css).toContain('margin-right: 1em');
    expect(css).toContain('margin-top: unset');
    expect(css).toContain('margin-bottom: unset');
  });

  it('sets paragraph margins horizontally (margin-top/bottom) when vertical is false', () => {
    const vs = makeViewSettings({ vertical: false, paragraphMargin: 0.8 });
    const css = getStyles(vs, theme);
    expect(css).toContain('margin-top: 0.8em');
    expect(css).toContain('margin-bottom: 0.8em');
    expect(css).toContain('margin-left: unset');
    expect(css).toContain('margin-right: unset');
  });

  it('sets div margins with !important when vertical and overrideLayout are both true', () => {
    const vs = makeViewSettings({
      vertical: true,
      overrideLayout: true,
      paragraphMargin: 0.5,
    });
    const css = getStyles(vs, theme);
    expect(css).toContain('margin-left: 0.5em !important');
    expect(css).toContain('margin-right: 0.5em !important');
  });

  it('sets div margins with !important when vertical is false and overrideLayout is true', () => {
    const vs = makeViewSettings({
      vertical: false,
      overrideLayout: true,
      paragraphMargin: 0.5,
    });
    const css = getStyles(vs, theme);
    expect(css).toContain('margin-top: 0.5em !important');
    expect(css).toContain('margin-bottom: 0.5em !important');
  });

  it('does not set div margins when overrideLayout is false', () => {
    const vs = makeViewSettings({
      vertical: false,
      overrideLayout: false,
      paragraphMargin: 0.5,
    });
    const css = getStyles(vs, theme);
    // The div block should not have paragraph margin !important
    // (the template literals produce empty strings when conditions are false)
    expect(css).not.toMatch(/div\s*\{[^}]*margin-top:\s*0\.5em\s*!important/);
  });

  it('applies writing-mode when not auto', () => {
    const vs = makeViewSettings({ writingMode: 'vertical-rl' });
    const css = getStyles(vs, theme);
    expect(css).toContain('writing-mode: vertical-rl !important');
  });

  it('does not set writing-mode when it is auto', () => {
    const vs = makeViewSettings({ writingMode: 'auto' });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('writing-mode: auto');
    expect(css).not.toContain('writing-mode: vertical');
    expect(css).not.toContain('writing-mode: horizontal');
  });

  it('applies justify override for aligned-left when justify and overrideLayout are true', () => {
    const vs = makeViewSettings({ fullJustification: true, overrideLayout: true });
    const css = getStyles(vs, theme);
    expect(css).toContain('text-align: justify !important');
  });

  it('applies initial override for aligned-justify when justify is false and overrideLayout is true', () => {
    const vs = makeViewSettings({ fullJustification: false, overrideLayout: true });
    const css = getStyles(vs, theme);
    expect(css).toContain('text-align: initial !important');
  });

  it('applies img.pi vertical transforms when vertical is true', () => {
    const vs = makeViewSettings({ vertical: true, lineHeight: 1.5 });
    const css = getStyles(vs, theme);
    expect(css).toContain('transform: rotate(90deg)');
    expect(css).toContain('transform-origin: center');
    expect(css).toContain('height: 2em');
    expect(css).toContain('width: 1.5em');
  });

  it('does not apply img.pi vertical transforms when vertical is false', () => {
    const vs = makeViewSettings({ vertical: false });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('transform: rotate(90deg)');
  });

  it('sets img.has-text-siblings width when vertical', () => {
    const vs = makeViewSettings({ vertical: true });
    const css = getStyles(vs, theme);
    expect(css).toMatch(/img\.has-text-siblings\s*\{[^}]*width:\s*1em/);
  });

  it('sets img.has-text-siblings height when not vertical', () => {
    const vs = makeViewSettings({ vertical: false });
    const css = getStyles(vs, theme);
    expect(css).toMatch(/img\.has-text-siblings\s*\{[^}]*height:\s*1em/);
  });

  it('includes zoom: 1 when zoomLevel is 100 (passed as 1.0)', () => {
    const vs = makeViewSettings({ zoomLevel: 100 });
    const css = getStyles(vs, theme);
    // getStyles passes 1.0 as zoomLevel to getLayoutStyles
    expect(css).toContain('zoom: 1');
  });

  it('omits only paragraph-related layout rules when useBookLayout is true', () => {
    const vs = makeViewSettings({
      useBookLayout: true,
      lineHeight: 1.7,
      wordSpacing: 3,
      letterSpacing: 2,
      textIndent: 2,
      paragraphMargin: 1.25,
      fullJustification: true,
      hyphenation: true,
      marginTopPx: 50,
    });
    const css = getStyles(vs, theme);
    // paragraph-specific tokens driven by the Paragraph section must be absent
    expect(css).not.toContain('--default-text-align:');
    expect(css).not.toContain('line-height: 1.7');
    expect(css).not.toContain('word-spacing: 3px');
    expect(css).not.toContain('letter-spacing: 2px');
    expect(css).not.toContain('text-indent: 2em');
    expect(css).not.toContain('hyphens: auto');
    expect(css).not.toContain('-webkit-hyphens: auto');
    // non-paragraph layout rules must still be emitted
    expect(css).toContain('@namespace epub');
    expect(css).toContain('--margin-top: 50px');
    expect(css).toContain('--margin-right:');
    expect(css).toContain('--margin-bottom:');
    expect(css).toContain('--margin-left:');
    // font/color/translation sections must still be present
    expect(css).toContain('--serif:');
    expect(css).toContain('--theme-bg-color');
    expect(css).toContain('.translation-source');
  });

  it('includes paragraph layout rules when useBookLayout is false', () => {
    const vs = makeViewSettings({
      useBookLayout: false,
      lineHeight: 1.7,
      wordSpacing: 3,
      letterSpacing: 2,
      textIndent: 2,
      fullJustification: true,
      hyphenation: true,
    });
    const css = getStyles(vs, theme);
    expect(css).toContain('--default-text-align: justify');
    expect(css).toContain('line-height: 1.7');
    expect(css).toContain('word-spacing: 3px');
    expect(css).toContain('letter-spacing: 2px');
    expect(css).toContain('text-indent: 2em');
    expect(css).toContain('hyphens: auto');
  });
});

// ---------------------------------------------------------------------------
// getColorStyles branches
// ---------------------------------------------------------------------------
describe('getColorStyles branches (via getStyles)', () => {
  it('sets color-scheme to light when isDarkMode is false', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    expect(css).toContain('color-scheme: light');
  });

  it('sets color-scheme to dark when isDarkMode is true', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('color-scheme: dark');
  });

  it('sets theme CSS variables', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ bg: '#fafafa', fg: '#111111', primary: '#0055cc' });
    const css = getStyles(vs, theme);
    expect(css).toContain('--theme-bg-color: #fafafa');
    expect(css).toContain('--theme-fg-color: #111111');
    expect(css).toContain('--theme-primary-color: #0055cc');
  });

  it('forces background-color and color on elements when overrideColor is true', () => {
    const vs = makeViewSettings({ overrideColor: true });
    const theme = makeThemeCode({ bg: '#fff', fg: '#000' });
    const css = getStyles(vs, theme);
    expect(css).toContain('background-color: #fff !important');
    expect(css).toContain('color: #000 !important');
    expect(css).toContain('border-color: #000 !important');
  });

  it('does not force color overrides when overrideColor is false', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ bg: '#fff', fg: '#000' });
    const css = getStyles(vs, theme);
    // Should not have !important color override on section/div/p etc
    expect(css).not.toMatch(/section,.*\{[^}]*color: #000 !important/);
  });

  it('applies invert filter on images when isDarkMode and invertImgColorInDark are true', () => {
    const vs = makeViewSettings({ invertImgColorInDark: true });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('filter: invert(100%)');
  });

  it('does not apply invert filter when isDarkMode is false', () => {
    const vs = makeViewSettings({ invertImgColorInDark: true });
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('filter: invert(100%)');
  });

  it('does not apply invert filter when invertImgColorInDark is false', () => {
    const vs = makeViewSettings({ invertImgColorInDark: false });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('filter: invert(100%)');
  });

  it('applies mix-blend-mode multiply on img when not dark and overrideColor is true', () => {
    const vs = makeViewSettings({ overrideColor: true });
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    expect(css).toContain('mix-blend-mode: multiply');
  });

  it('does not apply mix-blend-mode multiply when overrideColor is false and not dark', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    // mix-blend-mode: multiply on img should not appear; there's one for hr.background-img and
    // has-text-siblings (which is always present), but not in the img block
    expect(css).not.toMatch(/^\s*img\s*\{[^}]*mix-blend-mode: multiply/m);
  });

  it('sets bg-texture-id CSS variable', () => {
    const vs = makeViewSettings({ backgroundTextureId: 'paper' });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).toContain('--bg-texture-id: paper');
  });

  it('sets bg-texture-id to none', () => {
    const vs = makeViewSettings({ backgroundTextureId: 'none' });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).toContain('--bg-texture-id: none');
  });

  it('includes eink selection styles when isEink is true', () => {
    const vs = makeViewSettings({ isEink: true });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).toContain('::selection');
    expect(css).toContain('::-moz-selection');
    expect(css).toContain('background: var(--theme-fg-color)');
  });

  it('does not include eink selection styles when isEink is false', () => {
    const vs = makeViewSettings({ isEink: false });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).not.toContain('::selection');
    expect(css).not.toContain('::-moz-selection');
  });

  it('sets text-decoration to underline for links when isEink is true', () => {
    const vs = makeViewSettings({ isEink: true });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).toContain('text-decoration: underline');
  });

  it('sets text-decoration to none for links when isEink is false', () => {
    const vs = makeViewSettings({ isEink: false });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).toContain('text-decoration: none');
  });

  it('forces eink background on body when isEink is true', () => {
    const vs = makeViewSettings({ isEink: true });
    const theme = makeThemeCode({ bg: '#ffffff' });
    const css = getStyles(vs, theme);
    expect(css).toContain('background-color: #ffffff !important');
  });

  it('sets body.pbg background in dark mode', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toMatch(/body\.pbg\s*\{[^}]*background-color:\s*#1a1a1a\s*!important/);
  });

  it('does not set body.pbg background in light mode', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    // body.pbg block should be empty or absent
    expect(css).not.toMatch(/body\.pbg\s*\{[^}]*background-color:[^}]*!important/);
  });

  it('overrides inline white backgrounds in dark mode when overrideColor is false', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('background-color: #1a1a1a !important');
    expect(css).toContain('background-color: #fff"]');
    expect(css).toContain('body.theme-dark');
  });

  it('keeps body.theme-dark transparent in dark mode so the host background texture is not occluded (#4446)', () => {
    const vs = makeViewSettings({ overrideColor: false, backgroundTextureId: 'leaves' });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toMatch(/body\.theme-dark\s*\{\s*background-color: transparent !important;/);
    expect(css).not.toMatch(/body\.theme-dark\s*\{\s*background-color: #1a1a1a !important/);
    // #4392 inline light-callout overrides must keep forcing the theme bg
    expect(css).toContain('background-color: #fff"]');
    expect(css).toContain('background-color: #1a1a1a !important');
  });

  it('keeps body.theme-dark transparent even without a texture (docBackground is captured once per section load)', () => {
    const vs = makeViewSettings({ overrideColor: false, backgroundTextureId: 'none' });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toMatch(/body\.theme-dark\s*\{\s*background-color: transparent !important;/);
  });

  it('does not add inline white background overrides in light mode', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('background-color: #fff"]');
  });

  it('applies dark mode link color lightblue when overrideColor is false', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('color: lightblue');
  });

  it('applies primary color on links when overrideColor is true', () => {
    const vs = makeViewSettings({ overrideColor: true });
    const theme = makeThemeCode({
      isDarkMode: true,
      bg: '#1a1a1a',
      fg: '#e0e0e0',
      primary: '#ff6600',
    });
    const css = getStyles(vs, theme);
    expect(css).toContain('color: #ff6600 !important');
  });

  it('does not apply lightblue link color in light mode', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('color: lightblue');
  });

  it('applies dark code styles when isDarkMode is true', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('body.theme-dark code');
    expect(css).toContain('color: #e0e0e0cc');
    expect(css).toContain('color-mix(in srgb, #1a1a1a 90%, #000)');
  });

  it('applies dark blockquote background in dark mode', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('color-mix(in srgb, #1a1a1a 80%, #000)');
  });

  it('applies dark table override when isDarkMode and overrideColor are both true', () => {
    const vs = makeViewSettings({ overrideColor: true });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    // blockquote, table * rule should have background with color-mix
    expect(css).toMatch(
      /blockquote,\s*table\s*\*\s*\{[^}]*background:\s*color-mix\(in srgb,\s*#1a1a1a\s*80%,\s*#000\)/,
    );
  });

  it('does not tint table descendants in dark mode when overrideColor is false (#4419)', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    // When the user has NOT enabled color override, the `blockquote, table *`
    // rule must not paint a tinted background on table descendants. Otherwise
    // plain tables — and the invisible spacer cells some books use for vertical
    // layout — render with a color different from the page background, and the
    // spacing between words appears to change in dark mode. Regression from
    // #4055; the #4028 zebra-row legibility case is now handled by the
    // light-background rewriters from #4392. See issue #4419.
    const match = css.match(/blockquote,\s*table\s*\*\s*\{([^}]*)\}/);
    expect(match).not.toBeNull();
    expect(match![1]).not.toContain('color-mix');
  });

  it('still tints blockquotes in dark mode when overrideColor is false', () => {
    const vs = makeViewSettings({ overrideColor: false });
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    // The standalone `blockquote` rule keeps its dark-mode tint regardless of
    // overrideColor — only the shared `table *` part is gated.
    expect(css).toMatch(
      /blockquote\s*\{[^}]*background:\s*color-mix\(in srgb,\s*#1a1a1a\s*80%,\s*#000\)/,
    );
  });

  it('makes svg/img backgrounds transparent when overrideColor is true', () => {
    const vs = makeViewSettings({ overrideColor: true });
    const theme = makeThemeCode();
    const css = getStyles(vs, theme);
    expect(css).toMatch(/svg,\s*img\s*\{[^}]*background-color:\s*transparent\s*!important/);
  });

  it('applies screen blend mode for inline images in dark mode', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
    const css = getStyles(vs, theme);
    expect(css).toContain('mix-blend-mode: screen');
  });

  it('applies multiply blend mode for inline images in light mode', () => {
    const vs = makeViewSettings();
    const theme = makeThemeCode({ isDarkMode: false });
    const css = getStyles(vs, theme);
    expect(css).toContain('mix-blend-mode: multiply');
  });

  it('sets inline image parent background when overrideColor is true', () => {
    const vs = makeViewSettings({ overrideColor: true });
    const theme = makeThemeCode({ bg: '#fafafa' });
    const css = getStyles(vs, theme);
    expect(css).toMatch(
      /\*:has\(>\s*img\.has-text-siblings\):not\(body\)\s*\{[^}]*background-color:\s*#fafafa/,
    );
  });
});

// ---------------------------------------------------------------------------
// getTranslationStyles branches
// ---------------------------------------------------------------------------
describe('getTranslationStyles branches (via getStyles)', () => {
  const theme = makeThemeCode();

  it('adds margin to translation-target-block when showTranslateSource is true', () => {
    const vs = makeViewSettings({ showTranslateSource: true });
    const css = getStyles(vs, theme);
    expect(css).toContain('margin: 0.5em 0 !important');
  });

  it('does not add margin to translation-target-block when showTranslateSource is false', () => {
    const vs = makeViewSettings({ showTranslateSource: false });
    const css = getStyles(vs, theme);
    expect(css).not.toContain('margin: 0.5em 0 !important');
  });

  it('always includes translation-source and translation-target classes', () => {
    const vs = makeViewSettings({ showTranslateSource: false });
    const css = getStyles(vs, theme);
    expect(css).toContain('.translation-source');
    expect(css).toContain('.translation-target');
    expect(css).toContain('.translation-target.hidden');
    expect(css).toContain('.translation-target-block');
    expect(css).toContain('.translation-target-toc');
  });
});

// ---------------------------------------------------------------------------
// getRubyStyles branches (Word Lens gloss <rt> size + color)
// ---------------------------------------------------------------------------
describe('getRubyStyles branches (via getStyles)', () => {
  const theme = makeThemeCode();
  const rtBlock = (css: string) => css.match(/ruby\.wl-gloss\s*>\s*rt\s*\{([^}]*)\}/)?.[1] ?? '';

  it('defaults the gloss to 0.5em and muted (opacity 0.7, no color override)', () => {
    const block = rtBlock(getStyles(makeViewSettings(), theme));
    expect(block).toMatch(/font-size:\s*0\.5em/);
    expect(block).toMatch(/opacity:\s*0\.7/);
    expect(block).not.toContain('color:');
  });

  it('applies a configured gloss font size', () => {
    const block = rtBlock(getStyles(makeViewSettings({ wordLensGlossFontSize: 0.8 }), theme));
    expect(block).toMatch(/font-size:\s*0\.8em/);
  });

  it('applies a configured gloss color at full opacity', () => {
    const block = rtBlock(getStyles(makeViewSettings({ wordLensGlossColor: '#ff0000' }), theme));
    expect(block).toMatch(/color:\s*#ff0000/);
    expect(block).toMatch(/opacity:\s*1\b/);
  });
});

// ---------------------------------------------------------------------------
// getStyles integration: userStylesheet appended
// ---------------------------------------------------------------------------
describe('getStyles integration', () => {
  const theme = makeThemeCode();

  it('appends userStylesheet content at the end', () => {
    const customCSS = 'body { color: red !important; }';
    const vs = makeViewSettings({ userStylesheet: customCSS });
    const css = getStyles(vs, theme);
    expect(css).toContain(customCSS);
    // Should be at the end
    expect(css.indexOf(customCSS)).toBe(css.length - customCSS.length);
  });

  it('concatenates all style sections', () => {
    const vs = makeViewSettings();
    const css = getStyles(vs, theme);
    // layout styles
    expect(css).toContain('@namespace epub');
    // font styles
    expect(css).toContain('--serif:');
    // color styles
    expect(css).toContain('--theme-bg-color');
    // translation styles
    expect(css).toContain('.translation-source');
  });

  it('uses default themeCode (via getThemeCode) when none is provided', () => {
    const vs = makeViewSettings();
    // Should not throw even without a themeCode, though getThemeCode uses
    // localStorage which is mocked by jsdom as empty
    const css = getStyles(vs);
    expect(css).toBeTruthy();
    expect(css).toContain('--theme-bg-color');
  });
});

// ---------------------------------------------------------------------------
// custom @font-face inlining
// ---------------------------------------------------------------------------

/** Build a loaded CustomFont (blob URL in memory) for testing. */
function makeCustomFont(overrides: Partial<CustomFont> = {}): CustomFont {
  return {
    id: 'my-test-font',
    name: 'My Test Font',
    path: '/fonts/my-test-font.ttf',
    blobUrl: 'blob:http://localhost/my-test-font',
    ...overrides,
  };
}

describe('custom @font-face inlining (via getStyles)', () => {
  const theme = makeThemeCode();

  it('inlines an @font-face rule for each loaded custom font', () => {
    const vs = makeViewSettings();
    const css = getStyles(vs, theme, [makeCustomFont()]);
    expect(css).toContain('@font-face');
    expect(css).toContain('font-family: "My Test Font"');
    expect(css).toContain('blob:http://localhost/my-test-font');
  });

  it('keeps @namespace epub ahead of every @font-face rule (#4438)', () => {
    const vs = makeViewSettings();
    const css = getStyles(vs, theme, [makeCustomFont()]);
    // A `@namespace` rule is only honored when it precedes all style and
    // `@font-face` rules; a misplaced one is silently ignored, which drops
    // the namespaced `aside[epub|type~="footnote"]` selector and reveals the
    // footnote aside's border as a stray horizontal line (#4438). The custom
    // `@font-face` rules must therefore come after the namespace declaration.
    expect(css).toContain('@namespace epub');
    expect(css).toContain('@font-face');
    expect(css.indexOf('@namespace epub')).toBeLessThan(css.indexOf('@font-face'));
  });

  it('still inlines custom @font-face ahead of the font-family declarations', () => {
    const vs = makeViewSettings();
    const css = getStyles(vs, theme, [makeCustomFont()]);
    // Paginator writes this CSS into the iframe before its first paint, so the
    // custom `@font-face` rules should still precede the `--serif`/`--sans-serif`
    // font lists that reference them.
    expect(css.indexOf('@font-face')).toBeLessThan(css.indexOf('--serif:'));
  });

  it('emits one @font-face per loaded font', () => {
    const vs = makeViewSettings();
    const fonts = [
      makeCustomFont({ id: 'font-a', name: 'Font A', blobUrl: 'blob:http://localhost/a' }),
      makeCustomFont({ id: 'font-b', name: 'Font B', blobUrl: 'blob:http://localhost/b' }),
    ];
    const css = getStyles(vs, theme, fonts);
    expect(css.split('@font-face').length - 1).toBe(2);
    expect(css).toContain('font-family: "Font A"');
    expect(css).toContain('font-family: "Font B"');
  });

  it('skips fonts that have no blob URL', () => {
    const vs = makeViewSettings();
    const css = getStyles(vs, theme, [makeCustomFont({ blobUrl: undefined })]);
    expect(css).not.toContain('font-family: "My Test Font"');
  });

  it('emits no custom @font-face when no fonts are passed', () => {
    const vs = makeViewSettings();
    const css = getStyles(vs, theme);
    expect(css).not.toContain('font-family: "My Test Font"');
  });
});
