/**
 * excelTool.test.js — real-I/O coverage for the Spreadsheet (Excel) tool.
 *
 * Strategy: real fs + real exceljs (it's an installed dep). Builds the
 * fixture xlsx once via exceljs DIRECTLY (so a regression in the tool's
 * create path can't mask a regression in the read path), then exercises
 * every action against tmpDir files.
 */

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

let tmpDir;
let xlsxPath;
let csvPath;
let txtPath;
let emptyXlsxPath;

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

  // Build the fixture xlsx directly with exceljs (the tool under test
  // is NOT used here — keeps the create/read tests independent).
  const ExcelJS = (await import('exceljs')).default;
  const wb = new ExcelJS.Workbook();

  const sheet1 = wb.addWorksheet('Q1');
  sheet1.columns = [
    { header: 'Product', key: 'product', width: 20 },
    { header: 'Qty',     key: 'qty',     width: 10 },
    { header: 'Price',   key: 'price',   width: 10 },
    { header: 'Total',   key: 'total',   width: 10 },
  ];
  sheet1.addRow({ product: 'Widget A', qty: 100, price: 9.99 });
  sheet1.addRow({ product: 'Widget B', qty: 50,  price: 19.99 });
  // Add a formula cell so includeFormulas branch has something real to find.
  sheet1.getCell('D2').value = { formula: 'B2*C2' };

  wb.addWorksheet('Q2'); // empty second sheet
  xlsxPath = path.join(tmpDir, 'sample.xlsx');
  await wb.xlsx.writeFile(xlsxPath);

  // Empty workbook (no sheets) for the "no sheets" branch.
  const wb2 = new ExcelJS.Workbook();
  emptyXlsxPath = path.join(tmpDir, 'empty.xlsx');
  await wb2.xlsx.writeFile(emptyXlsxPath);

  // CSV fixture
  csvPath = path.join(tmpDir, 'sample.csv');
  await fs.writeFile(csvPath, 'a,b,c\n1,2,3\n4,5,6\n');

  // Wrong-extension fixture
  txtPath = path.join(tmpDir, 'not-spreadsheet.txt');
  await fs.writeFile(txtPath, 'hi');
});

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

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

describe('ExcelTool — identity & description', () => {
  test('constructor sets documented identity', () => {
    const tool = new ExcelTool();
    expect(tool.id).toBe('spreadsheet');
    expect(tool.name).toBe('Spreadsheet Tool');
    expect(tool.version).toBe('1.0.0');
    expect(tool.requiresProject).toBe(false);
    expect(tool.excelLoaded).toBe(false);
  });

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

  test('getDescription mentions every action and key parameters', () => {
    const d = new ExcelTool().getDescription();
    expect(d).toMatch(/get-info/);
    expect(d).toMatch(/\bread\b/);
    expect(d).toMatch(/\bcreate\b/);
    expect(d).toMatch(/sheetName/);
    expect(d).toMatch(/headerStyle/);
  });
});

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

describe('ExcelTool — parseParameters', () => {
  test('extracts action / filePath / sheetName', () => {
    const out = new ExcelTool().parseParameters(
      '<action>read</action><filePath>data.xlsx</filePath><sheetName>Q1</sheetName>',
    );
    expect(out.actions[0]).toMatchObject({
      action: 'read', filePath: 'data.xlsx', sheetName: 'Q1',
    });
  });

  test('defaults to get-info, no filePath, undefined sheetName', () => {
    const out = new ExcelTool().parseParameters('');
    expect(out.actions[0]).toMatchObject({
      action: 'get-info', filePath: '', sheetName: undefined,
    });
  });

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

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

describe('ExcelTool — execute dispatcher', () => {
  test('returns error when actions is empty', async () => {
    const r = await new ExcelTool().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 ExcelTool().execute({}, {});
    expect(r.success).toBe(false);
  });

  test('requires filePath for read/get-info', async () => {
    const r = await new ExcelTool().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 ExcelTool().execute(
      { actions: [{ action: 'get-info', filePath: path.join(tmpDir, 'missing.xlsx') }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/File not found/);
  });

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

  test('rejects unknown action on valid .xlsx', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'frobnicate', filePath: xlsxPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Unknown action/);
  });

  test('resolves relative filePath against projectDir', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'get-info', filePath: 'sample.xlsx' }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(true);
    expect(r.sheets.length).toBeGreaterThan(0);
  });

  test('reports ExcelJS load failure clearly', async () => {
    const tool = new ExcelTool();
    tool.excelError = 'cached install miss';
    const r = await tool.execute(
      { actions: [{ action: 'get-info', filePath: xlsxPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/ExcelJS module not available/);
    expect(r.output).toMatch(/cached install miss/);
  });
});

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

describe('ExcelTool — getInfo', () => {
  test('returns sheet metadata for a real xlsx', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'get-info', filePath: xlsxPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.sheets).toHaveLength(2);
    expect(r.sheets[0]).toMatchObject({ name: 'Q1' });
    expect(r.sheets[0].rowCount).toBeGreaterThan(0);
    expect(r.sheets[0].columnCount).toBeGreaterThan(0);
    expect(r.output).toMatch(/Sheets: 2/);
    expect(r.output).toMatch(/Q1/);
  });

  test('handles a .csv via the csv branch', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'get-info', filePath: csvPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.sheets.length).toBeGreaterThan(0);
  });
});

// ─────────────────────────── readSheet ────────────────────────────────

describe('ExcelTool — readSheet', () => {
  test('reads the first sheet when no sheetName is given', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.sheetName).toBe('Q1');
    expect(r.rows.length).toBeGreaterThan(0);
  });

  test('honours sheetName when given', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath, sheetName: 'Q2' }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.sheetName).toBe('Q2');
  });

  test('errors when sheetName is missing from the workbook', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath, sheetName: 'Q5-does-not-exist' }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Sheet not found/);
    expect(r.output).toMatch(/Available sheets/);
  });

  test('respects startRow / endRow range', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath, sheetName: 'Q1', startRow: 2, endRow: 2 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.startRow).toBe(2);
    expect(r.endRow).toBe(2);
    expect(r.rowCount).toBe(1);
  });

  test('clamps startRow below 1', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath, sheetName: 'Q1', startRow: 0 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.startRow).toBe(1);
  });

  test('clamps endRow beyond totalRows', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath, sheetName: 'Q1', endRow: 9999 }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.endRow).toBe(r.totalRows);
  });

  test('returns formula text when includeFormulas=true', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: xlsxPath, sheetName: 'Q1', includeFormulas: true }] },
      {},
    );
    expect(r.success).toBe(true);
    // D2 was '=B2*C2' in the fixture.
    const flat = r.rows.flat().map(v => String(v));
    expect(flat.some(v => v.startsWith('='))).toBe(true);
  });

  test('errors when the workbook has no sheets', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: emptyXlsxPath }] },
      {},
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/No sheets/);
  });

  test('reads from a .csv file', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'read', filePath: csvPath }] },
      {},
    );
    expect(r.success).toBe(true);
    expect(r.rowCount).toBeGreaterThanOrEqual(2);
  });
});

// ──────────────────────── createSpreadsheet ───────────────────────────

describe('ExcelTool — createSpreadsheet', () => {
  test('creates a workbook exercising every documented feature', async () => {
    const r = await new ExcelTool().execute({
      actions: [{
        action: 'create',
        outputPath: 'created.xlsx',
        content: {
          sheets: [{
            name: 'Sales',
            columns: [
              { header: 'Product', key: 'product', width: 25 },
              { header: 'Qty',     key: 'qty',     width: 12 },
              { header: 'Price',   key: 'price',   width: 12 },
              { header: 'Total',   key: 'total',   width: 15 },
            ],
            rows: [
              { product: 'A', qty: 100, price: 9.99, total: '=B2*C2' },
              { product: 'B', qty: 50,  price: 19.99 },
              ['Array Row', 1, 2, '=B4+C4'], // array-format row + inline formula
            ],
            headerStyle: { bold: true, fill: '#4472C4', fontColor: '#FFFFFF' },
            freezeRow: 1,
            autoFilter: true,
            formulas: [
              { cell: 'D2', formula: '=B2*C2' },
              { cell: 'D3', formula: 'B3*C3' }, // formula without leading '='
            ],
          }],
        },
      }],
    }, { projectDir: tmpDir });

    expect(r.success).toBe(true);
    expect(r.outputPath).toMatch(/created\.xlsx$/);
    expect(r.fileSize).toBeGreaterThan(0);
    expect(r.sheets).toEqual(['Sales']);
    await fs.access(r.outputPath);
  });

  test('accepts a sheet with only auto-inferred column keys (header → key)', async () => {
    const r = await new ExcelTool().execute({
      actions: [{
        action: 'create',
        outputPath: 'inferred.xlsx',
        content: {
          sheets: [{
            name: 'Inferred',
            columns: [
              { header: 'First Name' },             // key inferred as first_name
              { header: 'Last Name', width: 30 },
            ],
            rows: [{ first_name: 'Ada', last_name: 'Lovelace' }],
          }],
        },
      }],
    }, { projectDir: tmpDir });
    expect(r.success).toBe(true);
  });

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

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

  test('rejects content without sheets array', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{ action: 'create', outputPath: 'x.xlsx', content: { other: 1 } }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/sheets/i);
  });

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

  test('appends .xlsx extension when missing', async () => {
    const r = await new ExcelTool().execute(
      { actions: [{
        action: 'create',
        outputPath: 'no-ext',
        content: { sheets: [{ name: 'a' }] },
      }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(true);
    expect(r.outputPath).toMatch(/no-ext\.xlsx$/);
  });

  test('handles many columns to exercise the column-letter helper (Z→AA boundary)', async () => {
    // 27 columns forces the _getColumnLetter helper into the AA branch.
    const columns = Array.from({ length: 27 }, (_, i) => ({
      header: `Col${i + 1}`, key: `col${i + 1}`, width: 5,
    }));
    const r = await new ExcelTool().execute(
      { actions: [{
        action: 'create',
        outputPath: 'wide.xlsx',
        content: { sheets: [{ name: 'Wide', columns, rows: [], autoFilter: true }] },
      }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(true);
  });

  test('reports ExcelJS load failure during create', async () => {
    const tool = new ExcelTool();
    tool.excelError = 'cached install miss';
    const r = await tool.execute(
      { actions: [{
        action: 'create',
        outputPath: 'should-not-exist.xlsx',
        content: { sheets: [{ name: 'x' }] },
      }] },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/ExcelJS module not available/);
  });
});

// ──────────────────────── _getColumnLetter ────────────────────────────

describe('ExcelTool — _getColumnLetter', () => {
  test('maps 1..26 to A..Z and crosses to AA at 27', () => {
    const tool = new ExcelTool();
    expect(tool._getColumnLetter(1)).toBe('A');
    expect(tool._getColumnLetter(2)).toBe('B');
    expect(tool._getColumnLetter(26)).toBe('Z');
    expect(tool._getColumnLetter(27)).toBe('AA');
    expect(tool._getColumnLetter(52)).toBe('AZ');
    expect(tool._getColumnLetter(53)).toBe('BA');
    expect(tool._getColumnLetter(703)).toBe('AAA'); // 26 + 26*26 + 1
  });
});

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

describe('ExcelTool — lazy loading', () => {
  test('loadExcelJS caches success', async () => {
    const tool = new ExcelTool();
    expect(await tool.loadExcelJS()).toBe(true);
    expect(tool.excelLoaded).toBe(true);
    expect(await tool.loadExcelJS()).toBe(true);
  });

  test('loadExcelJS short-circuits to false after cached error', async () => {
    const tool = new ExcelTool();
    tool.excelError = 'cached miss';
    expect(await tool.loadExcelJS()).toBe(false);
  });
});
