import { describe, it, expect } from 'vitest';
import {
  OUTPUT_PATH_REGEX,
  splitTextByOutputPaths,
  stripOutputPrefix,
  linkifyOutputPathsInEscapedHtml,
} from './output-path-detect';

describe('OUTPUT_PATH_REGEX', () => {
  // Reset lastIndex between assertions because /g regex preserves state
  // between .test() / .exec() calls — easy to trip on.
  function resetAndMatch(input: string): string[] {
    OUTPUT_PATH_REGEX.lastIndex = 0;
    return Array.from(input.matchAll(OUTPUT_PATH_REGEX)).map(m => m[0]);
  }

  it('matches bare output/foo.md', () => {
    expect(resetAndMatch('see output/foo.md for details')).toEqual(['output/foo.md']);
  });

  it('matches nested paths', () => {
    expect(resetAndMatch('output/sub/dir/result.csv')).toEqual(['output/sub/dir/result.csv']);
  });

  it('matches multiple in one string', () => {
    expect(resetAndMatch('output/a.md and output/b.md')).toEqual(['output/a.md', 'output/b.md']);
  });

  it('matches common extensions', () => {
    for (const ext of ['md', 'csv', 'jsonl', 'pdf', 'xlsx', 'html', 'png', 'txt']) {
      const path = `output/result.${ext}`;
      expect(resetAndMatch(path)).toEqual([path]);
    }
  });

  it('ignores URL-embedded paths', () => {
    // Critical: prevents linkifying parts of unrelated URLs into bogus
    // workspace paths.
    expect(resetAndMatch('https://example.com/output/foo.md')).toEqual([]);
    expect(resetAndMatch('see /home/user/output/foo.md')).toEqual([]);
  });

  it('ignores prefix-extension words', () => {
    // `notoutput/foo.md` should NOT match — `output` must be at a word
    // boundary or beginning of string.
    expect(resetAndMatch('notoutput/foo.md')).toEqual([]);
    expect(resetAndMatch('myoutput/foo.md')).toEqual([]);
  });

  it('trims trailing closing punctuation', () => {
    // The regex stops at `)`, `]`, `}`, `"`, etc. so the linkified
    // anchor doesn't swallow the bracket from `(output/foo.md)`.
    expect(resetAndMatch('(output/foo.md)')).toEqual(['output/foo.md']);
    expect(resetAndMatch('"output/foo.md"')).toEqual(['output/foo.md']);
    expect(resetAndMatch('see output/foo.md。次は…')).toEqual(['output/foo.md']);
    expect(resetAndMatch('[label](output/foo.md)')).toEqual(['output/foo.md']);
  });

  it('rejects no-extension paths', () => {
    // `output/dirname` without extension is too ambiguous (could be
    // any directory) — skip to keep precision high.
    expect(resetAndMatch('output/dirname')).toEqual([]);
  });
});

describe('splitTextByOutputPaths', () => {
  it('returns single text segment when no paths', () => {
    expect(splitTextByOutputPaths('hello world')).toEqual([
      { type: 'text', text: 'hello world' },
    ]);
  });

  it('alternates text + path segments', () => {
    expect(splitTextByOutputPaths('see output/a.md and output/b.md done')).toEqual([
      { type: 'text', text: 'see ' },
      { type: 'path', path: 'output/a.md' },
      { type: 'text', text: ' and ' },
      { type: 'path', path: 'output/b.md' },
      { type: 'text', text: ' done' },
    ]);
  });

  it('handles leading/trailing path', () => {
    expect(splitTextByOutputPaths('output/foo.md is here')).toEqual([
      { type: 'path', path: 'output/foo.md' },
      { type: 'text', text: ' is here' },
    ]);
    expect(splitTextByOutputPaths('result: output/foo.md')).toEqual([
      { type: 'text', text: 'result: ' },
      { type: 'path', path: 'output/foo.md' },
    ]);
  });

  it('round-trips text content', () => {
    const original = 'A output/x.md B output/sub/y.csv C';
    const reassembled = splitTextByOutputPaths(original)
      .map(s => (s.type === 'text' ? s.text : s.path))
      .join('');
    expect(reassembled).toBe(original);
  });

  it('empty input is safe', () => {
    expect(splitTextByOutputPaths('')).toEqual([{ type: 'text', text: '' }]);
  });
});

describe('stripOutputPrefix', () => {
  it('strips output/ prefix', () => {
    expect(stripOutputPrefix('output/foo.md')).toBe('foo.md');
    expect(stripOutputPrefix('output/sub/foo.md')).toBe('sub/foo.md');
  });

  it('leaves non-output paths intact', () => {
    expect(stripOutputPrefix('foo.md')).toBe('foo.md');
  });
});

describe('linkifyOutputPathsInEscapedHtml', () => {
  it('wraps matches in data-output-path anchors', () => {
    const out = linkifyOutputPathsInEscapedHtml('see output/foo.md now');
    expect(out).toContain('data-output-path="output/foo.md"');
    expect(out).toContain('class="output-path-link"');
    expect(out).toContain('role="button"');
    expect(out).toContain('tabindex="0"');
  });

  it('leaves text without matches untouched', () => {
    expect(linkifyOutputPathsInEscapedHtml('hello world')).toBe('hello world');
  });

  it('handles multiple matches in escaped HTML', () => {
    const out = linkifyOutputPathsInEscapedHtml('A output/a.md B output/b.md');
    expect(out.match(/output-path-link/g)?.length).toBe(2);
  });
});
