// Regression test for the PDF auto-extract path in tools/web.js webFetch.
// Spins up a tiny local HTTP server, sends PDF-like payloads, and asserts the
// detection logic + formatting. pdf-parse itself is mocked because pdfjs-dist
// uses dynamic ESM imports that Jest's CJS runner can't load without
// --experimental-vm-modules — verifying our wiring is the goal here, not
// pdfjs-dist's parser (that's exercised by the standalone smoke test).
//
// Backstory: a sub-agent burned its 10-minute timeout trying to extract text
// from a CCW instructor PDF because web_fetch returned binary bytes and the
// system pdftotext was broken. This test guards the new behavior.

const http = require('http');

jest.mock('../src/constants', () => ({
  CONNECTIONS_FILE: '/tmp/test-web-fetch-connections.json',
  ARTIFACTS_DIR: '/tmp/test-web-fetch-artifacts',
}));

// Mock pdf-parse — return a deterministic result so tests don't depend on
// pdfjs-dist's runtime (which Jest can't load).
jest.mock('pdf-parse', () => {
  let __nextResult = { text: 'Mocked PDF text', info: { Title: 'Mocked Title' } };
  let __nextThrow = null;
  class PDFParse {
    constructor(_opts) { /* opts.data ignored — we return canned results */ }
    async getText() {
      if (__nextThrow) throw __nextThrow;
      return { text: __nextResult.text };
    }
    async getInfo() {
      if (__nextThrow) throw __nextThrow;
      return { total: 1, info: __nextResult.info };
    }
    async destroy() {}
  }
  return {
    PDFParse,
    __setNextResult: (r) => { __nextResult = r; __nextThrow = null; },
    __setNextThrow: (e) => { __nextThrow = e; },
  };
});

const pdfParseMock = require('pdf-parse');
const { webFetch } = require('../src/tools/web');

function startServer(handler) {
  return new Promise((resolve) => {
    const server = http.createServer(handler);
    server.listen(0, '127.0.0.1', () => resolve({ server, port: server.address().port }));
  });
}

// Minimal valid PDF header — enough bytes that magic-byte detection fires.
// pdf-parse is mocked so we don't actually need a real PDF.
const PDF_HEADER_BYTES = Buffer.from('%PDF-1.4\n%fake content for tests\n');

describe('web_fetch PDF auto-extract', () => {
  beforeEach(() => {
    pdfParseMock.__setNextResult({
      text: 'Instructor: Jane Doe — 16-hour course\nInstructor: John Smith — 16-hour course',
      info: { Title: 'CCW Instructor Roster' },
    });
  });

  test('detects PDF by Content-Type and returns extracted text', async () => {
    const { server, port } = await startServer((_req, res) => {
      res.setHeader('Content-Type', 'application/pdf');
      res.end(PDF_HEADER_BYTES);
    });
    try {
      const result = await webFetch({ url: `http://127.0.0.1:${port}/roster.pdf` });
      expect(result.startsWith('[PDF]')).toBe(true);
      expect(result).toContain('CCW Instructor Roster');
      expect(result).toContain('Jane Doe');
      expect(result).toContain('John Smith');
      expect(result).not.toContain('%PDF-'); // raw bytes must not leak
    } finally {
      server.close();
    }
  });

  test('detects PDF by magic bytes when Content-Type is wrong', async () => {
    const { server, port } = await startServer((_req, res) => {
      // Some static PDF hosts mis-label Content-Type as text/html — we still detect
      res.setHeader('Content-Type', 'text/html');
      res.end(PDF_HEADER_BYTES);
    });
    try {
      const result = await webFetch({ url: `http://127.0.0.1:${port}/roster.pdf` });
      expect(result.startsWith('[PDF]')).toBe(true);
      expect(result).toContain('Jane Doe');
    } finally {
      server.close();
    }
  });

  test('falls through to HTML path for plain HTML responses', async () => {
    const { server, port } = await startServer((_req, res) => {
      res.setHeader('Content-Type', 'text/html');
      res.end('<html><body><h1>Hello world</h1></body></html>');
    });
    try {
      const result = await webFetch({ url: `http://127.0.0.1:${port}/index.html` });
      expect(result).toContain('Hello world');
      expect(result.startsWith('[PDF]')).toBe(false);
    } finally {
      server.close();
    }
  });

  test('reports image-only PDFs cleanly when extraction yields no text', async () => {
    pdfParseMock.__setNextResult({ text: '', info: {} });
    const { server, port } = await startServer((_req, res) => {
      res.setHeader('Content-Type', 'application/pdf');
      res.end(PDF_HEADER_BYTES);
    });
    try {
      const result = await webFetch({ url: `http://127.0.0.1:${port}/scanned.pdf` });
      expect(result).toMatch(/no extractable text/i);
      expect(result).not.toContain('%PDF-');
    } finally {
      server.close();
    }
  });

  test('surfaces a helpful error when pdf-parse throws', async () => {
    pdfParseMock.__setNextThrow(new Error('document is encrypted'));
    const { server, port } = await startServer((_req, res) => {
      res.setHeader('Content-Type', 'application/pdf');
      res.end(PDF_HEADER_BYTES);
    });
    try {
      const result = await webFetch({ url: `http://127.0.0.1:${port}/locked.pdf` });
      expect(result).toMatch(/extraction failed/i);
      expect(result).toContain('document is encrypted');
      expect(result).not.toContain('%PDF-');
    } finally {
      server.close();
    }
  });

  test('truncates very long PDF text with a clear cutoff marker', async () => {
    const longText = 'Line of content. '.repeat(8000); // ~128K chars
    pdfParseMock.__setNextResult({ text: longText, info: { Title: 'Big doc' } });
    const { server, port } = await startServer((_req, res) => {
      res.setHeader('Content-Type', 'application/pdf');
      res.end(PDF_HEADER_BYTES);
    });
    try {
      const result = await webFetch({ url: `http://127.0.0.1:${port}/big.pdf` });
      expect(result).toContain('truncated');
      expect(result.length).toBeLessThan(longText.length + 1000);
    } finally {
      server.close();
    }
  });
});
