// NOTE: renderHelpHtml is NOT covered here — it depends on DOMPurify, which
// requires a real DOM (DOMPurify.isSupported === false in this node test
// environment, sanitize is not callable). Everything else in help.ts is pure.
import { describe, it, expect } from 'vitest';
import {
  splitFrontmatter,
  validateFrontmatter,
  parseHelpDoc,
  slugify,
  makeSlugger,
  filterSections,
  type HelpSection,
} from './help';

const DOC = `---
id: getting-started
title: はじめに
category: basic
order: 1
keywords: [setup, 初期設定]
---
# はじめに

本文です。`;

describe('splitFrontmatter', () => {
  it('splits the frontmatter block from the body', () => {
    const { frontmatter, body } = splitFrontmatter(DOC);
    expect(frontmatter).toContain('id: getting-started');
    expect(body).toContain('# はじめに');
    expect(body).not.toContain('---');
  });

  it('returns the whole input as body when there is no frontmatter', () => {
    const { frontmatter, body } = splitFrontmatter('plain body');
    expect(frontmatter).toBe('');
    expect(body).toBe('plain body');
  });

  it('normalizes CRLF and strips a BOM', () => {
    const { frontmatter, body } = splitFrontmatter('﻿---\r\nid: x\r\n---\r\nbody');
    expect(frontmatter).toBe('id: x');
    expect(body).toBe('body');
  });

  it('ignores a frontmatter block not at the very start', () => {
    const { frontmatter } = splitFrontmatter('intro\n---\nid: x\n---\n');
    expect(frontmatter).toBe('');
  });
});

describe('validateFrontmatter', () => {
  const VALID = { id: 'a', title: 't', category: 'basic', order: 1, keywords: ['k'] };

  it('accepts a valid frontmatter object', () => {
    const r = validateFrontmatter(VALID, 'doc.md');
    expect(r.ok).toBe(true);
    expect(r.value).toEqual(VALID);
  });

  it('defaults keywords to an empty array', () => {
    const r = validateFrontmatter({ ...VALID, keywords: undefined }, 'doc.md');
    expect(r.ok).toBe(true);
    expect(r.value?.keywords).toEqual([]);
  });

  it.each([
    ['missing id', { ...VALID, id: '' }, /'id' is required/],
    ['missing title', { ...VALID, title: undefined }, /'title' is required/],
    ['bad category', { ...VALID, category: 'misc' }, /'category' must be one of/],
    ['bad order', { ...VALID, order: 'first' }, /'order' is required/],
    ['bad keywords', { ...VALID, keywords: [1] }, /'keywords' must be a string array/],
  ])('rejects %s with a sourced error', (_label, data, re) => {
    const r = validateFrontmatter(data, 'doc.md');
    expect(r.ok).toBe(false);
    expect(r.errors.join('\n')).toMatch(re);
    expect(r.errors[0]).toContain('doc.md');
  });

  it('collects multiple errors for non-object input', () => {
    const r = validateFrontmatter(null, 'doc.md');
    expect(r.ok).toBe(false);
    expect(r.errors.length).toBeGreaterThanOrEqual(3);
  });
});

describe('parseHelpDoc', () => {
  it('parses YAML frontmatter and body', () => {
    const { data, body } = parseHelpDoc(DOC);
    expect(data).toMatchObject({ id: 'getting-started', category: 'basic', keywords: ['setup', '初期設定'] });
    expect(body).toContain('本文です。');
  });

  it('returns an empty object for a doc without frontmatter', () => {
    expect(parseHelpDoc('just text').data).toEqual({});
  });

  it('throws on YAML syntax errors', () => {
    expect(() => parseHelpDoc('---\nid: [unclosed\n---\nbody')).toThrow();
  });
});

describe('slugify', () => {
  it('lowercases and hyphenates ascii text', () => {
    expect(slugify('Getting Started!')).toBe('getting-started');
  });

  it('keeps Japanese characters', () => {
    expect(slugify('タスクの作成 方法')).toBe('タスクの作成-方法');
  });

  it('strips HTML tags', () => {
    expect(slugify('<code>config.yaml</code> settings')).toBe('config-yaml-settings');
  });

  it('falls back to "section" for empty results', () => {
    expect(slugify('!!!')).toBe('section');
  });
});

describe('makeSlugger', () => {
  it('appends -2, -3 on collisions', () => {
    const slug = makeSlugger();
    expect(slug('Setup')).toBe('setup');
    expect(slug('Setup')).toBe('setup-2');
    expect(slug('Setup')).toBe('setup-3');
    expect(slug('Other')).toBe('other');
  });
});

describe('filterSections', () => {
  const sections: HelpSection[] = [
    { id: 'a', title: 'タスクの作成', category: 'basic', order: 1, keywords: ['task'], body: '作成手順' },
    { id: 'b', title: 'Scheduling', category: 'advanced', order: 2, keywords: ['cron'], body: 'periodic runs' },
    { id: 'c', title: 'Admin', category: 'admin', order: 3, keywords: [], body: 'ユーザー管理と権限' },
  ];

  it('returns everything for an empty query', () => {
    expect(filterSections(sections, '  ')).toEqual(sections);
  });

  it('matches the title case-insensitively', () => {
    expect(filterSections(sections, 'scheduling').map((s) => s.id)).toEqual(['b']);
  });

  it('matches keywords', () => {
    expect(filterSections(sections, 'CRON').map((s) => s.id)).toEqual(['b']);
  });

  it('matches the body, including Japanese', () => {
    expect(filterSections(sections, '権限').map((s) => s.id)).toEqual(['c']);
  });

  it('returns an empty list when nothing matches', () => {
    expect(filterSections(sections, 'zzz')).toEqual([]);
  });
});
