import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { afterEach, describe, expect, it } from 'vitest';
import {
  sanitizeSourceName,
  appendSourceIndex,
  saveSourceFile,
  findSourceByUrl,
  resolveUniqueSourceFile,
  sourceDir,
  sourceIndexPath,
  type SourceIndexEntry,
} from './source-library.js';

function makeWorkspace(): string {
  return fs.mkdtempSync(path.join(tmpdir(), 'maestro-source-'));
}

function readIndex(workspacePath: string): SourceIndexEntry[] {
  const raw = fs.readFileSync(sourceIndexPath(workspacePath), 'utf-8');
  return raw.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l) as SourceIndexEntry);
}

describe('sanitizeSourceName', () => {
  it('derives a safe name from a URL with slashes and query', () => {
    const name = sanitizeSourceName('https://example.com/a/b/report.html?x=1&y=2');
    expect(name).not.toContain('/');
    expect(name).not.toContain('..');
    expect(name).not.toContain('?');
    expect(name).toContain('example.com');
  });

  it('strips path traversal and separators from plain titles', () => {
    const name = sanitizeSourceName('../../etc/passwd');
    expect(name).not.toContain('/');
    expect(name).not.toContain('..');
  });

  it('removes Windows-forbidden and control characters', () => {
    const name = sanitizeSourceName('a:b*c?"<>|d');
    expect(name).not.toMatch(/[:*?"<>|]/);
  });

  it('caps length at 120 chars', () => {
    const name = sanitizeSourceName('x'.repeat(500));
    expect(name.length).toBeLessThanOrEqual(120);
  });

  it('falls back to "source" for empty/garbage input', () => {
    expect(sanitizeSourceName('')).toBe('source');
    expect(sanitizeSourceName('///')).toBe('source');
  });
});

describe('resolveUniqueSourceFile', () => {
  let workspacePath = '';
  afterEach(() => {
    if (workspacePath) fs.rmSync(workspacePath, { recursive: true, force: true });
    workspacePath = '';
  });

  it('returns the preferred name when no collision', () => {
    workspacePath = makeWorkspace();
    expect(resolveUniqueSourceFile(workspacePath, 'page', '.md')).toBe('page.md');
  });

  it('versions with a numeric suffix on collision', () => {
    workspacePath = makeWorkspace();
    fs.mkdirSync(sourceDir(workspacePath), { recursive: true });
    fs.writeFileSync(path.join(sourceDir(workspacePath), 'page.md'), 'x');
    expect(resolveUniqueSourceFile(workspacePath, 'page', '.md')).toBe('page-1.md');
    fs.writeFileSync(path.join(sourceDir(workspacePath), 'page-1.md'), 'x');
    expect(resolveUniqueSourceFile(workspacePath, 'page', '.md')).toBe('page-2.md');
  });
});

describe('appendSourceIndex', () => {
  let workspacePath = '';
  afterEach(() => {
    if (workspacePath) fs.rmSync(workspacePath, { recursive: true, force: true });
    workspacePath = '';
  });

  it('creates source/ and appends a jsonl line', () => {
    workspacePath = makeWorkspace();
    appendSourceIndex(workspacePath, {
      file: 'a.md', url: 'https://example.com/a', fetched_at: '2026-06-19T00:00:00.000Z',
      tool: 'WebFetch', bytes: 10,
    });
    const entries = readIndex(workspacePath);
    expect(entries).toHaveLength(1);
    expect(entries[0]?.url).toBe('https://example.com/a');
    expect(entries[0]?.tool).toBe('WebFetch');
    expect(entries[0]?.bytes).toBe(10);
  });

  it('appends multiple lines', () => {
    workspacePath = makeWorkspace();
    appendSourceIndex(workspacePath, { file: 'a.md', url: 'u1', fetched_at: 't', tool: 'WebFetch', bytes: 1 });
    appendSourceIndex(workspacePath, { file: 'b.md', url: 'u2', fetched_at: 't', tool: 'DownloadFile', bytes: 2 });
    expect(readIndex(workspacePath)).toHaveLength(2);
  });
});

describe('findSourceByUrl', () => {
  let workspacePath = '';
  afterEach(() => {
    if (workspacePath) fs.rmSync(workspacePath, { recursive: true, force: true });
    workspacePath = '';
  });

  it('returns null when no index exists', () => {
    workspacePath = makeWorkspace();
    expect(findSourceByUrl(workspacePath, 'https://x')).toBeNull();
  });

  it('finds an existing url and ignores corrupt lines', () => {
    workspacePath = makeWorkspace();
    fs.mkdirSync(sourceDir(workspacePath), { recursive: true });
    fs.writeFileSync(sourceIndexPath(workspacePath),
      'not-json\n' + JSON.stringify({ file: 'a.md', url: 'https://found', fetched_at: 't', tool: 'WebFetch', bytes: 1 }) + '\n');
    expect(findSourceByUrl(workspacePath, 'https://found')?.file).toBe('a.md');
    expect(findSourceByUrl(workspacePath, 'https://missing')).toBeNull();
  });
});

describe('saveSourceFile', () => {
  let workspacePath = '';
  afterEach(() => {
    if (workspacePath) fs.rmSync(workspacePath, { recursive: true, force: true });
    workspacePath = '';
  });

  it('writes a .md under source/ and appends an index entry', () => {
    workspacePath = makeWorkspace();
    const rel = saveSourceFile(workspacePath, {
      url: 'https://example.com/page', body: '# Hello', tool: 'WebFetch',
    });
    expect(rel).toBeTruthy();
    const saved = fs.readFileSync(path.join(sourceDir(workspacePath), rel!), 'utf-8');
    expect(saved).toBe('# Hello');
    const entries = readIndex(workspacePath);
    expect(entries).toHaveLength(1);
    expect(entries[0]?.file).toBe(rel);
    expect(entries[0]?.url).toBe('https://example.com/page');
    expect(entries[0]?.bytes).toBe(Buffer.byteLength('# Hello'));
  });

  it('skips empty body', () => {
    workspacePath = makeWorkspace();
    expect(saveSourceFile(workspacePath, { url: 'u', body: '   ', tool: 'WebFetch' })).toBeNull();
    expect(fs.existsSync(sourceDir(workspacePath))).toBe(false);
  });

  it('dedupes by url (skips re-save of same url)', () => {
    workspacePath = makeWorkspace();
    const first = saveSourceFile(workspacePath, { url: 'https://dup', body: 'a', tool: 'WebFetch' });
    expect(first).toBeTruthy();
    const second = saveSourceFile(workspacePath, { url: 'https://dup', body: 'b', tool: 'WebFetch' });
    expect(second).toBeNull();
    expect(readIndex(workspacePath)).toHaveLength(1);
    // original content untouched
    expect(fs.readFileSync(path.join(sourceDir(workspacePath), first!), 'utf-8')).toBe('a');
  });

  it('versions filename when different urls collide on the same name', () => {
    workspacePath = makeWorkspace();
    const a = saveSourceFile(workspacePath, { url: 'https://example.com/page', body: 'a', tool: 'WebFetch' });
    const b = saveSourceFile(workspacePath, { url: 'https://example.com/page?v=2', body: 'b', tool: 'WebFetch' });
    expect(a).not.toBe(b);
    expect(readIndex(workspacePath)).toHaveLength(2);
  });

  it('uses title for the filename when provided', () => {
    workspacePath = makeWorkspace();
    const rel = saveSourceFile(workspacePath, {
      url: 'https://example.com/x', title: 'My Report Title', body: 'a', tool: 'WebFetch',
    });
    expect(rel).toContain('My-Report-Title');
  });
});
