/**
 * Unit tests for the PO Excel import parser.
 * Tests parseSheet() against the real Sample_PO.xlsx fixture and synthetic
 * workbooks built in-memory, without any HTTP or database layer.
 */
import { describe, it, expect, beforeAll } from "vitest";
import { readFileSync, existsSync } from "fs";
import { resolve } from "path";
import * as XLSX from "xlsx";
import { parseSheet, parseWorkbook, cellStr, cellNum } from "@/lib/po-import-parser";

const SAMPLE_PATH = resolve(__dirname, "../../../../Prototype/Sample_PO.xlsx");
// The original Sample_PO.xlsx lives outside the repo, so these fixture-backed
// tests skip wherever the file is absent (CI, other machines). The synthetic
// workbook tests below exercise the parser everywhere.
const HAS_SAMPLE = existsSync(SAMPLE_PATH);

// ── helpers ───────────────────────────────────────────────────────────────────

/** Build a minimal XLSX WorkSheet from a 2-D array of values. */
function sheetFrom(rows: (string | number | null)[][]): XLSX.WorkSheet {
  return XLSX.utils.aoa_to_sheet(rows);
}

/** Build a Workbook with a single sheet. */
function wbFrom(rows: (string | number | null)[][]): XLSX.WorkBook {
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, sheetFrom(rows), "Sheet1");
  return wb;
}

function wbBuffer(rows: (string | number | null)[][]): Buffer {
  return Buffer.from(XLSX.write(wbFrom(rows), { type: "buffer", bookType: "xlsx" }));
}

// ── cellStr / cellNum ─────────────────────────────────────────────────────────

describe("cellStr", () => {
  it("returns the string value of a cell", () => {
    const sheet = sheetFrom([["hello", 42, null]]);
    expect(cellStr(sheet, 0, 0)).toBe("hello");
  });

  it("coerces a number to string", () => {
    const sheet = sheetFrom([[null, 99]]);
    expect(cellStr(sheet, 0, 1)).toBe("99");
  });

  it("returns empty string for a missing cell", () => {
    const sheet = sheetFrom([[]]);
    expect(cellStr(sheet, 5, 5)).toBe("");
  });

  it("trims whitespace", () => {
    const sheet = sheetFrom([[" padded "]]);
    expect(cellStr(sheet, 0, 0)).toBe("padded");
  });
});

describe("cellNum", () => {
  it("returns the numeric value of a cell", () => {
    const sheet = sheetFrom([[42]]);
    expect(cellNum(sheet, 0, 0)).toBe(42);
  });

  it("returns 0 for a missing cell", () => {
    const sheet = sheetFrom([[]]);
    expect(cellNum(sheet, 0, 5)).toBe(0);
  });

  it("returns 0 for a non-numeric string cell", () => {
    const sheet = sheetFrom([["text"]]);
    expect(cellNum(sheet, 0, 0)).toBe(0);
  });

  it("parses a numeric string", () => {
    const sheet = sheetFrom([["182"]]);
    expect(cellNum(sheet, 0, 0)).toBe(182);
  });
});

// ── parseSheet against real Sample_PO.xlsx ───────────────────────────────────

describe.skipIf(!HAS_SAMPLE)("parseSheet — Sample_PO.xlsx", () => {
  let parsed: ReturnType<typeof parseSheet>;

  beforeAll(() => {
    const buffer = readFileSync(SAMPLE_PATH);
    const wb = XLSX.read(buffer, { type: "buffer" });
    parsed = parseSheet(wb.Sheets[wb.SheetNames[0]]);
  });

  it("extracts exactly one line item (T&C rows must not appear)", () => {
    expect(parsed.lineItems).toHaveLength(1);
  });

  it("extracts the correct line item name", () => {
    expect(parsed.lineItems[0].name).toBe("Eni EP 80W90 GEAR OIL");
  });

  it("extracts quantity 1050", () => {
    expect(parsed.lineItems[0].quantity).toBe(1050);
  });

  it("extracts unit price 182", () => {
    expect(parsed.lineItems[0].unitPrice).toBe(182);
  });

  it("extracts unit 'Ltr'", () => {
    expect(parsed.lineItems[0].unit).toBe("Ltr");
  });

  it("extracts GST rate 0.18 (raw value already <1)", () => {
    expect(parsed.lineItems[0].gstRate).toBeCloseTo(0.18);
  });

  it("extracts vendor name", () => {
    expect(parsed.vendorName).toBe("Apar Industries Ltd");
  });

  it("extracts PI/Quotation number", () => {
    expect(parsed.piQuotationNo).toBe("Verbal");
  });

  it("extracts PO number", () => {
    expect(parsed.poNumber).toMatch(/PMS\/HNR3\/056/);
  });

  it("extracts delivery T&C (strips 'DELIVERY :' prefix)", () => {
    expect(parsed.tcDelivery).toMatch(/Within 4 to 5 days/i);
  });

  it("extracts payment terms T&C (strips 'PAYMENT TERMS:' prefix)", () => {
    expect(parsed.tcPaymentTerms).toMatch(/30 days/i);
  });

  it("extracts place of delivery", () => {
    expect(parsed.placeOfDelivery).toMatch(/CBD Belapur/i);
  });
});

// ── parseSheet — synthetic cases ──────────────────────────────────────────────

describe("parseSheet — synthetic edge cases", () => {
  /** Build a minimal valid sheet: header row at 14, one item at 15. */
  function buildSheet(overrides: Partial<{
    snRow15: string | number;
    descRow15: string;
    unitRow15: string;
    qtyRow15: number;
    priceRow15: number;
    gstRow15: number;
    extraRows: (string | number | null)[][];
  }> = {}) {
    const rows: (string | number | null)[][] = Array.from({ length: 40 }, () =>
      Array(10).fill(null)
    );
    // Row 4: PO number
    rows[4][2] = "TEST-PO-001";
    // Row 5: PI quotation
    rows[5][2] = "INV-999";
    // Row 8: delivery address
    rows[8][2] = "Test Delivery Address";
    // Row 12: vendor
    rows[12][2] = "Test Vendor";
    // Row 14: header
    rows[14] = ["S.N.", "Description", null, "Unit", "Qnty", "Unit price", "Taxable", "GST%", "Total"];
    // Row 15: one line item
    rows[15][0] = overrides.snRow15 ?? 1;
    rows[15][1] = overrides.descRow15 ?? "Test Item";
    rows[15][3] = overrides.unitRow15 ?? "pc";
    rows[15][4] = overrides.qtyRow15 ?? 5;
    rows[15][5] = overrides.priceRow15 ?? 100;
    rows[15][7] = overrides.gstRow15 ?? 0.18;
    // Extra rows appended after row 15
    if (overrides.extraRows) {
      overrides.extraRows.forEach((row, i) => {
        rows[16 + i] = row.concat(Array(10).fill(null)).slice(0, 10);
      });
    }
    return sheetFrom(rows);
  }

  it("parses a single synthetic line item correctly", () => {
    const result = parseSheet(buildSheet());
    expect(result.lineItems).toHaveLength(1);
    expect(result.lineItems[0].name).toBe("Test Item");
    expect(result.lineItems[0].quantity).toBe(5);
    expect(result.lineItems[0].unitPrice).toBe(100);
  });

  it("converts a GST rate > 1 (percentage) to a fraction", () => {
    const result = parseSheet(buildSheet({ gstRow15: 18 }));
    expect(result.lineItems[0].gstRate).toBeCloseTo(0.18);
  });

  it("leaves a GST rate already < 1 unchanged", () => {
    const result = parseSheet(buildSheet({ gstRow15: 0.05 }));
    expect(result.lineItems[0].gstRate).toBeCloseTo(0.05);
  });

  it("falls back to gstRate 0.18 when GST cell is 0", () => {
    const result = parseSheet(buildSheet({ gstRow15: 0 }));
    expect(result.lineItems[0].gstRate).toBeCloseTo(0.18);
  });

  it("falls back to unit 'pc' when unit cell is empty", () => {
    const result = parseSheet(buildSheet({ unitRow15: "" }));
    expect(result.lineItems[0].unit).toBe("pc");
  });

  it("stops at INSTRUCTIONS TO VENDORS row — T&C rows not included", () => {
    const result = parseSheet(buildSheet({
      extraRows: [
        // row 16: "INSTRUCTIONS TO VENDORS" in col 0
        ["INSTRUCTIONS TO VENDORS", null, null, null, null, null, null, null, null, null],
        // row 17: T&C item — must NOT appear in lineItems
        [1, "Please quote this PO number in all correspondence", null, null, null, null, null, null, null, null],
        [2, "DELIVERY : Within 4 days", null, null, null, null, null, null, null, null],
      ],
    }));
    expect(result.lineItems).toHaveLength(1);
  });

  it("skips rows where both qty and unitPrice are 0 (text-only rows)", () => {
    const result = parseSheet(buildSheet({
      extraRows: [
        [2, "Some descriptive text but no price", null, null, 0, 0, null, null, null, null],
      ],
    }));
    expect(result.lineItems).toHaveLength(1);
  });

  it("skips fully empty rows", () => {
    const result = parseSheet(buildSheet({
      extraRows: [
        [null, null, null, null, null, null, null, null, null, null],
        [null, null, null, null, null, null, null, null, null, null],
        [2, "Second Item", null, "set", 3, 500, 1500, 0.18, 1770, null],
      ],
    }));
    expect(result.lineItems).toHaveLength(2);
    expect(result.lineItems[1].name).toBe("Second Item");
  });

  it("returns empty lineItems for a blank sheet", () => {
    const result = parseSheet(sheetFrom([]));
    expect(result.lineItems).toHaveLength(0);
  });
});

// ── parseWorkbook ─────────────────────────────────────────────────────────────

describe("parseWorkbook", () => {
  it.skipIf(!HAS_SAMPLE)("parses the real Sample_PO.xlsx and returns one result", () => {
    const buffer = readFileSync(SAMPLE_PATH);
    const results = parseWorkbook(buffer);
    expect(results).toHaveLength(1);
    expect(results[0].lineItems).toHaveLength(1);
  });

  it("returns empty array for a workbook with no parseable PO data", () => {
    const buffer = wbBuffer([["Header only"], ["no PO data here"]]);
    const results = parseWorkbook(buffer);
    expect(results).toHaveLength(0);
  });

  it("returns empty array for a non-XLSX buffer (errors are swallowed)", () => {
    // parseWorkbook catches XLSX.read errors — callers must check the result length
    let result: ReturnType<typeof parseWorkbook>;
    try {
      result = parseWorkbook(Buffer.from("not-an-xlsx-file"));
    } catch {
      // Some xlsx versions throw, others return garbage — both are acceptable
      result = [];
    }
    // Either it returned [] or threw; in both cases no PO data should be present
    expect(Array.isArray(result) ? result : []).toHaveLength(0);
  });
});
