/**
 * officeTool unit + fidelity tests.
 *
 * Coverage:
 *   - Envelope handling: plural canonical, legacy singular, action/type alias
 *   - Validation: unknown type, missing outputPath/filePath/content, wrong ext
 *   - doc.create: writes a real .docx that mammoth can read back
 *   - doc.info / doc.read: roundtrips text + reports word count
 *   - sheet.create: writes a real .xlsx with formulas + styling that exceljs
 *     can read back, and the round-trip preserves the formula
 *   - sheet.info / sheet.read: roundtrip data + report
 *   - pres.create: writes a real .pptx that pptxgenjs produces and that has
 *     the expected slide count
 *   - pres.info: counts slides via zip inspection
 *   - Path traversal protection
 *   - Multi-action envelope: returns summary {total, ok, failed}
 *   - Per-action failure is captured without aborting the batch
 */
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
import path from 'path';
import os from 'os';
import fs from 'fs/promises';
import { existsSync } from 'fs';

import OfficeTool from '../officeTool.js';

const SILENT_LOGGER = { info() {}, warn() {}, error() {}, debug() {} };

let tmpDir;
let context;

beforeAll(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'office-tool-test-'));
  context = { projectDir: tmpDir, agentId: 'test-agent' };
});

afterAll(async () => {
  if (tmpDir && existsSync(tmpDir)) {
    await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
  }
});

function makeTool() {
  return new OfficeTool({}, SILENT_LOGGER);
}

/* ------------------------------------------------------------------------ */
/* Envelope + validation                                                    */
/* ------------------------------------------------------------------------ */

describe('officeTool — envelope handling', () => {
  test('rejects empty actions array', async () => {
    const t = makeTool();
    const r = await t.execute({ actions: [] }, context);
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/No actions/i);
  });

  test('rejects unknown action type', async () => {
    const t = makeTool();
    const r = await t.execute({ actions: [{ type: 'foo.bar' }] }, context);
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Unknown action/i);
  });

  test('accepts legacy singular envelope', async () => {
    const t = makeTool();
    // Singular {action, ...} should be promoted to a one-element actions array.
    const filePath = path.join(tmpDir, 'doesnt-exist.docx');
    const r = await t.execute({ action: 'doc.info', filePath }, context);
    // The action runs (single-action result is returned directly) and reports
    // a not-found error — but it DOES dispatch, proving the legacy envelope is honoured.
    expect(r.success).toBe(false);
    expect(r.type).toBe('doc.info');
    expect(r.error).toMatch(/File not found/i);
  });

  test('accepts "action" as the discriminator key (alongside "type")', async () => {
    const t = makeTool();
    const r = await t.execute({ actions: [{ action: 'doc.info', filePath: '/nope.docx' }] }, context);
    expect(r.type).toBe('doc.info');
  });
});

/* ------------------------------------------------------------------------ */
/* doc.create + doc.read + doc.info (fidelity)                              */
/* ------------------------------------------------------------------------ */

describe('officeTool — doc.* fidelity', () => {
  const outPath = () => path.join(tmpDir, 'docs', `report-${Date.now()}-${Math.random().toString(36).slice(2)}.docx`);

  test('doc.create writes a .docx and reports the file size', async () => {
    const t = makeTool();
    const target = outPath();
    const r = await t.execute({
      actions: [{
        type: 'doc.create',
        outputPath: target,
        content: {
          title: 'Test Doc',
          author: 'Loxia Test',
          sections: [{
            children: [
              { type: 'heading', level: 1, text: 'Hello World' },
              { type: 'paragraph', text: 'The quick brown fox jumps over the lazy dog.' },
              { type: 'list', items: ['One', 'Two', 'Three'] },
              { type: 'callout', label: 'Note', content: 'Watch out for this.' },
            ],
          }],
        },
      }],
    }, context);
    expect(r.success).toBe(true);
    expect(r.outputPath).toBe(target);
    expect(r.fileSize).toBeGreaterThan(500);
    expect(existsSync(target)).toBe(true);
  });

  test('doc.create auto-appends .docx if the agent forgot it', async () => {
    const t = makeTool();
    const target = path.join(tmpDir, 'docs', 'no-ext');
    const r = await t.execute({
      actions: [{
        type: 'doc.create',
        outputPath: target,
        content: { sections: [{ children: ['Body'] }] },
      }],
    }, context);
    expect(r.success).toBe(true);
    expect(r.outputPath).toMatch(/\.docx$/);
  });

  test('doc.info round-trips the generated doc and reports a word count', async () => {
    const t = makeTool();
    const target = outPath();
    await t.execute({
      actions: [{
        type: 'doc.create',
        outputPath: target,
        content: { sections: [{ children: [
          { type: 'paragraph', text: 'one two three four five six' },
        ]}]},
      }],
    }, context);
    const info = await t.execute({ actions: [{ type: 'doc.info', filePath: target }] }, context);
    expect(info.success).toBe(true);
    expect(info.info.wordCount).toBeGreaterThanOrEqual(6);
  });

  test('doc.read round-trips the generated doc as text', async () => {
    const t = makeTool();
    const target = outPath();
    await t.execute({
      actions: [{
        type: 'doc.create',
        outputPath: target,
        content: { sections: [{ children: [
          { type: 'paragraph', text: 'UniqueMarkerString_42' },
        ]}]},
      }],
    }, context);
    const read = await t.execute({ actions: [{ type: 'doc.read', filePath: target, format: 'text' }] }, context);
    expect(read.success).toBe(true);
    expect(read.format).toBe('text');
    expect(read.content).toMatch(/UniqueMarkerString_42/);
  });

  test('doc.read returns HTML when format=html', async () => {
    const t = makeTool();
    const target = outPath();
    await t.execute({
      actions: [{
        type: 'doc.create',
        outputPath: target,
        content: { sections: [{ children: [{ type: 'heading', level: 2, text: 'Header X' }] }] },
      }],
    }, context);
    const read = await t.execute({ actions: [{ type: 'doc.read', filePath: target, format: 'html' }] }, context);
    expect(read.success).toBe(true);
    expect(read.format).toBe('html');
    expect(read.content).toMatch(/<h\d[^>]*>.*Header X/);
  });
});

/* ------------------------------------------------------------------------ */
/* sheet.create + sheet.read + sheet.info (fidelity)                        */
/* ------------------------------------------------------------------------ */

describe('officeTool — sheet.* fidelity', () => {
  const outPath = () => path.join(tmpDir, 'sheets', `data-${Date.now()}-${Math.random().toString(36).slice(2)}.xlsx`);

  test('sheet.create writes a .xlsx with multiple sheets', async () => {
    const t = makeTool();
    const target = outPath();
    const r = await t.execute({
      actions: [{
        type: 'sheet.create',
        outputPath: target,
        content: {
          sheets: [
            {
              name: 'Q1',
              columns: [
                { header: 'Region', key: 'region', width: 18 },
                { header: 'Sales', key: 'sales', width: 12, type: 'currency' },
              ],
              rows: [
                { region: 'East',  sales: 1200 },
                { region: 'West',  sales: 980 },
                { region: 'North', sales: 1430 },
              ],
              headerStyle: { bold: true, fontColor: '#FFFFFF', fill: '#1F3864' },
              alternateRowFill: '#F4F6FB',
              freezeRow: 1,
              autoFilter: true,
            },
            {
              name: 'Q2',
              columns: [{ header: 'A', key: 'a' }],
              rows: [{ a: 1 }, { a: 2 }],
            },
          ],
        },
      }],
    }, context);
    expect(r.success).toBe(true);
    expect(r.sheetCount).toBe(2);
    expect(r.fileSize).toBeGreaterThan(500);
  });

  test('sheet.create preserves inline formulas in object-shaped rows', async () => {
    const t = makeTool();
    const target = outPath();
    const create = await t.execute({
      actions: [{
        type: 'sheet.create',
        outputPath: target,
        content: {
          sheets: [{
            name: 'Calc',
            columns: [
              { header: 'A', key: 'a', type: 'number' },
              { header: 'B', key: 'b', type: 'number' },
              { header: 'Sum', key: 'sum', type: 'number' },
            ],
            rows: [
              { a: 10, b: 20, sum: '=A2+B2' },
              { a: 5,  b: 7,  sum: '=A3+B3' },
            ],
          }],
        },
      }],
    }, context);
    expect(create.success).toBe(true);

    const read = await t.execute({
      actions: [{ type: 'sheet.read', filePath: target, sheetName: 'Calc', includeFormulas: true }],
    }, context);
    expect(read.success).toBe(true);
    // Header row + 2 data rows
    expect(read.rows.length).toBe(3);
    // Formulas come back with a leading "="
    expect(String(read.rows[1][2])).toMatch(/^=A2\+B2$/);
  });

  test('sheet.info round-trips name + row counts', async () => {
    const t = makeTool();
    const target = outPath();
    await t.execute({
      actions: [{
        type: 'sheet.create',
        outputPath: target,
        content: { sheets: [{ name: 'OnlyOne', columns: [{ header: 'X', key: 'x' }], rows: [{ x: 1 }, { x: 2 }, { x: 3 }] }] },
      }],
    }, context);
    const info = await t.execute({ actions: [{ type: 'sheet.info', filePath: target }] }, context);
    expect(info.success).toBe(true);
    expect(info.info.sheetCount).toBe(1);
    expect(info.info.sheets[0].name).toBe('OnlyOne');
    expect(info.info.sheets[0].rowCount).toBeGreaterThanOrEqual(3); // header + rows
  });
});

/* ------------------------------------------------------------------------ */
/* pres.create + pres.info (fidelity)                                       */
/* ------------------------------------------------------------------------ */

describe('officeTool — pres.* fidelity', () => {
  const outPath = () => path.join(tmpDir, 'decks', `deck-${Date.now()}-${Math.random().toString(36).slice(2)}.pptx`);

  test('pres.create writes a .pptx with the requested number of slides', async () => {
    const t = makeTool();
    const target = outPath();
    const r = await t.execute({
      actions: [{
        type: 'pres.create',
        outputPath: target,
        content: {
          title: 'Bench Deck',
          theme: { primaryColor: '#1F3864', accentColor: '#2E75B6' },
          slides: [
            { layout: 'title', title: 'Welcome', subtitle: 'A test deck' },
            { layout: 'section-header', title: 'Section A' },
            { layout: 'content', title: 'Bullets',
              content: [{ type: 'bullets', items: ['First', 'Second', 'Third'], x: 0.5, y: 1.5, w: 9, h: 4 }],
            },
            { layout: 'content', title: 'Table',
              content: [{ type: 'table', headers: ['A', 'B'], rows: [['1', '2'], ['3', '4']], x: 0.5, y: 1.5, w: 9, h: 2 }],
            },
          ],
        },
      }],
    }, context);
    expect(r.success).toBe(true);
    expect(r.slideCount).toBe(4);
    expect(r.fileSize).toBeGreaterThan(1000);
  }, 15000);

  test('pres.info reports the slide count via zip inspection', async () => {
    const t = makeTool();
    const target = outPath();
    await t.execute({
      actions: [{
        type: 'pres.create',
        outputPath: target,
        content: { slides: [{ title: 'A' }, { title: 'B' }, { title: 'C' }] },
      }],
    }, context);
    const info = await t.execute({ actions: [{ type: 'pres.info', filePath: target }] }, context);
    expect(info.success).toBe(true);
    expect(info.info.slideCount).toBe(3);
  });

  // ─── Themed presentation system (string theme routes to the new builder) ──

  test('pres.system lists themes, archetypes, backgrounds — no file needed', async () => {
    const t = makeTool();
    const r = await t.execute({ actions: [{ type: 'pres.system' }] }, context);
    expect(r.success).toBe(true);
    expect(r.themes.map(x => x.id)).toEqual(expect.arrayContaining(['editorial-warm', 'tech-bold', 'data-heavy']));
    expect(r.archetypes).toEqual(expect.arrayContaining(['hero', 'agenda', 'statCallout', 'twoColChart', 'closing']));
    expect(r.backgrounds).toEqual(expect.arrayContaining(['solid', 'gradient', 'mesh', 'noise', 'geometric', 'grid']));
  });

  test('pres.create with string theme + archetype slides routes through the themed builder', async () => {
    const t = makeTool();
    const target = outPath();
    const r = await t.execute({
      actions: [{
        type: 'pres.create',
        outputPath: target,
        content: {
          title: 'Themed Deck',
          theme: 'editorial-warm',
          slides: [
            { archetype: 'hero', eyebrow: 'Q4', title: 'Hello', subtitle: 'World', author: 'Bench', date: '2026' },
            { archetype: 'statCallout', stat: '47%', label: 'YoY growth', footnote: 'Source: bench' },
            { archetype: 'bullets', title: 'Wins', items: ['A', 'B', 'C'] },
            { archetype: 'closing', title: 'Thanks' },
          ],
        },
      }],
    }, context);
    expect(r.success).toBe(true);
    expect(r.slideCount).toBe(4);
    expect(r.theme).toBe('editorial-warm');
    expect(r.palette).toBe('cream-charcoal');
    expect(r.fileSize).toBeGreaterThan(10_000);
  }, 15000);

  test('pres.read returns slide outline + tokens for an existing .pptx', async () => {
    const t = makeTool();
    const target = outPath();
    // Build a template deck with {{tokens}}.
    await t.execute({
      actions: [{
        type: 'pres.create',
        outputPath: target,
        content: {
          title: 'Template',
          theme: 'tech-bold',
          slides: [
            { archetype: 'hero', title: 'Hello {{customerName}}', subtitle: 'On {{date}}' },
            { archetype: 'statCallout', stat: '{{metric}}', label: 'For {{customerName}}' },
          ],
        },
      }],
    }, context);

    const r = await t.execute({ actions: [{ type: 'pres.read', filePath: target }] }, context);
    expect(r.success).toBe(true);
    expect(r.slideCount).toBe(2);
    expect(r.tokens).toEqual(expect.arrayContaining(['customerName', 'date', 'metric']));
  }, 15000);

  test('pres.fill replaces {{tokens}} with provided values', async () => {
    const t = makeTool();
    const templatePath = outPath();
    const filledPath = outPath();
    await t.execute({
      actions: [{
        type: 'pres.create',
        outputPath: templatePath,
        content: {
          theme: 'editorial-warm',
          slides: [
            { archetype: 'hero', title: 'Hi {{name}}', subtitle: '{{date}}' },
          ],
        },
      }],
    }, context);
    const r = await t.execute({
      actions: [{
        type: 'pres.fill',
        sourcePath: templatePath,
        outputPath: filledPath,
        values: { name: 'Acme', date: 'May 28' },
      }],
    }, context);
    expect(r.success).toBe(true);
    expect(r.replaced).toEqual({ name: 1, date: 1 });
    expect(r.unresolved).toEqual([]);
  }, 15000);
});

/* ------------------------------------------------------------------------ */
/* Multi-action envelope + partial success                                  */
/* ------------------------------------------------------------------------ */

describe('officeTool — multi-action + safety', () => {
  test('multi-action returns a summary with per-action results', async () => {
    const t = makeTool();
    const docPath   = path.join(tmpDir, 'multi.docx');
    const sheetPath = path.join(tmpDir, 'multi.xlsx');
    const r = await t.execute({
      actions: [
        { type: 'doc.create',   outputPath: docPath,   content: { sections: [{ children: ['Hello'] }] } },
        { type: 'sheet.create', outputPath: sheetPath, content: { sheets: [{ name: 'A', columns: [{ header: 'X', key: 'x' }], rows: [{ x: 1 }] }] } },
      ],
    }, context);
    expect(r.success).toBe(true);
    expect(r.summary).toEqual({ total: 2, ok: 2, failed: 0 });
    expect(r.results.length).toBe(2);
    expect(r.results[0].type).toBe('doc.create');
    expect(r.results[1].type).toBe('sheet.create');
  });

  test('one failed action does NOT abort the batch', async () => {
    const t = makeTool();
    const docPath = path.join(tmpDir, 'batch.docx');
    const r = await t.execute({
      actions: [
        { type: 'doc.create', outputPath: docPath, content: { sections: [{ children: ['ok'] }] } },
        { type: 'doc.read',   filePath: '/nonexistent/file.docx' }, // will fail
      ],
    }, context);
    expect(r.success).toBe(false);
    expect(r.summary.ok).toBe(1);
    expect(r.summary.failed).toBe(1);
    expect(r.results[0].success).toBe(true);
    expect(r.results[1].success).toBe(false);
  });

  test('rejects path traversal on create', async () => {
    const t = makeTool();
    const r = await t.execute({
      actions: [{
        type: 'doc.create',
        outputPath: path.join(tmpDir, '..', 'escape.docx'),
        content: { sections: [{ children: ['x'] }] },
      }],
    }, context);
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/traversal/i);
  });

  test('missing outputPath on create returns 400-style error', async () => {
    const t = makeTool();
    const r = await t.execute({
      actions: [{ type: 'doc.create', content: { sections: [] } }],
    }, context);
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/outputPath/i);
  });

  test('missing content on create returns 400-style error', async () => {
    const t = makeTool();
    const r = await t.execute({
      actions: [{ type: 'doc.create', outputPath: path.join(tmpDir, 'x.docx') }],
    }, context);
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/content/i);
  });
});
