/**
 * docxTool.test.js — real-I/O tests for the Document (DOCX) tool.
 *
 * Strategy: no mocks. The tool wraps `docx` (write) and `mammoth` (read),
 * both already installed. A stub-driven test would tell us about as much
 * as the existing 0% coverage did — i.e. nothing. So we use a tmpDir,
 * write a real docx with the `docx` package once, and exercise the tool
 * against it for the read/info paths. Create-path tests write into the
 * same tmpDir and assert the file actually lands.
 *
 * Trade-off: each test does real I/O + module load on first hit; the
 * whole file takes a couple of seconds. That's an acceptable price for
 * coverage of a tool that previously had ZERO.
 */

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

let tmpDir;
let sampleDocxPath;
let txtPath;

beforeAll(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'loxia-docx-test-'));

  // Build a known-good sample using `docx` directly (not through the tool
  // we're testing). Keeps the read/info tests independent of the create
  // path so a regression in `createDocument` cannot mask a regression in
  // `getInfo` or `readDocument`.
  const docx = await import('docx');
  const { Document, Packer, Paragraph, TextRun, HeadingLevel } = docx;
  const doc = new Document({
    sections: [{
      children: [
        new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun('Sample Title')] }),
        new Paragraph({ children: [new TextRun('Hello world. This is a sample document for tests.')] }),
        new Paragraph({ children: [new TextRun('Second paragraph with more words.')] }),
      ],
    }],
  });
  const buf = await Packer.toBuffer(doc);
  sampleDocxPath = path.join(tmpDir, 'sample.docx');
  await fs.writeFile(sampleDocxPath, buf);

  // Wrong-extension file for negative test.
  txtPath = path.join(tmpDir, 'not-a-doc.txt');
  await fs.writeFile(txtPath, 'just text');
});

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

describe('DocxTool — identity & metadata', () => {
  test('constructor sets id, name, version', () => {
    const tool = new DocxTool();
    expect(tool.id).toBe('doc');
    expect(tool.name).toBe('Document Tool');
    expect(tool.version).toBe('1.0.0');
    expect(tool.requiresProject).toBe(false);
    expect(tool.docxLoaded).toBe(false);
    expect(tool.mammothLoaded).toBe(false);
  });

  test('getSupportedActions returns the three documented actions', () => {
    const tool = new DocxTool();
    expect(tool.getSupportedActions()).toEqual(['get-info', 'read', 'create']);
  });

  test('getDescription mentions all three actions', () => {
    const tool = new DocxTool();
    const desc = tool.getDescription();
    expect(desc).toMatch(/get-info/);
    expect(desc).toMatch(/\bread\b/);
    expect(desc).toMatch(/\bcreate\b/);
    expect(desc).toMatch(/filePath/);
  });
});

describe('DocxTool — parseParameters', () => {
  test('extracts action / filePath / format from tagged content', () => {
    const tool = new DocxTool();
    const result = tool.parseParameters('<action>read</action><filePath>foo.docx</filePath><format>html</format>');
    expect(result.actions).toHaveLength(1);
    expect(result.actions[0]).toMatchObject({
      action: 'read',
      filePath: 'foo.docx',
      format: 'html',
    });
  });

  test('defaults action=get-info, format=text when no tags present', () => {
    const tool = new DocxTool();
    const result = tool.parseParameters('');
    expect(result.actions[0]).toMatchObject({
      action: 'get-info',
      filePath: '',
      format: 'text',
    });
  });

  test('trims whitespace from extracted values', () => {
    const tool = new DocxTool();
    const result = tool.parseParameters('<action>  read  </action><filePath>  ./x.docx  </filePath>');
    expect(result.actions[0].action).toBe('read');
    expect(result.actions[0].filePath).toBe('./x.docx');
  });

  test('wraps TagParser failures in a friendly error', () => {
    // TagParser.extractContent is lenient with weird inputs (regex coerces),
    // so to exercise the catch branch we make the parser throw on purpose.
    const spy = jest.spyOn(TagParser, 'extractContent').mockImplementation(() => {
      throw new Error('tag parse exploded');
    });
    try {
      const tool = new DocxTool();
      expect(() => tool.parseParameters('<action>read</action>'))
        .toThrow(/Failed to parse Document tool parameters.*tag parse exploded/);
    } finally {
      spy.mockRestore();
    }
  });
});

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

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

  test('returns error when read/get-info has no filePath', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({ actions: [{ action: 'get-info' }] }, {});
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/File path/i);
  });

  test('returns error when file does not exist', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: path.join(tmpDir, 'does-not-exist.docx') }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/File not found/);
  });

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

  test('rejects unknown action on existing .docx', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'frobnicate', filePath: sampleDocxPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Unknown action/);
  });

  test('resolves relative path against projectDir', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: 'sample.docx' }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(true);
    expect(r.info.fileName).toBe('sample.docx');
  });

  test('accepts a .doc extension (legacy)', async () => {
    // Copy the sample to .doc to verify the extension check accepts it.
    const docPath = path.join(tmpDir, 'legacy.doc');
    await fs.copyFile(sampleDocxPath, docPath);
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: docPath }] },
      {},
    );
    expect(r.success).toBe(true);
  });
});

describe('DocxTool — getInfo', () => {
  test('returns metadata and word count for a real document', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: sampleDocxPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.action).toBe('get-info');
    expect(r.info.fileName).toBe('sample.docx');
    expect(r.info.fileSize).toBeGreaterThan(0);
    expect(r.info.modified).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO timestamp
    // "Sample Title", "Hello world.", "This is a sample document for tests.",
    // "Second paragraph with more words." → at least 12 words.
    expect(r.info.wordCount).toBeGreaterThanOrEqual(12);
    expect(r.output).toMatch(/Document Info/);
    expect(r.message).toMatch(/sample\.docx/);
  });
});

describe('DocxTool — readDocument', () => {
  test('reads text content (default format)', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'read', filePath: sampleDocxPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.format).toBe('text');
    expect(r.content).toMatch(/Hello world/);
    expect(r.content).toMatch(/Second paragraph/);
    expect(r.output).toMatch(/Document Content/);
    expect(Array.isArray(r.warnings)).toBe(true);
  });

  test('reads HTML content when format=html', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'read', filePath: sampleDocxPath, format: 'html' }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.format).toBe('html');
    // mammoth wraps content in tags like <p>, <h1>, etc.
    expect(r.content).toMatch(/<[a-z][\s\S]*?>/);
    expect(r.content).toMatch(/Hello world/);
  });

  test('treats unknown format as text', async () => {
    const tool = new DocxTool();
    const r = await tool.execute(
      { actions: [{ action: 'read', filePath: sampleDocxPath, format: 'markdown' }] },
      {},
    );
    expect(r.success).toBe(true);
    // Format flag still echoes back what the caller asked for, but the
    // content path falls through to extractRawText (no HTML tags).
    expect(r.content).toMatch(/Hello world/);
    expect(r.content).not.toMatch(/<p>/i);
  });
});

describe('DocxTool — createDocument', () => {
  test('creates a document covering every element type', async () => {
    const tool = new DocxTool();
    const outPath = 'created-everything.docx';
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: outPath,
        content: {
          title: 'Full Test',
          author: 'Test Suite',
          sections: [{
            children: [
              { type: 'heading', level: 1, text: 'Heading 1' },
              { type: 'heading', level: 2, text: 'Heading 2 centered', alignment: 'center' },
              { type: 'heading', level: 3, text: 'Heading 3' },
              { type: 'paragraph', text: 'Bold italic large right', bold: true, italic: true, fontSize: 28, alignment: 'right' },
              { type: 'paragraph', text: 'Plain paragraph' },
              { type: 'table', headers: ['Col A', 'Col B'], rows: [['1', '2'], ['3', '4']], widths: [50, 50] },
              { type: 'table', headers: ['NoRows'] }, // table-with-only-headers branch
              { type: 'list', ordered: true, items: ['First', 'Second'] },
              { type: 'list', ordered: false, items: ['Bullet A', 'Bullet B'] },
              { type: 'list', ordered: false, items: [] }, // empty list
              { type: 'pageBreak' },
              { type: 'unknown-element', text: 'should be skipped' },
            ],
          }],
        },
      }],
    }, { projectDir: tmpDir });

    expect(r.success).toBe(true);
    expect(r.action).toBe('create');
    expect(r.outputPath).toMatch(/created-everything\.docx$/);
    expect(r.fileSize).toBeGreaterThan(0);
    expect(r.output).toMatch(/Sections: 1/);
    await fs.access(r.outputPath); // file actually lands on disk
  });

  test('handles section with no children gracefully', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: 'no-children.docx',
        content: { sections: [{}, { children: 'not-an-array' }, { children: [] }] },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(true);
  });

  test('handles table row that is not an array', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: 'odd-table.docx',
        content: {
          sections: [{
            children: [
              { type: 'table', headers: ['A'], rows: [['ok'], 'not-an-array-row', ['fine']] },
            ],
          }],
        },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(true);
  });

  test('rejects missing outputPath', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{ action: 'create', content: { sections: [] } }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Output path/i);
  });

  test('rejects missing content', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{ action: 'create', outputPath: 'foo.docx' }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/sections/i);
  });

  test('rejects content without sections array', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{ action: 'create', outputPath: 'foo.docx', content: { title: 'No sections' } }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/sections/i);
  });

  test('blocks path traversal outside projectDir', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: '../../escape.docx',
        content: { sections: [{ children: [] }] },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/traversal/i);
  });

  test('appends .docx extension when missing', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: 'no-extension',
        content: { sections: [{ children: [{ type: 'paragraph', text: 'x' }] }] },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(true);
    expect(r.outputPath).toMatch(/no-extension\.docx$/);
    await fs.access(r.outputPath);
  });

  test('creates nested output directories', async () => {
    const tool = new DocxTool();
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: path.join('nested', 'deep', 'doc.docx'),
        content: { sections: [{ children: [{ type: 'paragraph', text: 'hi' }] }] },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(true);
    await fs.access(r.outputPath);
  });

  test('absolute outputPath under projectDir is accepted', async () => {
    const tool = new DocxTool();
    const absPath = path.join(tmpDir, 'abs-out.docx');
    const r = await tool.execute({
      actions: [{
        action: 'create',
        outputPath: absPath,
        content: { sections: [{ children: [{ type: 'paragraph', text: 'a' }] }] },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(true);
    expect(r.outputPath).toBe(absPath);
  });
});

describe('DocxTool — lazy module loading', () => {
  test('loadDocx is idempotent and flips the loaded flag', async () => {
    const tool = new DocxTool();
    expect(tool.docxLoaded).toBe(false);
    expect(await tool.loadDocx()).toBe(true);
    expect(tool.docxLoaded).toBe(true);
    // Second call short-circuits via the cached flag.
    expect(await tool.loadDocx()).toBe(true);
  });

  test('loadMammoth is idempotent and flips the loaded flag', async () => {
    const tool = new DocxTool();
    expect(tool.mammothLoaded).toBe(false);
    expect(await tool.loadMammoth()).toBe(true);
    expect(tool.mammothLoaded).toBe(true);
    expect(await tool.loadMammoth()).toBe(true);
  });

  test('loadDocx returns false and caches the error when the module is missing', async () => {
    // Force the "previous failure" branch: set the error string by hand
    // and confirm the early-return path returns false without re-throwing.
    const tool = new DocxTool();
    tool.docxError = 'simulated install failure';
    expect(await tool.loadDocx()).toBe(false);
    expect(tool.docxLoaded).toBe(false);
  });

  test('loadMammoth returns false and caches the error when the module is missing', async () => {
    const tool = new DocxTool();
    tool.mammothError = 'simulated install failure';
    expect(await tool.loadMammoth()).toBe(false);
    expect(tool.mammothLoaded).toBe(false);
  });
});
