import { describe, expect, it } from 'vitest';
import ExcelJS from 'exceljs';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { resolveThemePalette, applyTint, extractSheetStyles } from './excel-styles.js';

async function roundTrip(build: (ws: ExcelJS.Worksheet) => void): Promise<ExcelJS.Worksheet> {
  const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet('S'); build(ws);
  const p = join(mkdtempSync(join(tmpdir(), 'xls-')), 't.xlsx');
  await wb.xlsx.writeFile(p);
  const wb2 = new ExcelJS.Workbook(); await wb2.xlsx.readFile(p);
  return wb2.getWorksheet('S')!;
}

describe('resolveThemePalette', () => {
  it('parses srgbClr + sysClr and applies the lt/dk swap', () => {
    const xml = `<a:theme><a:themeElements><a:clrScheme name="x">
      <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
      <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
      <a:dk2><a:srgbClr val="44546A"/></a:dk2>
      <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
      <a:accent1><a:srgbClr val="4472C4"/></a:accent1>
      <a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
      <a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
      <a:accent4><a:srgbClr val="FFC000"/></a:accent4>
      <a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
      <a:accent6><a:srgbClr val="70AD47"/></a:accent6>
      <a:hlink><a:srgbClr val="0563C1"/></a:hlink>
      <a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
    </a:clrScheme></a:themeElements></a:theme>`;
    const pal = resolveThemePalette(xml);
    expect(pal[0]).toBe('#FFFFFF'); // theme idx 0 = Light1 = lt1
    expect(pal[1]).toBe('#000000'); // theme idx 1 = Dark1 = dk1
    expect(pal[4]).toBe('#4472C4'); // accent1
  });
  it('returns [] on missing scheme', () => { expect(resolveThemePalette(undefined)).toEqual([]); });
});

describe('applyTint', () => {
  it('lightens with positive tint and darkens with negative', () => {
    const lighter = applyTint('#4472C4', 0.4);
    const darker = applyTint('#4472C4', -0.4);
    expect(lighter).not.toBe('#4472C4');
    expect(darker).not.toBe('#4472C4');
    // positive tint raises luminance
    const lum = (h: string) => parseInt(h.slice(1,3),16)+parseInt(h.slice(3,5),16)+parseInt(h.slice(5,7),16);
    expect(lum(lighter)).toBeGreaterThan(lum('#4472C4'));
    expect(lum(darker)).toBeLessThan(lum('#4472C4'));
  });
});

describe('extractSheetStyles', () => {
  it('dedups identical fills into one legend entry with a merged range', async () => {
    const ws = await roundTrip((ws) => {
      for (const a of ['A1','B1','C1','D1']) ws.getCell(a).fill = { type:'pattern', pattern:'solid', fgColor:{ argb:'FFFFF2CC' } };
    });
    const r = extractSheetStyles(ws, [], 250);
    expect(r.legend.length).toBe(1);
    expect(r.legend[0].sig).toContain('#FFF2CC');
    expect(r.assignments[0].ranges).toEqual(['A1:D1']); // horizontal run merged
  });
  it('captures a decorated but EMPTY cell', async () => {
    const ws = await roundTrip((ws) => { ws.getCell('B5').fill = { type:'pattern', pattern:'solid', fgColor:{ argb:'FFFF0000' } }; });
    const r = extractSheetStyles(ws, [], 250);
    expect(r.assignments.some(a => a.ranges.includes('B5'))).toBe(true);
  });
  it('reports font bold + color', async () => {
    const ws = await roundTrip((ws) => { ws.getCell('A1').value='x'; ws.getCell('A1').font={ bold:true, color:{ argb:'FF9C0006' } }; });
    const r = extractSheetStyles(ws, [], 250);
    expect(r.legend[0].sig).toMatch(/font:.*bold/);
    expect(r.legend[0].sig).toContain('#9C0006');
  });
  it('merges vertically-adjacent identical column-runs into a rectangle', async () => {
    const ws = await roundTrip((ws) => {
      for (let row=2; row<=20; row++) ws.getCell(`B${row}`).fill={ type:'pattern', pattern:'solid', fgColor:{ argb:'FF00FF00' } };
    });
    const r = extractSheetStyles(ws, [], 250);
    expect(r.assignments[0].ranges).toEqual(['B2:B20']);
  });
  it('respects max_style_ranges cap', async () => {
    const ws = await roundTrip((ws) => {
      // 5 disjoint single cells with distinct fills (no merge possible)
      const colors=['FFAA0000','FF00AA00','FF0000AA','FFAAAA00','FF00AAAA'];
      colors.forEach((c,i)=>{ ws.getCell(`A${i*2+1}`).fill={type:'pattern',pattern:'solid',fgColor:{argb:c}}; });
    });
    const r = extractSheetStyles(ws, [], 2);
    expect(r.truncated).toBe(true);
  });

  // Fix 4 (M-1): merges
  it('includes merged cell range in result merges', async () => {
    const ws = await roundTrip((ws) => {
      ws.getCell('A1').value = 'merged';
      ws.mergeCells('A1:C2');
    });
    const r = extractSheetStyles(ws, [], 250);
    expect(r.merges).toContain('A1:C2');
  });

  // Fix 4 (M-2): conditional formatting — round-trip keeps conditionalFormattings
  it('detects conditional formatting', async () => {
    const ws = await roundTrip((ws) => {
      ws.addConditionalFormatting({
        ref: 'A1:A10',
        rules: [{ type: 'expression', formulae: ['TRUE'], style: { fill: { type: 'pattern', pattern: 'solid', bgColor: { argb: 'FFFF0000' } } } }],
      });
    });
    // ExcelJS preserves conditionalFormattings after round-trip (empirically verified)
    const r = extractSheetStyles(ws, [], 250);
    expect(r.conditionalFormatting).toBe(true);
  });

  // Fix 4 (M-3): border + numFmt + alignment
  it('captures border, numFmt, and alignment in cell signature', async () => {
    const ws = await roundTrip((ws) => {
      const cell = ws.getCell('B2');
      cell.value = 0.42;
      cell.border = { bottom: { style: 'thin' } };
      cell.numFmt = '0.00%';
      cell.alignment = { horizontal: 'center' };
    });
    const r = extractSheetStyles(ws, [], 250);
    expect(r.legend.length).toBeGreaterThan(0);
    const sig = r.legend[0]!.sig;
    expect(sig).toContain('border:');
    expect(sig).toContain('numFmt:"0.00%"');
    expect(sig).toContain('align:');
  });

  // Fix 4 (I-1 range honoring): rangeBounds filters cells
  it('honors rangeBounds — includes A1 but excludes Z99', async () => {
    const ws = await roundTrip((ws) => {
      ws.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF0000FF' } };
      ws.getCell('Z99').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF00FF00' } };
    });
    const r = extractSheetStyles(ws, [], 250, { minRow: 1, maxRow: 5, minCol: 1, maxCol: 5 });
    const allRanges = r.assignments.flatMap(a => a.ranges);
    expect(allRanges.some(rng => rng === 'A1' || rng.startsWith('A1:'))).toBe(true);
    expect(allRanges.every(rng => !rng.includes('Z99') && !rng.startsWith('Z'))).toBe(true);
  });
});
