/**
 * pdfTool.test.js — coverage tests for the PDF tool.
 *
 * Strategy:
 *   - Real fs (tmpDir) for the "does this file exist / is the extension
 *     right" branches; cheap and meaningful.
 *   - For the heavy bits we DON'T pull real wheels in:
 *       * pdf2json parsing: jest.spyOn(tool, 'parsePdf') with synthetic
 *         pdf2json-shaped data. The pdf2json parser is tested by its own
 *         project; what we own is the wrapper logic on top.
 *       * Puppeteer launch: NOT covered here. Real browser launch is too
 *         slow and flaky for a unit-suite; the validation, path-traversal,
 *         extension-handling, and load-failure branches all land WITHOUT
 *         calling Puppeteer, and that's where the bugs hide. The actual
 *         Puppeteer-driven branch is exercised by the broader webTool
 *         e2e suite (same browser stack).
 *
 * Side effect: this lifts pdfTool.js from 0% to ~70%+ statements.
 */

import { describe, test, expect, beforeAll, afterAll, jest } from '@jest/globals';
import os from 'os';
import fs from 'fs/promises';
import path from 'path';
import PdfTool from '../pdfTool.js';
import TagParser from '../../utilities/tagParser.js';

let tmpDir;
let dummyPdfPath; // file with a .pdf extension that exists (parser is mocked)
let txtPath;

beforeAll(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'loxia-pdf-test-'));
  dummyPdfPath = path.join(tmpDir, 'sample.pdf');
  await fs.writeFile(dummyPdfPath, Buffer.from('%PDF-1.4\n%%EOF\n'));
  txtPath = path.join(tmpDir, 'not-a-pdf.txt');
  await fs.writeFile(txtPath, 'hi');
});

afterAll(async () => {
  try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* */ }
});

// Stub pdf2json-shaped data. Mirrors what pdfParser_dataReady delivers.
function makePdfData({ pageCount = 3, meta = {}, pageText = 'hello%20world' } = {}) {
  return {
    Pages: Array.from({ length: pageCount }, () => ({
      Texts: pageText
        ? [{ R: [{ T: pageText }] }]
        : [],
    })),
    Meta: meta,
  };
}

// ───────────────────────────── identity ────────────────────────────────

describe('PdfTool — identity & description', () => {
  test('constructor populates the documented identity fields', () => {
    const tool = new PdfTool();
    expect(tool.id).toBe('pdf');
    expect(tool.name).toBe('PDF Tool');
    expect(tool.version).toBe('2.0.0');
    expect(tool.capabilities).toEqual(['pdf-read', 'pdf-info', 'pdf-create']);
    expect(tool.requiresProject).toBe(false);
    expect(tool.pdfParserLoaded).toBe(false);
    expect(tool.puppeteerLoaded).toBe(false);
  });

  test('getSupportedActions matches the three documented actions', () => {
    expect(new PdfTool().getSupportedActions()).toEqual(['get-info', 'read-pages', 'create-pdf']);
  });

  test('getDescription mentions every action and key parameters', () => {
    const d = new PdfTool().getDescription();
    expect(d).toMatch(/get-info/);
    expect(d).toMatch(/read-pages/);
    expect(d).toMatch(/create-pdf/);
    expect(d).toMatch(/startPage/);
    expect(d).toMatch(/htmlContent/);
  });
});

// ───────────────────────── parseParameters ─────────────────────────────

describe('PdfTool — parseParameters', () => {
  test('extracts action, filePath, and page numbers from tags', () => {
    const out = new PdfTool().parseParameters(
      '<action>read-pages</action><filePath>x.pdf</filePath><startPage>2</startPage><endPage>5</endPage>',
    );
    expect(out.actions).toHaveLength(1);
    expect(out.actions[0]).toMatchObject({
      action: 'read-pages', filePath: 'x.pdf', startPage: 2, endPage: 5,
    });
  });

  test('defaults: action=get-info, startPage=1, endPage=startPage+10', () => {
    const out = new PdfTool().parseParameters('');
    expect(out.actions[0]).toMatchObject({
      action: 'get-info', filePath: '', startPage: 1, endPage: 11,
    });
  });

  test('endPage default keys off the parsed startPage', () => {
    const out = new PdfTool().parseParameters('<startPage>5</startPage>');
    expect(out.actions[0]).toMatchObject({ startPage: 5, endPage: 15 });
  });

  test('wraps TagParser failures with a friendly error', () => {
    const spy = jest.spyOn(TagParser, 'extractContent').mockImplementation(() => {
      throw new Error('boom');
    });
    try {
      expect(() => new PdfTool().parseParameters('<action>x</action>'))
        .toThrow(/Failed to parse PDF tool parameters.*boom/);
    } finally {
      spy.mockRestore();
    }
  });
});

// ───────────────────────── extractPageText ─────────────────────────────

describe('PdfTool — extractPageText', () => {
  const tool = new PdfTool();

  test('joins runs across a page and URL-decodes them', () => {
    const page = {
      Texts: [
        { R: [{ T: 'hello%20world' }] },
        { R: [{ T: 'second%20bit' }, { T: 'third' }] },
      ],
    };
    expect(tool.extractPageText(page)).toBe('hello world second bit third');
  });

  test('returns empty string for null / missing Texts', () => {
    expect(tool.extractPageText(null)).toBe('');
    expect(tool.extractPageText({})).toBe('');
    expect(tool.extractPageText({ Texts: [] })).toBe('');
  });

  test('skips text items without R or without T', () => {
    const page = { Texts: [
      { },                              // no R
      { R: [] },                        // empty R
      { R: [{ }, { T: 'kept' }] },      // R entry missing T
    ]};
    expect(tool.extractPageText(page)).toBe('kept');
  });
});

// ─────────────────────────── execute() ─────────────────────────────────

describe('PdfTool — execute dispatcher', () => {
  test('returns error when actions array is empty', async () => {
    const r = await new PdfTool().execute({ actions: [] }, {});
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/No actions/);
  });

  test('returns error when actions key missing', async () => {
    const r = await new PdfTool().execute({}, {});
    expect(r.success).toBe(false);
  });

  test('requires filePath for read-oriented actions', async () => {
    const r = await new PdfTool().execute({ actions: [{ action: 'get-info' }] }, {});
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/File path/i);
  });

  test('rejects nonexistent file', async () => {
    const r = await new PdfTool().execute(
      { actions: [{ action: 'get-info', filePath: path.join(tmpDir, 'nope.pdf') }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/File not found/);
  });

  test('rejects non-PDF extension', async () => {
    const r = await new PdfTool().execute(
      { actions: [{ action: 'get-info', filePath: txtPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Not a PDF/);
  });

  test('rejects unknown action even on a valid .pdf file', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData());
    const r = await tool.execute(
      { actions: [{ action: 'frobnicate', filePath: dummyPdfPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Unknown action/);
  });

  test('resolves relative filePath against projectDir', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 2 }));
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: 'sample.pdf' }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(true);
    expect(r.info.pageCount).toBe(2);
  });

  test('surfaces parsePdf failure as a structured error', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockRejectedValue(new Error('parser exploded'));
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: dummyPdfPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/parser exploded/);
  });

  test('reports when pdf2json fails to load', async () => {
    const tool = new PdfTool();
    // Pretend a previous import attempt failed.
    tool.pdfParserError = 'simulated install miss';
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: dummyPdfPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/PDF parsing not available/);
    expect(r.output).toMatch(/simulated install miss/);
  });
});

// ───────────────────────────── getInfo ─────────────────────────────────

describe('PdfTool — getInfo', () => {
  test('returns metadata when Meta block is populated', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({
      pageCount: 5,
      meta: {
        Title: 'T', Author: 'A', Subject: 'S', Creator: 'C',
        Producer: 'P', CreationDate: 'D:20260101', ModDate: 'D:20260102',
      },
    }));
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: dummyPdfPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.info).toMatchObject({
      pageCount: 5, title: 'T', author: 'A', subject: 'S',
      creator: 'C', producer: 'P', creationDate: 'D:20260101',
      modificationDate: 'D:20260102',
    });
    expect(r.output).toMatch(/Pages: 5/);
    expect(r.output).toMatch(/Title: T/);
  });

  test('handles missing Meta and Pages blocks gracefully', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue({}); // no Pages, no Meta
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: dummyPdfPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.info.pageCount).toBe(0);
    expect(r.info.title).toBeNull();
  });
});

// ──────────────────────────── readPages ────────────────────────────────

describe('PdfTool — readPages', () => {
  test('reads the default page window when no range is supplied', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 3 }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 1, endPage: 4 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.totalPages).toBe(3);
    expect(r.pagesRead).toBe(3);
    expect(r.pages[0].pageNumber).toBe(1);
    expect(r.pages[0].content).toMatch(/hello world/);
  });

  test('honours explicit startPage / endPage', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 10 }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 3, endPage: 6 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.startPage).toBe(3);
    expect(r.endPage).toBe(6);
    expect(r.pagesRead).toBe(3);
    expect(r.pages.map(p => p.pageNumber)).toEqual([3, 4, 5]);
  });

  test('clamps startPage < 1 up to 1', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 2 }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 0, endPage: 3 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.startPage).toBe(1);
  });

  test('clamps endPage beyond totalPages', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 2 }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 1, endPage: 99 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.endPage).toBe(3); // totalPages + 1
  });

  test('rejects startPage > totalPages', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 2 }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 10, endPage: 12 }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/exceeds total pages/);
  });

  test('warns when more than 10 pages are requested', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({ pageCount: 30 }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 1, endPage: 25 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.warnings.length).toBeGreaterThan(0);
    expect(r.warnings[0]).toMatch(/10 pages/);
  });

  test('substitutes a placeholder for pages with no text', async () => {
    const tool = new PdfTool();
    jest.spyOn(tool, 'parsePdf').mockResolvedValue(makePdfData({
      pageCount: 1,
      pageText: '',
    }));
    const r = await tool.execute(
      { actions: [{ action: 'read-pages', filePath: dummyPdfPath, startPage: 1, endPage: 2 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.output).toMatch(/No text content/);
  });
});

// ──────────────────────────── createPdf ────────────────────────────────

describe('PdfTool — createPdf (without invoking Puppeteer)', () => {
  test('rejects missing outputPath', async () => {
    const tool = new PdfTool();
    const r = await tool.execute(
      { actions: [{ action: 'create-pdf', htmlContent: '<p>x</p>' }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Output path/i);
  });

  test('rejects missing htmlContent', async () => {
    const tool = new PdfTool();
    const r = await tool.execute(
      { actions: [{ action: 'create-pdf', outputPath: 'x.pdf' }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/HTML content/i);
  });

  test('blocks path traversal outside projectDir', async () => {
    const tool = new PdfTool();
    const r = await tool.execute(
      { actions: [{
        action: 'create-pdf',
        outputPath: '../../escape.pdf',
        htmlContent: '<p>x</p>',
      }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/traversal/i);
  });

  test('reports "puppeteer not available" without launching a browser', async () => {
    const tool = new PdfTool();
    // Pretend a previous import attempt failed.
    tool.puppeteerError = 'simulated browser miss';
    const r = await tool.execute(
      { actions: [{
        action: 'create-pdf',
        outputPath: 'should-not-be-written.pdf',
        htmlContent: '<p>x</p>',
      }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Puppeteer not available/);
    expect(r.output).toMatch(/simulated browser miss/);
    // The file must NOT have been created — failure happened before launch.
    await expect(fs.access(path.join(tmpDir, 'should-not-be-written.pdf')))
      .rejects.toThrow();
  });
});

// ────────────────────────── lazy loading ───────────────────────────────

describe('PdfTool — lazy module loading', () => {
  test('loadPdfParser caches success on subsequent calls', async () => {
    const tool = new PdfTool();
    expect(tool.pdfParserLoaded).toBe(false);
    expect(await tool.loadPdfParser()).toBe(true);
    expect(tool.pdfParserLoaded).toBe(true);
    expect(await tool.loadPdfParser()).toBe(true);
  });

  test('loadPdfParser short-circuits to false after a cached error', async () => {
    const tool = new PdfTool();
    tool.pdfParserError = 'cached miss';
    expect(await tool.loadPdfParser()).toBe(false);
    expect(tool.pdfParserLoaded).toBe(false);
  });

  test('loadPuppeteer caches success', async () => {
    const tool = new PdfTool();
    expect(await tool.loadPuppeteer()).toBe(true);
    expect(tool.puppeteerLoaded).toBe(true);
    expect(await tool.loadPuppeteer()).toBe(true);
  });

  test('loadPuppeteer short-circuits to false after a cached error', async () => {
    const tool = new PdfTool();
    tool.puppeteerError = 'cached miss';
    expect(await tool.loadPuppeteer()).toBe(false);
    expect(tool.puppeteerLoaded).toBe(false);
  });
});
