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

import {
  joinSkillMarkdown,
  readFrontmatterScalar,
  resolveSkillRelativePath,
  splitSkillMarkdown,
  syncSkillMarkdownFrontmatter,
  updateFrontmatterScalar,
} from './skill-markdown';

describe('skill markdown round-trip', () => {
  it('preserves nested and unknown frontmatter while editing the body', () => {
    const source = [
      '---',
      'name: research-skill',
      'description: Research helper',
      'metadata:',
      '  owner: team-a',
      '  flags:',
      '    - safe',
      'x-extra: keep-me',
      '---',
      '',
      '# Instructions',
      '',
      '- Read `references/policy.md`.',
    ].join('\n');
    const parts = splitSkillMarkdown(source);
    parts.body = `${parts.body}\n- Return a concise answer.`;
    const next = joinSkillMarkdown(parts);

    expect(next).toContain('metadata:\n  owner: team-a\n  flags:\n    - safe');
    expect(next).toContain('x-extra: keep-me');
    expect(next).toContain('Return a concise answer.');
  });

  it.each([
    '---\nname: exact\n---',
    '---\nname: exact\n---\n',
    '---\nname: exact\n---\n\n# Title\n',
    '---\r\nname: exact\r\n---\r\n\r\n# Title',
  ])('round-trips delimiters and whitespace exactly', (source) => {
    expect(joinSkillMarkdown(splitSkillMarkdown(source))).toBe(source);
  });

  it('does not treat a non-delimiter line as closing frontmatter', () => {
    const source = '---\nname: exact\n---foo\n# title';
    expect(splitSkillMarkdown(source).hasFrontmatter).toBe(false);
  });

  it('only changes the requested top-level scalar', () => {
    const frontmatter =
      'name: old-name\ncustom:\n  name: nested-name\nx-extra: true';
    const updated = updateFrontmatterScalar(frontmatter, 'name', 'new-name');
    expect(readFrontmatterScalar(updated, 'name')).toBe('new-name');
    expect(updated).toContain('  name: nested-name');
    expect(updated).toContain('x-extra: true');
  });

  it('syncs new Skill metadata without losing the body or unknown frontmatter', () => {
    const source =
      '---\nname: old-name\ndescription: old description\nx-owner: team-a\n---\n\n# Instructions\n\nKeep this body.';
    const updated = syncSkillMarkdownFrontmatter(
      source,
      'new-name',
      'New description',
    );

    expect(updated).toContain('name: "new-name"');
    expect(updated).toContain('description: "New description"');
    expect(updated).toContain('x-owner: team-a');
    expect(updated).toContain('# Instructions\n\nKeep this body.');
  });

  it('adds required frontmatter to a Markdown document that has none', () => {
    const updated = syncSkillMarkdownFrontmatter(
      '# Instructions\n\nKeep this body.',
      'new-skill',
      'New description',
    );

    expect(updated).toContain('name: "new-skill"');
    expect(updated).toContain('description: "New description"');
    expect(updated).toContain('# Instructions\n\nKeep this body.');
    expect(splitSkillMarkdown(updated).hasFrontmatter).toBe(true);
  });

  it('refuses structured edits for multiline YAML scalars', () => {
    const frontmatter =
      'name: exact\ndescription: |\n  line one\n  line two\nx-extra: true';
    expect(() =>
      updateFrontmatterScalar(frontmatter, 'description', 'next'),
    ).toThrow('源码模式');
    expect(frontmatter).toContain('line two');
  });

  it('reads block scalars and preserves them when metadata is unchanged', () => {
    const source =
      '---\nname: exact\ndescription: |\n  line one\n  line two\nx-extra: true\n---\n\n# Instructions\n';
    const parts = splitSkillMarkdown(source);

    expect(readFrontmatterScalar(parts.frontmatter, 'description')).toBe(
      'line one\nline two\n',
    );
    expect(
      syncSkillMarkdownFrontmatter(source, 'exact', 'line one\nline two'),
    ).toBe(source);
  });

  it('returns an empty scalar for malformed or non-string frontmatter', () => {
    expect(readFrontmatterScalar('name: [broken', 'name')).toBe('');
    expect(readFrontmatterScalar('name:\n  nested: value', 'name')).toBe('');
  });

  it.each([
    'description: |2\n  text',
    'description: >-\n  text',
    'description: |2- # comment\n  text',
    'description: first line\n  continuation',
    'description: plain # keep this comment',
  ])('routes unsafe scalar edits to source mode', (frontmatter) => {
    expect(() =>
      updateFrontmatterScalar(frontmatter, 'description', 'next'),
    ).toThrow('源码模式');
  });

  it('resolves links from the current markdown file directory', () => {
    expect(
      resolveSkillRelativePath(
        'references/guides/start.md',
        '../policy.md#scope',
      ),
    ).toBe('references/policy.md');
    expect(resolveSkillRelativePath('SKILL.md', 'examples/sample.md')).toBe(
      'examples/sample.md',
    );
    expect(
      resolveSkillRelativePath('SKILL.md', '../outside.md'),
    ).toBeUndefined();
  });

  it.each([
    'javascript:alert(1)',
    'file:///etc/passwd',
    'blob:https://example.com/foreign',
    'data:text/html,<script>alert(1)</script>',
    '//example.com/path',
    'references/a.md\u0000.png',
  ])(
    'rejects URI schemes and control characters from relative links',
    (href) => {
      expect(resolveSkillRelativePath('SKILL.md', href)).toBeUndefined();
    },
  );

  it('preserves raw HTML as source text for the text-only Milkdown HTML node', () => {
    const source =
      '---\nname: safe\ndescription: safe\n---\n\n<img src=x onerror="window.pwned=true">';
    expect(joinSkillMarkdown(splitSkillMarkdown(source))).toBe(source);
  });
});
