import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { afterEach, describe, expect, it } from 'vitest';
import AdmZip from 'adm-zip';
import { executeTool } from './office.js';
import type { ToolContext, ToolsConfig } from './core.js';

// --- Helpers (mirrors office.test.ts house style) ---

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

function makeContext(workspacePath: string, toolsConfig?: ToolsConfig): ToolContext {
  return { workspacePath, editAllowed: true, ...(toolsConfig ? { toolsConfig } : {}) };
}

function inputPath(workspacePath: string, name: string): string {
  const dir = path.join(workspacePath, 'input');
  fs.mkdirSync(dir, { recursive: true });
  return path.join(dir, name);
}

/**
 * Write a fixture that PASSES validateFileFormat (correct magic bytes for the
 * expected format) but exceeds `sizeBytes`. The size-limit check in office.ts
 * runs AFTER the magic-byte format check but BEFORE any real parse, so the file
 * does not need to be a valid Office document — only the leading magic bytes +
 * the right extension matter, plus total size > the configured cap.
 *
 * OOXML magic = "PK\x03\x04" (ReadExcel/ReadDocx/ReadPPTX). PDF magic = "%PDF-".
 */
function writeOversizedOoxml(filePath: string, sizeBytes: number): void {
  const buf = Buffer.alloc(sizeBytes, 0);
  // Local file header signature for a ZIP / OOXML container.
  buf[0] = 0x50; // P
  buf[1] = 0x4b; // K
  buf[2] = 0x03;
  buf[3] = 0x04;
  fs.writeFileSync(filePath, buf);
}

function writeOversizedPdf(filePath: string, sizeBytes: number): void {
  const buf = Buffer.alloc(sizeBytes, 0x20); // pad with spaces
  Buffer.from('%PDF-1.4\n').copy(buf, 0);
  fs.writeFileSync(filePath, buf);
}

/**
 * Build a REAL .pptx (zip) whose total UNCOMPRESSED size exceeds the configured
 * uncompressed cap, while staying tiny on disk (highly compressible zeros) — i.e.
 * a decompression-bomb shape. adm-zip records the true uncompressed size in each
 * entry's header.size, which is exactly what office.ts sums for ZIP-bomb detection.
 */
function writeZipBomb(filePath: string, uncompressedBytes: number): void {
  const zip = new AdmZip();
  // A single fat, highly-compressible entry: large uncompressed, tiny on disk.
  zip.addFile('ppt/slides/slide1.xml', Buffer.alloc(uncompressedBytes, 0));
  zip.writeZip(filePath);
}

const MB = 1024 * 1024;

describe('office tools — file size limits', () => {
  let workspacePath = '';

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

  // --- 1. ReadPPTX file-size cap (officePptxMaxSizeMb) ---

  it('ReadPPTX rejects a file exceeding officePptxMaxSizeMb', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'big.pptx');
    writeOversizedOoxml(fp, 2 * MB); // 2MB > 1MB cap
    const ctx = makeContext(workspacePath, { officePptxMaxSizeMb: 1 });

    const result = await executeTool('ReadPPTX', { path: 'input/big.pptx' }, ctx);

    expect(result?.isError).toBe(true);
    expect(result?.output).toContain('exceeds limit of 1MB');
    expect(result?.output).toContain('File size');
    // Distinct from the format-mismatch error family (Japanese guidance text).
    expect(result?.output).not.toContain('ReadToolDoc');
  });

  // --- 2. ReadPPTX ZIP-bomb path (officePptxMaxUncompressedMb) ---

  it('ReadPPTX rejects a ZIP bomb whose uncompressed size exceeds officePptxMaxUncompressedMb', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'bomb.pptx');
    // ~3MB uncompressed of zeros -> a few KB on disk (well under the 50MB file cap),
    // so it passes the file-size check and reaches the uncompressed-size guard.
    writeZipBomb(fp, 3 * MB);
    const onDisk = fs.statSync(fp).size;
    expect(onDisk).toBeLessThan(50 * MB); // sanity: stays under default file cap

    const ctx = makeContext(workspacePath, { officePptxMaxUncompressedMb: 1 });
    const result = await executeTool('ReadPPTX', { path: 'input/bomb.pptx' }, ctx);

    expect(result?.isError).toBe(true);
    expect(result?.output).toContain('ZIP bomb detected');
    expect(result?.output).toContain('exceeds limit of 1MB');
  });

  it('ReadPPTX accepts a small zip when uncompressed size is under the cap', async () => {
    // Guards against the bomb check being over-eager / regressed to always-fire.
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'tiny.pptx');
    writeZipBomb(fp, 4 * 1024); // 4KB uncompressed, well under 1MB cap

    const ctx = makeContext(workspacePath, { officePptxMaxUncompressedMb: 1 });
    const result = await executeTool('ReadPPTX', { path: 'input/tiny.pptx' }, ctx);

    // It will fail later for other reasons (no presentation.xml / no slides parsed),
    // but it must NOT be rejected by the ZIP-bomb guard.
    expect(result?.output ?? '').not.toContain('ZIP bomb detected');
  });

  // --- 3. ReadExcel / ReadDocx / ReadPdf each reject when size > their cap ---

  it('ReadExcel rejects a file exceeding officeExcelMaxSizeMb', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'big.xlsx');
    writeOversizedOoxml(fp, 2 * MB);
    const ctx = makeContext(workspacePath, { officeExcelMaxSizeMb: 1 });

    const result = await executeTool('ReadExcel', { path: 'input/big.xlsx' }, ctx);

    expect(result?.isError).toBe(true);
    expect(result?.output).toContain('File size');
    expect(result?.output).toContain('exceeds limit of 1MB');
    // Distinct from the format-mismatch error (which names other tools / Read).
    expect(result?.output).not.toContain('ReadDocx');
  });

  it('ReadDocx rejects a file exceeding officeDocxMaxSizeMb', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'big.docx');
    writeOversizedOoxml(fp, 2 * MB);
    const ctx = makeContext(workspacePath, { officeDocxMaxSizeMb: 1 });

    const result = await executeTool('ReadDocx', { path: 'input/big.docx' }, ctx);

    expect(result?.isError).toBe(true);
    expect(result?.output).toContain('File size');
    expect(result?.output).toContain('exceeds limit of 1MB');
  });

  it('ReadPdf rejects a file exceeding officePdfMaxSizeMb', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'big.pdf');
    writeOversizedPdf(fp, 2 * MB);
    const ctx = makeContext(workspacePath, { officePdfMaxSizeMb: 1 });

    const result = await executeTool('ReadPdf', { path: 'input/big.pdf' }, ctx);

    expect(result?.isError).toBe(true);
    expect(result?.output).toContain('File size');
    expect(result?.output).toContain('exceeds limit of 1MB');
  });

  // The size-limit error is structurally DISTINCT from the format-mismatch error.
  it('size-limit error differs from the format-mismatch error', async () => {
    workspacePath = makeWorkspace();

    // (a) size-limit: valid OOXML magic + oversize -> FILE_TOO_LARGE message.
    const bigFp = inputPath(workspacePath, 'big.xlsx');
    writeOversizedOoxml(bigFp, 2 * MB);
    const sizeRes = await executeTool(
      'ReadExcel',
      { path: 'input/big.xlsx' },
      makeContext(workspacePath, { officeExcelMaxSizeMb: 1 }),
    );

    // (b) format-mismatch: PDF bytes handed to ReadExcel -> guidance message.
    const pdfFp = inputPath(workspacePath, 'wrong.xlsx');
    writeOversizedPdf(pdfFp, 4 * 1024); // tiny so size cap is irrelevant
    const fmtRes = await executeTool(
      'ReadExcel',
      { path: 'input/wrong.xlsx' },
      makeContext(workspacePath, { officeExcelMaxSizeMb: 1 }),
    );

    expect(sizeRes?.isError).toBe(true);
    expect(fmtRes?.isError).toBe(true);
    expect(sizeRes?.output).toContain('exceeds limit of');
    expect(fmtRes?.output).not.toContain('exceeds limit of');
    expect(sizeRes?.output).not.toBe(fmtRes?.output);
  });

  // --- 4. Config override changes the threshold ---

  it('config override: a file that passes at default fails at a lowered officeExcelMaxSizeMb', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'mid.xlsx');
    // 2MB: under the 10MB default Excel cap, over a lowered 1MB cap.
    writeOversizedOoxml(fp, 2 * MB);

    // Default cap (no override) -> NOT rejected for size (it fails later on parse,
    // but never with the FILE_TOO_LARGE message).
    const defaultRes = await executeTool(
      'ReadExcel',
      { path: 'input/mid.xlsx' },
      makeContext(workspacePath),
    );
    expect(defaultRes?.output ?? '').not.toContain('exceeds limit of');

    // Lowered cap -> rejected for size.
    const loweredRes = await executeTool(
      'ReadExcel',
      { path: 'input/mid.xlsx' },
      makeContext(workspacePath, { officeExcelMaxSizeMb: 1 }),
    );
    expect(loweredRes?.isError).toBe(true);
    expect(loweredRes?.output).toContain('exceeds limit of 1MB');
  });

  it('config override: raising officePptxMaxUncompressedMb lets a previously-bombed zip through the guard', async () => {
    workspacePath = makeWorkspace();
    const fp = inputPath(workspacePath, 'fat.pptx');
    writeZipBomb(fp, 3 * MB); // 3MB uncompressed

    // Low cap (1MB) -> tripped by the bomb guard.
    const lowRes = await executeTool(
      'ReadPPTX',
      { path: 'input/fat.pptx' },
      makeContext(workspacePath, { officePptxMaxUncompressedMb: 1 }),
    );
    expect(lowRes?.output).toContain('ZIP bomb detected');

    // Raised cap (10MB) -> NOT tripped by the bomb guard (fails later on missing slides).
    const highRes = await executeTool(
      'ReadPPTX',
      { path: 'input/fat.pptx' },
      makeContext(workspacePath, { officePptxMaxUncompressedMb: 10 }),
    );
    expect(highRes?.output ?? '').not.toContain('ZIP bomb detected');
  });
});
