import type { OcrPage } from "../../statements/ocr/ocr.provider";
import {
  detectPolicyProvider,
  parsePolicy,
  type ParsedCoverage,
} from "./policy-parser";

/**
 * Verbatim excerpts of what the GMX portal's translation PDF actually
 * rendered through pdftotext — same convention as the statement parser
 * tests, where invented-clean input would test nothing because clean input
 * is not the failure mode.
 */
function page(text: string): OcrPage {
  return { text, words: [], confidence: 0.95 };
}

describe("detectPolicyProvider", () => {
  it("claims GMX from the brand wordmark on the letterhead", () => {
    expect(
      detectPolicyProvider(
        "Grupo Mexicano de Seguros, S.A. de C.V.\nTecoyotitla 412, Edificio GMX",
      ),
    ).toBe("GMX");
  });

  it("claims GMX from the 'gmx.com.mx' footer URL", () => {
    expect(detectPolicyProvider("JUNTOS EL RIESGO ES MENOR\nwww.gmx.com.mx")).toBe("GMX");
  });
});

describe("parsePolicy / GMX", () => {
  // Verbatim text extracted from ~/Downloads/HC_Folio_000767_Traduccion.pdf via
  // `pdftotext -layout`. Two pages joined by "\n\n".
  const GMX_FULL = page(
    "Multiple Policy\nHome\n" +
      "Policy 007-037-07005947-0000-02 in accordance with the enclosed clauses, to insurance:\n" +
      "Insured JON ASHLEY STRABALA\n" +
      "Additional insured VIVIAN\n" +
      "Legal address BONAMPACK No. EXT26 No.INT 0 COL. Punta Bandera, Tijuana, Baja California, C.P. 22550\n" +
      "ZIP 22550 Income Tax No. XEXX-010101-000\n" +
      "Broker (1176) Jorge Humberto Cuadros\n" +
      "Term 12 months\n" +
      "From 19/07/2026\n" +
      "To 19/07/2027 at twelve hours (noon) Mexico City time.\n" +
      "Currency DOLARES Premium payment CONTADO\n" +
      "Free translation from the Spanish Insurance contract. The English text is just copy given by courtesy. In case of a dispute, the Spanish will prevail over the English version.\n" +
      "Agreed clauses:\n" +
      "•The insured and GMX Hereby declared...\n" +
      "From the above, the present contract shall not be considered under the condition mentioned within article 36-B from the Insurance Companies General Law. Therefore it shall not be required its registration before the Comision National de Seguros y Fianzas.\n" +
      "July 23, 2026\n" +
      "Authority sign.\n" +
      "Grupo Mexicano de Seguros, S.A. de C.V.\n" +
      "Tecoyotitla 412, Edificio GMX\n" +
      "JUNTOS EL RIESGO ES MENOR\n" +
      "www.gmx.com.mx\n\n" +
      "Risk Insured Amount Deductible Loss Participation\n" +
      "Building $350,000.00 Not applies Not applies\n" +
      "Contents $60,000.00 Not applies Not applies\n" +
      "ADDITIONAL RISK\n" +
      "Risk Insured Amount Deductible Loss Participation\n" +
      "Debris removal Building $35,000.00 Not applies Not applies\n" +
      "Debris removal Contents $6,000.00 Not applies Not applies\n" +
      "Outdoors Constructions $10,000.00 5% 10%\n" +
      "Coverage Extention Covered Not applies Not applies\n" +
      "All Risk Covered Not applies Not applies\n" +
      "Earthquake and/or volcanic eruption Covered 2% of the sum insured for each damage structure 20%\n" +
      "Extra Expenses $41,000.00 Not applies Not applies\n" +
      "Robbery with violence $10,000.00 Not applies Not applies\n" +
      "Jewerly $3,900.00 Not applies Not applies\n" +
      "Electronic Equipment $10,000.00 Not applies Not applies\n" +
      "Glasses $10,000.00 Not applies Not applies\n" +
      "Tenant $200,000.00 Not applies Not applies\n" +
      "Family $200,000.00 Not applies Not applies\n" +
      "Family $200,000.00 Not applies Not applies\n" +
      "Domestic workers $7,010.00 Not applies Not applies\n" +
      "VALUES ADDED, HOME GMX",
  );

  it("extracts the policy number, insured name, broker, dates, and currency", () => {
    const p = parsePolicy(GMX_FULL);
    expect(p.provider).toBe("GMX");
    expect(p.policyNumber).toBe("007-037-07005947-0000-02");
    expect(p.insuredName).toBe("JON ASHLEY STRABALA");
    expect(p.additionalInsured).toBe("VIVIAN");
    expect(p.agentName).toBe("Jorge Humberto Cuadros");
    expect(p.policyFrom?.toISOString().slice(0, 10)).toBe("2026-07-19");
    expect(p.policyTo?.toISOString().slice(0, 10)).toBe("2027-07-19");
    expect(p.policyDate?.toISOString().slice(0, 10)).toBe("2026-07-23");
    expect(p.currency).toBe("USD");
    expect(p.zip).toBe("22550");
    expect(p.legalAddress).toContain("BONAMPACK");
    expect(p.premiumPayment).toBe("CONTADO");
  });

  it("extracts every coverage row off the second page table", () => {
    const p = parsePolicy(GMX_FULL);
    const byName = Object.fromEntries(p.coverages.map((c) => [c.risk, c]));
    expect(byName.Building?.insuredAmount).toBe(350000);
    expect(byName.Contents?.insuredAmount).toBe(60000);
    expect(byName["Debris removal Building"]?.insuredAmount).toBe(35000);
    expect(byName["Outdoors Constructions"]?.insuredAmount).toBe(10000);
    expect(byName["Outdoors Constructions"]?.deductible).toBe("5%");
    expect(byName["Outdoors Constructions"]?.lossParticipation).toBe("10%");
    // Free-text coverage cells kept verbatim (the policy form surfaces them
    // as observations, not as numbers).
    expect(byName["Earthquake and/or volcanic eruption"]?.insuredAmount).toBeNull();
    expect(byName["Earthquake and/or volcanic eruption"]?.deductible).toContain("2%");
    expect(byName["Earthquake and/or volcanic eruption"]?.lossParticipation).toBe("20%");
    expect(byName["All Risk"]?.insuredAmount).toBeNull();
    expect(p.coverages.length).toBeGreaterThan(10);
  });

  it("leaves premium fields null on the certificate page and notes it", () => {
    const p = parsePolicy(GMX_FULL);
    expect(p.netPremium).toBeNull();
    expect(p.total).toBeNull();
    expect(p.policyFee).toBeNull();
    expect(p.notes.join(" ")).toMatch(/prima/i);
  });

  it("still parses when the broker parens are missing", () => {
    const p = parsePolicy(
      page(
        "Insured JON ASHLEY STRABALA\nBroker Jorge Humberto Cuadros\n" +
          "From 19/07/2026\nTo 19/07/2027\nCurrency DOLARES\n" +
          "Grupo Mexicano de Seguros",
      ),
    );
    expect(p.agentName).toBe("Jorge Humberto Cuadros");
  });

  it("rejects a page that carries no GMX signal at all", () => {
    const p = parsePolicy(page("Random unrelated document with no policy data."));
    expect(p.provider).toBe("");
    expect(p.notes.join(" ")).toContain("no se reconoció el proveedor");
  });

  it("captures the deductible / loss-participation columns verbatim as strings", () => {
    const p = parsePolicy(GMX_FULL);
    const eq = p.coverages.find((c) => c.risk === "Earthquake and/or volcanic eruption");
    expect(eq).toBeDefined();
    const eqTyped = eq as ParsedCoverage;
    expect(eqTyped.deductible).toContain("sum insured");
    expect(eqTyped.lossParticipation).toBe("20%");
  });
});

/**
 * The second GMX document family: the Spanish PVL "especificación" the office
 * receives as `…-CondicionesParticulares.pdf`. Verbatim excerpts from
 * `007_LGS-HGMX_07006957_01_0-CondicionesParticulares.pdf` through
 * `pdftotext -layout`, indentation included — the column positions and the
 * blank lines between blocks are what the parser reads, so a cleaned-up
 * fixture would test nothing.
 */
describe("parsePolicy / GMX especificación (PVL Hogar)", () => {
  const HEADER =
    "                    ESPECIFICACIÓN QUE SE ADHIERE Y FORMA PARTE INTEGRANTE DE LA PÓLIZA\n" +
    "                                           07-037-07006957-00000-01\n" +
    "\n";

  const GMX_ESPEC = page(
    HEADER +
      "\n" +
      "                                            Nombre del asegurado EMMER . KATHLEEN\n" +
      "\n" +
      "                                          Tipo Persona Asegurada Propietario\n" +
      "\n" +
      "                                               Ubicación del riesgo LOS PELICANOS ESTE NO. 98 Col. LAS GAVIOTAS PLAYAS\n" +
      "                                                                    DE ROSARITO BAJA CALIFORNIA 22713\n" +
      "\n" +
      "                                      Características del Inmueble Casa Tipo constructivo Combinado: Macizo y Madera.\n" +
      "                                                                   Consta de 2 pisos incluyendo sótanos y planta baja.\n" +
      "\n" +
      "                                             -500 mts.cuerpo agua SI\n" +
      "\n" +
      "                                              Asegurado Adicional\n" +
      "\n" +
      "PVL Hogar - GMX Seguros                                                     Página: 1 de 10\n" +
      HEADER +
      "\n" +
      " SECCIÓN INCENDIO EDIFICIO Y CONTENIDOS\n" +
      "\n" +
      " EDIFICIO\n" +
      "\n" +
      " Límite Máximo de Responsabilidad:\n" +
      " $200,000.00 USD\n" +
      "\n" +
      " Quedan amparados los muros de contención y bardas, así como puertas y portones, hasta un sublimite de $ 50,000.00 M.N. o su\n" +
      " equivalente en dólares americanos, o hasta el 10% de la suma asegurada de la sección de Edificio, lo que resulte menor.\n" +
      "\n" +
      "\n" +
      " CONTENIDOS\n" +
      "\n" +
      " Límite Máximo de Responsabilidad:\n" +
      " $20,000.00 USD\n" +
      "\n" +
      "    2. Terremoto o erupción volcánica: Sección Edificio EXCLUIDO, Sección Contenidos EXCLUIDO\n" +
      "\n" +
      "    3. Fenómenos hidrometeorológicos: Sección Edificio $200,000.00 USD, Sección Contenidos $20,000.00 USD\n" +
      "\n" +
      " Riesgos adicionales.\n" +
      "\n" +
      " Remoción de escombros\n" +
      "\n" +
      "    Límite Máximo de Responsabilidad:\n" +
      "    Edificio\n" +
      "    $20,000.00 USD\n" +
      "    Contenidos\n" +
      "    $2,000.00 USD\n" +
      "\n" +
      " Gastos extraordinarios para casa habitación\n" +
      "\n" +
      " En caso de siniestro por los riesgos cubiertos en esta póliza, GMX Seguros pagará la renta de casa o departamento, casa de\n" +
      " huéspedes u hotel cuando se asegure el inmueble, así como los gastos de mudanza, seguro de transporte del menaje de casa y\n" +
      " efectuados.\n" +
      "\n" +
      "    Límite Máximo de Responsabilidad:\n" +
      "    $22,000.00 USD\n" +
      "    Periodo de indemnización: 4 meses.\n" +
      "\n" +
      " Bienes a la Intemperie:\n" +
      "\n" +
      "\n" +
      "     5 POR CIENTO SOBRE SUMA ASEGURADA, 20 PORCIENTO DE PARTICIPACIÓN A CARGO DEL ASEGURADO DE TODA\n" +
      "     Y CADA PÉRDIDA.\n" +
      "\n" +
      "\n" +
      "     Límite Máximo de Responsabilidad: $10,000.00 USD\n" +
      "\n" +
      " DEDUCIBLES:\n" +
      "\n" +
      " El procedimiento que se seguirá para la aplicación de deducibles en caso de que la póliza cuente con cláusula inflacionaria en todas\n" +
      " y/o en algunas de sus coberturas será como sigue:\n" +
      "\n" +
      " Fenómenos hidrometeorológicos\n" +
      " Zona: A2\n" +
      " Deducible\n" +
      "     Edificio: 1 POR CIENTO SOBRE SUMA ASEGURADA\n" +
      "     Coaseguro:\n" +
      "     Zona 1: (INTERIOR) Participación a cargo del asegurado del 10% de toda y cada pérdida.\n" +
      "     Zona 2: Participación a cargo del asegurado del 10% de toda y cada pérdida.\n" +
      "\n" +
      "     Deducible\n" +
      "     Contenidos: 1 POR CIENTO SOBRE SUMA ASEGURADA\n" +
      "\n" +
      " II.- SECCIÓN DIVERSOS MISCELÁNEOS\n" +
      "\n" +
      " ROBO DE CONTENIDOS\n" +
      "\n" +
      " Límite de Responsabilidad:\n" +
      " $4,000.00 USD\n" +
      "\n" +
      "\n" +
      " Deducible:\n" +
      " Sin deducible\n" +
      "\n" +
      "\n" +
      " Sublímites:\n" +
      " Joyas, artículos de oro y plata, armas, relojes, pieles, piedras preciosas montadas, colecciones, obras de arte y demás que por su\n" +
      "\n" +
      "PVL Hogar - GMX Seguros                                                     Página: 7 de 10\n" +
      HEADER +
      "\n" +
      " naturaleza se consideran como objetos de difícil o imposible reposición\n" +
      "\n" +
      "\n" +
      " Límite de Responsabilidad:\n" +
      "\n" +
      " $2,000.00 USD\n" +
      "\n" +
      "\n" +
      " Deducible:\n" +
      " Sin deducible\n" +
      "\n" +
      " Las condiciones generales que forman parte de la presente póliza son las identificadas bajo el nombre:\n" +
      " W_HogarGMX_12.11.2025.pdf\n" +
      "\n" +
      "PVL Hogar - GMX Seguros                                                    Página: 10 de 10\n",
  );

  const byRisk = (p: ReturnType<typeof parsePolicy>) =>
    Object.fromEntries(p.coverages.map((c) => [c.risk, c]));

  it("reads a policy number whose groups are not the caratula's widths", () => {
    // 2-3-8-5-2 here vs 3-3-8-4-2 on the English caratula. Pinning the widths
    // reads one family and returns null on the other.
    expect(parsePolicy(GMX_ESPEC).policyNumber).toBe("07-037-07006957-00000-01");
  });

  it("reads the insured, the risk location across its wrapped line, and the ZIP", () => {
    const p = parsePolicy(GMX_ESPEC);
    expect(p.provider).toBe("GMX");
    expect(p.insuredName).toBe("EMMER . KATHLEEN");
    expect(p.legalAddress).toBe(
      "LOS PELICANOS ESTE NO. 98 Col. LAS GAVIOTAS PLAYAS DE ROSARITO BAJA CALIFORNIA 22713",
    );
    expect(p.zip).toBe("22713");
    // The cell is printed but empty on this policy — an empty label must not
    // capture the next line of the form.
    expect(p.additionalInsured).toBeNull();
  });

  it("leaves the fields this document does not carry null, and says so", () => {
    const p = parsePolicy(GMX_ESPEC);
    expect(p.policyFrom).toBeNull();
    expect(p.policyTo).toBeNull();
    expect(p.policyDate).toBeNull();
    expect(p.agentName).toBeNull();
    expect(p.netPremium).toBeNull();
    expect(p.total).toBeNull();
    // The note must tell the reviewer to key them in — those three are
    // captured by hand on this layout — and must say what silently breaks if
    // the vigencia is left empty.
    const notes = p.notes.join(" ");
    expect(notes).toMatch(/no trae vigencia, agente ni prima/i);
    expect(notes).toMatch(/captúrelos a mano/i);
    expect(notes).toMatch(/avisos de renovación/i);
  });

  it("takes the currency from the printed limits, not from the M.N. sublimits", () => {
    // The body prose quotes sublimits in pesos ("$ 50,000.00 M.N."); every
    // limit is in USD, and only the limits vote.
    expect(parsePolicy(GMX_ESPEC).currency).toBe("USD");
  });

  it("reads each coverage under its own heading", () => {
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c.EDIFICIO?.insuredAmount).toBe(200000);
    expect(c.CONTENIDOS?.insuredAmount).toBe(20000);
    expect(c["ROBO DE CONTENIDOS"]?.insuredAmount).toBe(4000);
    expect(c["ROBO DE CONTENIDOS"]?.deductible).toBe("Sin deducible");
  });

  it("splits a limit printed under Edificio / Contenidos sub-labels", () => {
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c["Remoción de escombros — Edificio"]?.insuredAmount).toBe(20000);
    expect(c["Remoción de escombros — Contenidos"]?.insuredAmount).toBe(2000);
  });

  it("names a coverage after its heading, not after the wrapped tail of the prose above it", () => {
    // Walking back from the limit hits "efectuados." — short, and the only
    // thing separating it from a heading is that it is not preceded by a
    // blank line.
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c["Gastos extraordinarios para casa habitación"]?.insuredAmount).toBe(22000);
    expect(c["efectuados."]).toBeUndefined();
  });

  it("reads a limit printed on the label's own line", () => {
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c["Bienes a la Intemperie"]?.insuredAmount).toBe(10000);
  });

  it("reads a deductible stated as a sentence above the limit", () => {
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c["Bienes a la Intemperie"]?.deductible).toBe(
      "5 POR CIENTO SOBRE SUMA ASEGURADA, 20 PORCIENTO DE PARTICIPACIÓN A CARGO DEL ASEGURADO DE TODA Y CADA PÉRDIDA.",
    );
  });

  it("never borrows a neighbouring coverage's prose as a deductible", () => {
    // "…o hasta el 10% de la suma asegurada de la sección de Edificio" is a
    // sublimit rule for EDIFICIO, printed two paragraphs above CONTENIDOS.
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c.CONTENIDOS?.deductible).toBeNull();
    expect(c.EDIFICIO?.deductible).toBeNull();
  });

  it("does not read the page-level DEDUCIBLES paragraph as a deductible", () => {
    const p = parsePolicy(GMX_ESPEC);
    expect(
      p.coverages.some((c) => (c.deductible ?? "").includes("cláusula inflacionaria")),
    ).toBe(false);
  });

  it("reads a sublimit block as a sublimit OF the coverage above it", () => {
    // The amount sits after a blank line AND a page break, and the block's
    // own heading ("Sublímites:") names no risk.
    const c = byRisk(parsePolicy(GMX_ESPEC));
    expect(c["ROBO DE CONTENIDOS — sublímite"]?.insuredAmount).toBe(2000);
  });

  it("records an excluded catastrophic risk as excluded, never as zero", () => {
    const p = parsePolicy(GMX_ESPEC);
    const quake = p.coverages.filter((c) => /Terremoto/i.test(c.risk));
    expect(quake).toHaveLength(2);
    for (const c of quake) {
      expect(c.risk).toMatch(/EXCLUIDO/);
      // A coverage insured for $0 and an excluded coverage are the same
      // number and very different facts.
      expect(c.insuredAmount).toBeNull();
    }
  });

  it("attaches the hydrometeorological deductible and coinsurance from its own block", () => {
    const c = byRisk(parsePolicy(GMX_ESPEC));
    const building = c["Fenómenos hidrometeorológicos — Sección Edificio"];
    expect(building?.insuredAmount).toBe(200000);
    expect(building?.deductible).toBe("1 POR CIENTO SOBRE SUMA ASEGURADA");
    expect(building?.lossParticipation).toBe("10%");
    expect(c["Fenómenos hidrometeorológicos — Sección Contenidos"]?.insuredAmount).toBe(20000);
  });

  it("carries the underwriting context the fields have no home for", () => {
    const notes = parsePolicy(GMX_ESPEC).notes.join(" | ");
    expect(notes).toMatch(/tipo de persona asegurada: Propietario/);
    expect(notes).toMatch(/características del inmueble: Casa/);
    expect(notes).toMatch(/cuerpo de agua/);
    expect(notes).toMatch(/zona catastrófica declarada: A2/);
    expect(notes).toMatch(/W_HogarGMX_12\.11\.2025\.pdf/);
  });
});