import type { OcrPage } from "../ocr/ocr.provider";
import {
  detectProvider,
  normalizeCadastralKey,
  normalizeZofematKey,
  parseStatement,
} from "./statement-parser";

/**
 * Every string in this file is a verbatim excerpt of what the OCR engine
 * actually returned for a real receipt — misreads, dropped spaces, mangled
 * accents and all. That is the point: these are the specific ways these five
 * layouts have been observed to fail, and the assertions pin down what the
 * parser is supposed to do about each one. Inventing clean input here would
 * test nothing, because clean input was never the problem.
 */
function page(text: string): OcrPage {
  return { text, words: [], confidence: 0.9 };
}

describe("detectProvider", () => {
  it("reads a Rosarito predial receipt as predial, not as a water bill", () => {
    // "Clave Catastral" is also a CESPT structural marker, so a predial page
    // whose header OCR'd badly must still not be claimed by the CESPT rule.
    expect(
      detectProvider(
        "e | Clave Catastral. KP-128-105 IMPUESTO PREDIAL ea rita\n" +
          "TASA | VALOR FISCAL | BIMESTRES | INCISO. | IMPUESTO",
      ),
    ).toBe("PREDIAL ROSARITO");
  });

  it("keeps telling the three municipalities apart by their RFC", () => {
    expect(detectProvider("R.F.C. ATB-541201-KK2")).toBe("PREDIAL TIJUANA");
    expect(detectProvider("R.F.C. AMP-981201-HJ4")).toBe("PREDIAL ROSARITO");
    expect(detectProvider("MEN-540301-9J5")).toBe("PREDIAL ENSENADA");
  });

  it("does not let the CFE rule claim a gas bill over 'PERIODO FACTURADO'", () => {
    expect(
      detectProvider("Orden de Facturación: 000009801640\nPERIODO FACTURADO: 20260630-20260630"),
    ).toBe("GAS TIJUANA");
  });
});

describe("normalizeCadastralKey", () => {
  it("keeps a letter in the third position instead of digitising it", () => {
    // `MMB01041` is a real key on file; mapping its B to 8 produced a key that
    // matches no property at all.
    expect(normalizeCadastralKey("MM-B01-041", [])).toBe("MMB01041");
  });

  it("repairs the spurious I tesseract inserts into the prefix", () => {
    expect(normalizeCadastralKey("MIM-200-010", [])).toBe("MM200010");
  });

  it("digitises confusable glyphs from position four onward", () => {
    expect(normalizeCadastralKey("KP-1O8-O45", [])).toBe("KP108045");
  });

  it("flags a prefix it had to truncate", () => {
    const notes: string[] = [];
    expect(normalizeCadastralKey("KPX-128-106", notes)).toBe("KP128106");
    expect(notes).toHaveLength(1);
  });
});

describe("parsePredialTijuana", () => {
  const TIJUANA = page(
    "Hats | AYUNTAMIENTO DE TIJUANA, BC $2,613.00 23/01/2026\n" +
      "y) TELEFONO: 973-7000 R.F.C. ATB-541201-KK2\n" +
      "ER AÑO VALOR FISCAL TASA IMPUESTO |CONCEPTO IMPORTE\n" +
      "ED ca 2026 1,207,15778 246 2,969.61 1102 - IMPUESTO PREDIAL 2,969.61\n" +
      "55164964310126000002613000054192\n" +
      "se 0 O (54427 [a] | TOTALAPAGAR: 2,613.00\n" +
      "Dc 1097 : FECHA VENCE : 31/ENE/2026",
  );

  it("splits the payment barcode into account, deadline and amount", () => {
    const p = parseStatement(TIJUANA);
    expect(p.provider).toBe("PREDIAL TIJUANA");
    expect(p.serviceKind).toBe("PROPERTY_TAX");
    expect(p.accountRef).toBe("55164964");
    expect(p.amount).toBe(2613);
    expect(p.dueDate?.toISOString().slice(0, 10)).toBe("2026-01-31");
    expect(p.period).toBe("2026");
  });

  it("reads the printed total even when the space in the label is lost", () => {
    // The real page OCR'd the label as "TOTALAPAGAR:", and it is that reading
    // that cross-checks the barcode's amount.
    expect(parseStatement(TIJUANA).crossChecked).toBe(true);
  });

  it("refuses to trust a barcode the printed total contradicts", () => {
    const p = parseStatement(
      page(
        "R.F.C. ATB-541201-KK2\n" +
          "55164964310126000002613000054192\n" +
          "TOTAL A PAGAR: 9,613.00\nFECHA VENCE : 31/ENE/2026",
      ),
    );
    expect(p.crossChecked).toBe(false);
    expect(p.notes.join(" ")).toContain("no coincide");
  });
});

describe("parsePredialRosarito", () => {
  it("takes the rounded Total, not the Sub Total printed above it", () => {
    const p = parseStatement(
      page(
        "AYUNTAMIENTO MUNICIPAL DE PLAYAS DE ROSARITO, B.C.\n" +
          "Ce Clave Catastral: + JR-400-008 7 | IMPUESTO PREDIAL\n" +
          "SUPERFICIE: 228.31 ZONA 30025 “Redondeo IT049 -$0.39 Sub Total $5,409.39\n" +
          "¿XTEMPORANEO DESPUES DE: 31/01/2026 Elaboro: MGLG\n" +
          "Total | $5,409.00\n" +
          "| Periodo por Pagar: 2026/1 2026/6",
      ),
    );
    expect(p.cadastralKey).toBe("JR400008");
    expect(p.amount).toBe(5409);
    expect(p.dueDate?.toISOString().slice(0, 10)).toBe("2026-01-31");
    expect(p.period).toBe("2026");
  });

  it("is not fooled by the unspaced 'SubTotal' spelling", () => {
    // This exact page read $9,624.85 off a receipt for $9,625.00 while the
    // lookbehind still assumed a space.
    const p = parseStatement(
      page(
        "AMP-981201-HJ4 IMPUESTO PREDIAL\n" +
          "SUPERFICIE. 367.62 ZONA:30151 | Redondco 17049 $0.15 SubTotal $9,624.85\n" +
          ": Total | $9,625.00",
      ),
    );
    expect(p.amount).toBe(9625);
  });
});

describe("parsePredialEnsenada", () => {
  const totals = (tail: string) =>
    page(
      "IMPRESION MAQUINA REGISTRADORA ez | MUNICIPIO DE ENSENADA\n" +
        "+7] DATOS. DEL.CAUSANTE alta A pe CLAVE MM-200-010 2 CUENTA\n" +
        `ES g € S| TOTALES 12,744.47 0.00 0.00 324.56 0.00 13,069.03 ${tail} |`,
    );

  it("reads the paid total off the TOTALES row however the label OCR'd", () => {
    expect(parseStatement(totals("TOTA LA A $5,797.00")).amount).toBe(5797);
    expect(parseStatement(totals("orAL: M7 z] $14,414.00")).amount).toBe(14414);
    expect(parseStatement(totals("| TOTAL: = $6 246.00")).amount).toBe(6246);
  });

  it("reports no amount rather than one whose $ was misread as an 8", () => {
    // `TOTAL: A 82,203.00` is a $2,203.00 receipt. Posting $82,203 would look
    // entirely ordinary in the ledger, so this page must go to review instead.
    const p = parseStatement(totals("TOTAL: A 82,203.00"));
    expect(p.amount).toBeNull();
    expect(p.notes.join(" ")).toContain("capturarlo a mano");
  });

  it("never falls back to the assessed total on the same row", () => {
    expect(parseStatement(totals("yo: se TE= 58/4690]")).amount).toBeNull();
  });
});

describe("parseGas", () => {
  const gas = (...cuentas: string[]) =>
    page(
      "GTI4608032K2 COMPAÑIA DE GAS DE TIJUANA\n" +
        "Fecha de Vencimiento: 2026/08/08\n" +
        cuentas.map((c) => `Cuenta: ${c}`).join("\n") +
        "\nPERIODO FACTURADO: 20260630-20260630\nTOTAL A PAGAR: $275.82",
    );

  it("strips the printed leading zero to the stored account number", () => {
    const p = parseStatement(gas("0900003463", "0900003463", "0900003463"));
    expect(p.serviceKind).toBe("GAS");
    expect(p.accountRef).toBe("900003463");
    expect(p.amount).toBe(275.82);
    expect(p.dueDate?.toISOString().slice(0, 10)).toBe("2026-08-08");
    expect(p.period).toBe("2026-06");
    expect(p.crossChecked).toBe(true);
  });

  it("takes the majority reading but still sends a disagreement to review", () => {
    const p = parseStatement(gas("0900003463", "0900003463", "0900003468"));
    expect(p.accountRef).toBe("900003463");
    expect(p.crossChecked).toBe(false);
  });

  it("claims no cross-check from a single printing", () => {
    expect(parseStatement(gas("0900003463")).crossChecked).toBeNull();
  });
});

describe("parseZonaFederal", () => {
  /**
   * The Tijuana zona federal receipt, trimmed to the rows the parser reads.
   * Verbatim from page 7 of the August 2026 batch, including the two ways the
   * heading OCR'd: the clave line is struck through by the office's own
   * highlighter, which is what cost two of eight pages their concession clave.
   */
  const zf = (clave: string, body = "") =>
    page(
      "ESIZ <pYl Av. Independencia y Esq. Paseo del CentenaxiaiiArlhnto de Tijuana, B.C.\n" +
        "Teléfono: 9737000 R.F.C. ATB-541201-BK2 0070000146 12:54 PM\n" +
        "Zona Federal Marítimo Terrestre\n" +
        `${clave} Nombre: DENNIS JOHN SEIN Concesión:\n` +
        "Periodo Construcción Tasa  Ornato Tasa Impuesto Actualiza. Recargo Multa Importe\n" +
        "2026-2 / 2026-2 316.40 35.00 0.00 12.11 1,845.66 0.00 27.13 1,000.00 2,872.79\n" +
        "SubTotal 1,845.66 0.00 27.13 1,000.00 2,872.79\n" +
        "Concepto: Derechos de ocupación de Zona Federal Marítimo Terrestre\n" +
        body,
    );

  it("is not claimed by the predial parser that shares its RFC and header", () => {
    // Tijuana bills predial and zona federal from the same treasury, so
    // "Ayuntamiento de Tijuana" and ATB-541201 identify neither on their own.
    expect(detectProvider("R.F.C. ATB-541201-BK2\nZona Federal Marítimo Terrestre")).toBe(
      "ZONA FEDERAL TIJUANA",
    );
    expect(parseStatement(zf("Clave: 14-D -014")).serviceKind).toBe("FEDERAL_ZONE");
  });

  it("still recognises the layout when the heading itself did not survive OCR", () => {
    // Real: page 1 came back as "Zona Ledera) Maritimo Terrestre".
    expect(
      detectProvider("Zona Ledera) Maritimo Terrestre\nClave EJ -012% Nombre: STEFAN"),
    ).toBe("ZONA FEDERAL TIJUANA");
  });

  it("reads the clave through the loose spacing the receipt prints", () => {
    expect(parseStatement(zf("Clave: 14-D -014")).accountRef).toBe("14D014");
    expect(parseStatement(zf("Clave: 14-A-119")).accountRef).toBe("14A119");
  });

  it("keeps the letter instead of digitising it", () => {
    // toDigits maps D to 0 and B to 8; a real 14-D -014 must not become 140014.
    expect(normalizeZofematKey("14-D -014")).toBe("14D014");
    expect(normalizeZofematKey("12-B -013")).toBe("12B013");
  });

  it("takes the payable amount from the SubTotal row, rounded to whole pesos", () => {
    // The municipality rounds and prints the difference as "Ajuste Ley Hacienda
    // Mpal"; 2,872.79 is charged as $2,873.00.
    expect(parseStatement(zf("Clave: 14-D -014")).amount).toBe(2873);
  });

  it("prefers the printed total and cross-checks it against the subtotal", () => {
    const p = parseStatement(zf("Clave: 14-D -014", "Total a pagar $2,873.00"));
    expect(p.amount).toBe(2873);
    expect(p.crossChecked).toBe(true);
  });

  it("sends a printed total that contradicts the subtotal to review", () => {
    const p = parseStatement(zf("Clave: 14-D -014", "Total a pagar $2,973.00"));
    expect(p.crossChecked).toBe(false);
  });

  it("translates the printed bimester into the ledger's own vocabulary", () => {
    expect(parseStatement(zf("Clave: 14-D -014")).period).toBe("MAR/APR");
  });

  it("leaves the clave blank rather than guessing when the marker ate it", () => {
    const p = parseStatement(zf("Clave EJ -012%"));
    expect(p.accountRef).toBeNull();
    expect(p.notes.join(" ")).toContain("clave");
  });
});
